commit d4d59fa3395012ca37ba12665da4ec11c7dcf9cb Author: Dustin Howett Date: Thu May 2 15:29:04 2019 -0700 Initial release of the Windows Terminal source code This commit introduces all of the Windows Terminal and Console Host source, under the MIT license. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..bf0e54a56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* -text + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4071eab24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,245 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +objfre/ +objchk/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# Visual Studio Code cache/options directory +.vscode/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +*.opendb +*.db + +# Windows Build System files +build*.dbb +build*.err +build*.evt +build*.log +build*.prf +build*.trc +build*.rec +build*.wrn +build*.metadata + +# .razzlerc.cmd file - used by dev environment +tools/.razzlerc.* +# message compiler output +MSG*.bin +/*.exe + +# python +*.pyc + +**Generated Files/ +**/Merged/* +**/Unmerged/* +profiles.json +*.metaproj diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..294731978 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "dep/gsl"] + path = dep/gsl + url = https://github.com/Microsoft/gsl +[submodule "dep/wil"] + path = dep/wil + url = https://github.com/Microsoft/wil diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..017b9885a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..01bacfe55 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/OpenConsole.sln b/OpenConsole.sln new file mode 100644 index 000000000..8ca6a8c45 --- /dev/null +++ b/OpenConsole.sln @@ -0,0 +1,1164 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2008 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.EXE", "src\host\exe\Host.EXE.vcxproj", "{9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}" + ProjectSection(ProjectDependencies) = postProject + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PropertiesLibrary", "src\propslib\propslib.vcxproj", "{345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Dependencies", "_Dependencies", "{81C352DB-1818-45B7-A284-18E259F1CC87}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wil", "wil", "{4C8E6BB0-4713-4ADB-BD04-81628ECEAF20}" + ProjectSection(SolutionItems) = preProject + dep\wil\include\wil\common.h = dep\wil\include\wil\common.h + dep\wil\include\wil\filesystem.h = dep\wil\include\wil\filesystem.h + dep\wil\include\wil\functional.h = dep\wil\include\wil\functional.h + dep\wil\include\wil\registry.h = dep\wil\include\wil\registry.h + dep\wil\include\wil\resource.h = dep\wil\include\wil\resource.h + dep\wil\include\wil\result.h = dep\wil\include\wil\result.h + dep\wil\include\wil\resultmacros.h = dep\wil\include\wil\resultmacros.h + dep\wil\include\wil\stl.h = dep\wil\include\wil\stl.h + dep\wil\include\wil\win32helpers.h = dep\wil\include\wil\win32helpers.h + dep\wil\include\wil\wistd_functional.h = dep\wil\include\wil\wistd_functional.h + dep\wil\include\wil\wistd_memory.h = dep\wil\include\wil\wistd_memory.h + dep\wil\include\wil\wistd_type_traits.h = dep\wil\include\wil\wistd_type_traits.h + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Console", "Console", "{D57841D1-8294-4F2B-BB8B-D2A35738DECD}" + ProjectSection(SolutionItems) = preProject + dep\Console\winconp.h = dep\Console\winconp.h + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TextServicesFramework", "src\tsf\tsf.vcxproj", "{2FD12FBB-1DDB-46D8-B818-1023C624CACA}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalParser", "src\terminal\parser\lib\parser.vcxproj", "{3AE13314-1939-4DFA-9C14-38CA0834050C}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAdapter", "src\terminal\adapter\lib\adapter.vcxproj", "{DCF55140-EF6A-4736-A403-957E4F7430BB}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalInput", "src\terminal\input\lib\terminalinput.vcxproj", "{1CF55140-EF6A-4736-A403-957E4F7430BB}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererBase", "src\renderer\base\lib\base.vcxproj", "{AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererGdi", "src\renderer\gdi\lib\gdi.vcxproj", "{1C959542-BAC2-4E55-9A6D-13251914CBB9}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host", "src\host\lib\hostlib.vcxproj", "{06EC74CB-9A12-429C-B551-8562EC954746}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.unittest", "src\host\ut_lib\host.unittest.vcxproj", "{06EC74CB-9A12-429C-B551-8562EC954747}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.Tests.Unit", "src\host\ut_host\Host.UnitTests.vcxproj", "{531C23E7-4B76-4C08-8AAD-04164CB628C9}" + ProjectSection(ProjectDependencies) = postProject + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + {06EC74CB-9A12-429C-B551-8562EC954747} = {06EC74CB-9A12-429C-B551-8562EC954747} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TextBuffer.UnitTests", "src\buffer\out\ut_textbuffer\TextBuffer.UnitTests.vcxproj", "{531C23E7-4B76-4C08-8BBD-04164CB628C9}" + ProjectSection(ProjectDependencies) = postProject + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Host.Tests.Feature", "src\host\ft_host\Host.FeatureTests.vcxproj", "{8CDB8850-7484-4EC7-B45B-181F85B2EE54}" + ProjectSection(ProjectDependencies) = postProject + {18D09A24-8240-42D6-8CB6-236EEE820263} = {18D09A24-8240-42D6-8CB6-236EEE820263} + {FC802440-AD6A-4919-8F2C-7701F2B38D79} = {FC802440-AD6A-4919-8F2C-7701F2B38D79} + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} = {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalParser.UnitTests", "src\terminal\parser\ut_parser\Parser.UnitTests.vcxproj", "{12144E07-FE63-4D33-9231-748B8D8C3792}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalAdapter.UnitTests", "src\terminal\adapter\ut_adapter\Adapter.UnitTests.vcxproj", "{6AF01638-84CF-4B65-9870-484DFFCAC772}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalParser.Fuzzer", "src\terminal\parser\ft_fuzzer\VTCommandFuzzer.vcxproj", "{96927B31-D6E8-4ABD-B03E-A5088A30BEBE}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalParser.FuzzWrapper", "src\terminal\parser\ft_fuzzwrapper\FuzzWrapper.vcxproj", "{F210A4AE-E02A-4BFC-80BB-F50A672FE763}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Propsheet.DLL", "src\propsheet\propsheet.vcxproj", "{5D23E8E1-3C64-4CC1-A8F7-6861677F7239}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Build Common", "_Build Common", "{04170EEF-983A-4195-BFEF-2321E5E38A1E}" + ProjectSection(SolutionItems) = preProject + src\common.build.dll.props = src\common.build.dll.props + src\common.build.exe.or.dll.props = src\common.build.exe.or.dll.props + src\common.build.exe.props = src\common.build.exe.props + src\common.build.lib.props = src\common.build.lib.props + src\common.build.post.props = src\common.build.post.props + src\common.build.pre.props = src\common.build.pre.props + src\common.build.tests.props = src\common.build.tests.props + common.openconsole.props = common.openconsole.props + src\cppwinrt.build.post.props = src\cppwinrt.build.post.props + src\cppwinrt.build.pre.props = src\cppwinrt.build.pre.props + src\wap-common.build.post.props = src\wap-common.build.post.props + src\wap-common.build.pre.props = src\wap-common.build.pre.props + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Server", "src\server\lib\server.vcxproj", "{18D09A24-8240-42D6-8CB6-236EEE820262}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Host.Tests.UIA", "src\host\ft_uia\Host.Tests.UIA.csproj", "{C17E1BF3-9D34-4779-9458-A8EF98CC5662}" + ProjectSection(ProjectDependencies) = postProject + {099193A0-1E43-4BBC-BA7F-7B351E1342DF} = {099193A0-1E43-4BBC-BA7F-7B351E1342DF} + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB} = {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB} + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} = {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VTApp", "src\tools\vtapp\VTApp.csproj", "{099193A0-1E43-4BBC-BA7F-7B351E1342DF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tools", "_Tools", "{A10C4720-DCA4-4640-9749-67F4314F527C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Nihilist", "src\tools\nihilist\Nihilist.vcxproj", "{FC802440-AD6A-4919-8F2C-7701F2B38D79}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FontList", "src\tools\fontlist\FontList.vcxproj", "{919544AC-D39B-463F-8414-3C3C67CF727C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Scratch", "src\tools\scratch\Scratch.vcxproj", "{ED82003F-FC5D-4E94-8B36-F480018ED064}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InteractivityWin32", "src\interactivity\win32\lib\win32.LIB.vcxproj", "{06EC74CB-9A12-429C-B551-8532EC964726}" + ProjectSection(ProjectDependencies) = postProject + {1C959542-BAC2-4E55-9A6D-13251914CBB9} = {1C959542-BAC2-4E55-9A6D-13251914CBB9} + {990F2657-8580-4828-943F-5DD657D11842} = {990F2657-8580-4828-943F-5DD657D11842} + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} = {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "buffersize", "src\tools\buffersize\buffersize.vcxproj", "{ED82003F-FC5D-4E94-8B47-F480018ED064}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InteractivityBase", "src\interactivity\base\lib\InteractivityBase.vcxproj", "{06EC74CB-9A12-429C-B551-8562EC964846}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Interactivity.Win32.Tests.Unit", "src\interactivity\win32\ut_interactivity_win32\Interactivity.Win32.UnitTests.vcxproj", "{D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}" + ProjectSection(ProjectDependencies) = postProject + {990F2657-8580-4828-943F-5DD657D11842} = {990F2657-8580-4828-943F-5DD657D11842} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CloseTest", "src\tools\closetest\CloseTest.vcxproj", "{C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererVt", "src\renderer\vt\lib\vt.vcxproj", "{990F2657-8580-4828-943F-5DD657D11842}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VtPipeTerm", "src\tools\vtpipeterm\VtPipeTerm.vcxproj", "{814DBDDE-894E-4327-A6E1-740504850098}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConEchoKey", "src\tools\echokey\ConEchoKey.vcxproj", "{814CBEEE-894E-4327-A6E1-740504850098}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Types", "src\types\lib\types.vcxproj", "{18D09A24-8240-42D6-8CB6-236EEE820263}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererVt.unittest", "src\renderer\vt\ut_lib\vt.unittest.vcxproj", "{990F2657-8580-4828-943F-5DD657D11843}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BufferOut", "src\buffer\out\lib\bufferout.vcxproj", "{0CF235BD-2DA0-407E-90EE-C467E8BBC714}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererDx", "src\renderer\dx\lib\dx.vcxproj", "{48D21369-3D7B-4431-9967-24E81292CF62}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Terminal", "Terminal", "{59840756-302F-44DF-AA47-441A9D673202}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalConnection", "src\cascadia\TerminalConnection\TerminalConnection.vcxproj", "{CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalCore", "src\cascadia\TerminalCore\lib\TerminalCore-lib.vcxproj", "{CA5CAD1A-ABCD-429C-B551-8562EC954746}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalControl", "src\cascadia\TerminalControl\TerminalControl.vcxproj", "{CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} + {CA5CAD1A-ABCD-429C-B551-8562EC954746} = {CA5CAD1A-ABCD-429C-B551-8562EC954746} + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} + {1CF55140-EF6A-4736-A403-957E4F7430BB} = {1CF55140-EF6A-4736-A403-957E4F7430BB} + EndProjectSection +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "CascadiaPackage", "src\cascadia\CascadiaPackage\CascadiaPackage.wapproj", "{CA5CAD1A-224A-4171-B13A-F16E576FDD12}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WindowsTerminal", "src\cascadia\WindowsTerminal\WindowsTerminal.vcxproj", "{CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} = {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12} = {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12} + {CA5CAD1A-ABCD-429C-B551-8562EC954746} = {CA5CAD1A-ABCD-429C-B551-8562EC954746} + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} = {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalApp", "src\cascadia\TerminalApp\TerminalApp.vcxproj", "{CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}" + ProjectSection(ProjectDependencies) = postProject + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} = {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TerminalSettings", "src\cascadia\TerminalSettings\TerminalSettings.vcxproj", "{CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}" +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "OpenConsolePackage", "pkg\appx\OpenConsolePackage.wapproj", "{2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.UI.Xaml.Markup", "src\cascadia\Microsoft.UI.Xaml.Markup\Microsoft.UI.Xaml.Markup.vcxproj", "{015A0047-772D-4F1A-88C9-45C18F0ADFB6}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests_TerminalCore", "src\cascadia\UnitTests_TerminalCore\UnitTests.vcxproj", "{2C2BEEF4-9333-4D05-B12A-1905CBF112F9}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Internal", "src\internal\internal.vcxproj", "{EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gsl", "gsl", "{16376381-CE22-42BE-B667-C6B35007008D}" + ProjectSection(SolutionItems) = preProject + dep\gsl\include\gsl\gsl = dep\gsl\include\gsl\gsl + dep\gsl\include\gsl\gsl_algorithm = dep\gsl\include\gsl\gsl_algorithm + dep\gsl\include\gsl\gsl_assert = dep\gsl\include\gsl\gsl_assert + dep\gsl\include\gsl\gsl_byte = dep\gsl\include\gsl\gsl_byte + dep\gsl\include\gsl\gsl_util = dep\gsl\include\gsl\gsl_util + dep\gsl\include\gsl\multi_span = dep\gsl\include\gsl\multi_span + dep\gsl\include\gsl\pointers = dep\gsl\include\gsl\pointers + dep\gsl\include\gsl\span = dep\gsl\include\gsl\span + dep\gsl\include\gsl\string_span = dep\gsl\include\gsl\string_span + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Virtual Terminal", "Virtual Terminal", "{F1995847-4AE5-479A-BBAF-382E51A63532}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rendering", "Rendering", "{05500DEF-2294-41E3-AF9A-24E580B82836}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conhost", "Conhost", "{E8F24881-5E37-4362-B191-A3BA0ED7F4EB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Buffer", "Buffer", "{1E4A062E-293B-4817-B20D-BF16B979E350}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{89CDCC5C-9F53-4054-97A4-639D99F169CD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + AuditMode|ARM64 = AuditMode|ARM64 + AuditMode|x64 = AuditMode|x64 + AuditMode|x86 = AuditMode|x86 + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.Build.0 = Release|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.ActiveCfg = Release|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.Build.0 = Release|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.ActiveCfg = Release|Win32 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.Build.0 = Release|Win32 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.Build.0 = Debug|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.ActiveCfg = Debug|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Build.0 = Debug|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.ActiveCfg = Debug|Win32 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.Build.0 = Debug|Win32 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.ActiveCfg = Release|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.Build.0 = Release|ARM64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.ActiveCfg = Release|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.Build.0 = Release|x64 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.ActiveCfg = Release|Win32 + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.Build.0 = Release|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|ARM64.Build.0 = Release|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x64.ActiveCfg = Release|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x64.Build.0 = Release|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x86.ActiveCfg = Release|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.AuditMode|x86.Build.0 = Release|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|ARM64.Build.0 = Debug|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x64.ActiveCfg = Debug|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x64.Build.0 = Debug|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x86.ActiveCfg = Debug|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Debug|x86.Build.0 = Debug|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|ARM64.ActiveCfg = Release|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|ARM64.Build.0 = Release|ARM64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x64.ActiveCfg = Release|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x64.Build.0 = Release|x64 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x86.ActiveCfg = Release|Win32 + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC}.Release|x86.Build.0 = Release|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|ARM64.Build.0 = Release|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x64.ActiveCfg = Release|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x64.Build.0 = Release|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x86.ActiveCfg = Release|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.AuditMode|x86.Build.0 = Release|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|ARM64.Build.0 = Debug|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x64.ActiveCfg = Debug|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x64.Build.0 = Debug|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x86.ActiveCfg = Debug|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Debug|x86.Build.0 = Debug|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|ARM64.ActiveCfg = Release|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|ARM64.Build.0 = Release|ARM64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x64.ActiveCfg = Release|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x64.Build.0 = Release|x64 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x86.ActiveCfg = Release|Win32 + {2FD12FBB-1DDB-46D8-B818-1023C624CACA}.Release|x86.Build.0 = Release|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|ARM64.Build.0 = Release|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x64.ActiveCfg = Release|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x64.Build.0 = Release|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x86.ActiveCfg = Release|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.AuditMode|x86.Build.0 = Release|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|ARM64.Build.0 = Debug|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x64.ActiveCfg = Debug|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x64.Build.0 = Debug|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x86.ActiveCfg = Debug|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Debug|x86.Build.0 = Debug|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|ARM64.ActiveCfg = Release|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|ARM64.Build.0 = Release|ARM64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x64.ActiveCfg = Release|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x64.Build.0 = Release|x64 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x86.ActiveCfg = Release|Win32 + {3AE13314-1939-4DFA-9C14-38CA0834050C}.Release|x86.Build.0 = Release|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.Build.0 = Release|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.ActiveCfg = Release|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.Build.0 = Release|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.ActiveCfg = Release|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.Build.0 = Release|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.Build.0 = Debug|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.ActiveCfg = Debug|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.Build.0 = Debug|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.ActiveCfg = Debug|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.Build.0 = Debug|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.ActiveCfg = Release|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.Build.0 = Release|ARM64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.ActiveCfg = Release|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.Build.0 = Release|x64 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.ActiveCfg = Release|Win32 + {DCF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.Build.0 = Release|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|ARM64.Build.0 = Release|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.ActiveCfg = Release|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x64.Build.0 = Release|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.ActiveCfg = Release|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.AuditMode|x86.Build.0 = Release|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|ARM64.Build.0 = Debug|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.ActiveCfg = Debug|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x64.Build.0 = Debug|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.ActiveCfg = Debug|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Debug|x86.Build.0 = Debug|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.ActiveCfg = Release|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|ARM64.Build.0 = Release|ARM64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.ActiveCfg = Release|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x64.Build.0 = Release|x64 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.ActiveCfg = Release|Win32 + {1CF55140-EF6A-4736-A403-957E4F7430BB}.Release|x86.Build.0 = Release|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|ARM64.Build.0 = Release|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x64.ActiveCfg = Release|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x64.Build.0 = Release|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x86.ActiveCfg = Release|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.AuditMode|x86.Build.0 = Release|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|ARM64.Build.0 = Debug|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x64.ActiveCfg = Debug|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x64.Build.0 = Debug|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x86.ActiveCfg = Debug|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Debug|x86.Build.0 = Debug|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|ARM64.ActiveCfg = Release|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|ARM64.Build.0 = Release|ARM64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x64.ActiveCfg = Release|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x64.Build.0 = Release|x64 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x86.ActiveCfg = Release|Win32 + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F}.Release|x86.Build.0 = Release|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|ARM64.Build.0 = Release|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x64.ActiveCfg = Release|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x64.Build.0 = Release|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x86.ActiveCfg = Release|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.AuditMode|x86.Build.0 = Release|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|ARM64.Build.0 = Debug|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x64.ActiveCfg = Debug|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x64.Build.0 = Debug|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x86.ActiveCfg = Debug|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Debug|x86.Build.0 = Debug|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|ARM64.ActiveCfg = Release|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|ARM64.Build.0 = Release|ARM64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x64.ActiveCfg = Release|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x64.Build.0 = Release|x64 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x86.ActiveCfg = Release|Win32 + {1C959542-BAC2-4E55-9A6D-13251914CBB9}.Release|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.AuditMode|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|ARM64.Build.0 = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x64.ActiveCfg = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x64.Build.0 = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x86.ActiveCfg = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.Debug|x86.Build.0 = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954746}.Release|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.AuditMode|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|ARM64.Build.0 = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x64.ActiveCfg = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x64.Build.0 = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x86.ActiveCfg = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.Debug|x86.Build.0 = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC954747}.Release|x86.Build.0 = Release|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|ARM64.Build.0 = Release|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x64.ActiveCfg = Release|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x64.Build.0 = Release|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x86.ActiveCfg = Release|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.AuditMode|x86.Build.0 = Release|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|ARM64.Build.0 = Debug|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x64.ActiveCfg = Debug|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x64.Build.0 = Debug|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x86.ActiveCfg = Debug|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Debug|x86.Build.0 = Debug|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|ARM64.ActiveCfg = Release|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|ARM64.Build.0 = Release|ARM64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x64.ActiveCfg = Release|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x64.Build.0 = Release|x64 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x86.ActiveCfg = Release|Win32 + {531C23E7-4B76-4C08-8AAD-04164CB628C9}.Release|x86.Build.0 = Release|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|ARM64.Build.0 = Release|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x64.ActiveCfg = Release|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x64.Build.0 = Release|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x86.ActiveCfg = Release|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.AuditMode|x86.Build.0 = Release|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|ARM64.Build.0 = Debug|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x64.ActiveCfg = Debug|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x64.Build.0 = Debug|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x86.ActiveCfg = Debug|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Debug|x86.Build.0 = Debug|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|ARM64.ActiveCfg = Release|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|ARM64.Build.0 = Release|ARM64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x64.ActiveCfg = Release|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x64.Build.0 = Release|x64 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x86.ActiveCfg = Release|Win32 + {531C23E7-4B76-4C08-8BBD-04164CB628C9}.Release|x86.Build.0 = Release|Win32 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|ARM64.Build.0 = Release|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x64.ActiveCfg = Release|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x64.Build.0 = Release|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x86.ActiveCfg = Release|Win32 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.AuditMode|x86.Build.0 = Release|Win32 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|ARM64.Build.0 = Debug|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x64.ActiveCfg = Debug|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x64.Build.0 = Debug|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Debug|x86.ActiveCfg = Debug|Win32 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|ARM64.ActiveCfg = Release|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|ARM64.Build.0 = Release|ARM64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x64.ActiveCfg = Release|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x64.Build.0 = Release|x64 + {8CDB8850-7484-4EC7-B45B-181F85B2EE54}.Release|x86.ActiveCfg = Release|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|ARM64.Build.0 = Release|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x64.ActiveCfg = Release|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x64.Build.0 = Release|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x86.ActiveCfg = Release|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.AuditMode|x86.Build.0 = Release|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|ARM64.Build.0 = Debug|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x64.ActiveCfg = Debug|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x64.Build.0 = Debug|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x86.ActiveCfg = Debug|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Debug|x86.Build.0 = Debug|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|ARM64.ActiveCfg = Release|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|ARM64.Build.0 = Release|ARM64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x64.ActiveCfg = Release|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x64.Build.0 = Release|x64 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x86.ActiveCfg = Release|Win32 + {12144E07-FE63-4D33-9231-748B8D8C3792}.Release|x86.Build.0 = Release|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|ARM64.Build.0 = Release|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x64.ActiveCfg = Release|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x64.Build.0 = Release|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x86.ActiveCfg = Release|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.AuditMode|x86.Build.0 = Release|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|ARM64.Build.0 = Debug|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x64.ActiveCfg = Debug|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x64.Build.0 = Debug|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x86.ActiveCfg = Debug|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Debug|x86.Build.0 = Debug|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|ARM64.ActiveCfg = Release|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|ARM64.Build.0 = Release|ARM64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x64.ActiveCfg = Release|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x64.Build.0 = Release|x64 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x86.ActiveCfg = Release|Win32 + {6AF01638-84CF-4B65-9870-484DFFCAC772}.Release|x86.Build.0 = Release|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|ARM64.Build.0 = Release|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x64.ActiveCfg = Release|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x64.Build.0 = Release|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x86.ActiveCfg = Release|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.AuditMode|x86.Build.0 = Release|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|ARM64.Build.0 = Debug|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x64.ActiveCfg = Debug|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x64.Build.0 = Debug|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x86.ActiveCfg = Debug|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Debug|x86.Build.0 = Debug|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|ARM64.ActiveCfg = Release|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|ARM64.Build.0 = Release|ARM64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x64.ActiveCfg = Release|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x64.Build.0 = Release|x64 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x86.ActiveCfg = Release|Win32 + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE}.Release|x86.Build.0 = Release|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|ARM64.Build.0 = Release|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x64.ActiveCfg = Release|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x64.Build.0 = Release|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x86.ActiveCfg = Release|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.AuditMode|x86.Build.0 = Release|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|ARM64.Build.0 = Debug|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x64.ActiveCfg = Debug|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x64.Build.0 = Debug|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x86.ActiveCfg = Debug|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Debug|x86.Build.0 = Debug|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|ARM64.ActiveCfg = Release|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|ARM64.Build.0 = Release|ARM64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x64.ActiveCfg = Release|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x64.Build.0 = Release|x64 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x86.ActiveCfg = Release|Win32 + {F210A4AE-E02A-4BFC-80BB-F50A672FE763}.Release|x86.Build.0 = Release|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|ARM64.Build.0 = Release|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x64.ActiveCfg = Release|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x64.Build.0 = Release|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x86.ActiveCfg = Release|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.AuditMode|x86.Build.0 = Release|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|ARM64.Build.0 = Debug|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x64.ActiveCfg = Debug|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x64.Build.0 = Debug|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x86.ActiveCfg = Debug|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Debug|x86.Build.0 = Debug|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|ARM64.ActiveCfg = Release|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|ARM64.Build.0 = Release|ARM64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x64.ActiveCfg = Release|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x64.Build.0 = Release|x64 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x86.ActiveCfg = Release|Win32 + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239}.Release|x86.Build.0 = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|ARM64.Build.0 = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x64.ActiveCfg = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x64.Build.0 = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x86.ActiveCfg = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.AuditMode|x86.Build.0 = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|ARM64.Build.0 = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x64.ActiveCfg = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x64.Build.0 = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x86.ActiveCfg = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Debug|x86.Build.0 = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|ARM64.ActiveCfg = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|ARM64.Build.0 = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x64.ActiveCfg = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x64.Build.0 = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x86.ActiveCfg = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820262}.Release|x86.Build.0 = Release|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|ARM64.ActiveCfg = Debug|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|ARM64.Build.0 = Debug|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x64.ActiveCfg = Release|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x64.Build.0 = Release|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x86.ActiveCfg = Release|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.AuditMode|x86.Build.0 = Release|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|ARM64.ActiveCfg = Debug|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x64.ActiveCfg = Debug|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x64.Build.0 = Debug|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x86.ActiveCfg = Debug|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Debug|x86.Build.0 = Debug|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|ARM64.ActiveCfg = Release|ARM64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x64.ActiveCfg = Release|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x64.Build.0 = Release|x64 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x86.ActiveCfg = Release|Win32 + {C17E1BF3-9D34-4779-9458-A8EF98CC5662}.Release|x86.Build.0 = Release|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|ARM64.ActiveCfg = Debug|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|ARM64.Build.0 = Debug|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x64.ActiveCfg = Release|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x64.Build.0 = Release|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x86.ActiveCfg = Release|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.AuditMode|x86.Build.0 = Release|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|ARM64.ActiveCfg = Debug|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x64.ActiveCfg = Debug|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x64.Build.0 = Debug|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x86.ActiveCfg = Debug|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Debug|x86.Build.0 = Debug|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|ARM64.ActiveCfg = Release|ARM64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x64.ActiveCfg = Release|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x64.Build.0 = Release|x64 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x86.ActiveCfg = Release|Win32 + {099193A0-1E43-4BBC-BA7F-7B351E1342DF}.Release|x86.Build.0 = Release|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|ARM64.Build.0 = Release|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x64.ActiveCfg = Release|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x64.Build.0 = Release|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x86.ActiveCfg = Release|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.AuditMode|x86.Build.0 = Release|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|ARM64.Build.0 = Debug|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x64.ActiveCfg = Debug|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x64.Build.0 = Debug|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x86.ActiveCfg = Debug|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Debug|x86.Build.0 = Debug|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|ARM64.ActiveCfg = Release|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|ARM64.Build.0 = Release|ARM64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x64.ActiveCfg = Release|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x64.Build.0 = Release|x64 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x86.ActiveCfg = Release|Win32 + {FC802440-AD6A-4919-8F2C-7701F2B38D79}.Release|x86.Build.0 = Release|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|ARM64.Build.0 = Release|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x64.ActiveCfg = Release|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x64.Build.0 = Release|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x86.ActiveCfg = Release|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.AuditMode|x86.Build.0 = Release|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|ARM64.Build.0 = Debug|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x64.ActiveCfg = Debug|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x64.Build.0 = Debug|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x86.ActiveCfg = Debug|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Debug|x86.Build.0 = Debug|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|ARM64.ActiveCfg = Release|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|ARM64.Build.0 = Release|ARM64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x64.ActiveCfg = Release|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x64.Build.0 = Release|x64 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x86.ActiveCfg = Release|Win32 + {919544AC-D39B-463F-8414-3C3C67CF727C}.Release|x86.Build.0 = Release|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|ARM64.Build.0 = Release|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x64.ActiveCfg = Release|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x64.Build.0 = Release|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x86.ActiveCfg = Release|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.AuditMode|x86.Build.0 = Release|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|ARM64.Build.0 = Debug|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x64.ActiveCfg = Debug|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x64.Build.0 = Debug|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x86.ActiveCfg = Debug|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Debug|x86.Build.0 = Debug|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|ARM64.ActiveCfg = Release|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|ARM64.Build.0 = Release|ARM64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x64.ActiveCfg = Release|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x64.Build.0 = Release|x64 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x86.ActiveCfg = Release|Win32 + {ED82003F-FC5D-4E94-8B36-F480018ED064}.Release|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.AuditMode|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|ARM64.Build.0 = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x64.ActiveCfg = Debug|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x64.Build.0 = Debug|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x86.ActiveCfg = Debug|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.Debug|x86.Build.0 = Debug|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8532EC964726}.Release|x86.Build.0 = Release|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|ARM64.Build.0 = Release|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x64.ActiveCfg = Release|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x64.Build.0 = Release|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x86.ActiveCfg = Release|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.AuditMode|x86.Build.0 = Release|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|ARM64.Build.0 = Debug|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x64.ActiveCfg = Debug|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x64.Build.0 = Debug|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x86.ActiveCfg = Debug|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Debug|x86.Build.0 = Debug|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|ARM64.ActiveCfg = Release|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|ARM64.Build.0 = Release|ARM64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x64.ActiveCfg = Release|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x64.Build.0 = Release|x64 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x86.ActiveCfg = Release|Win32 + {ED82003F-FC5D-4E94-8B47-F480018ED064}.Release|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.AuditMode|x86.Build.0 = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|ARM64.Build.0 = Debug|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x64.ActiveCfg = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x64.Build.0 = Debug|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x86.ActiveCfg = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.Debug|x86.Build.0 = Debug|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|ARM64.ActiveCfg = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|ARM64.Build.0 = Release|ARM64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x64.ActiveCfg = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x64.Build.0 = Release|x64 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x86.ActiveCfg = Release|Win32 + {06EC74CB-9A12-429C-B551-8562EC964846}.Release|x86.Build.0 = Release|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|ARM64.Build.0 = Release|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x64.ActiveCfg = Release|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x64.Build.0 = Release|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x86.ActiveCfg = Release|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.AuditMode|x86.Build.0 = Release|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|ARM64.Build.0 = Debug|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x64.ActiveCfg = Debug|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x64.Build.0 = Debug|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x86.ActiveCfg = Debug|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Debug|x86.Build.0 = Debug|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|ARM64.ActiveCfg = Release|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|ARM64.Build.0 = Release|ARM64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x64.ActiveCfg = Release|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x64.Build.0 = Release|x64 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x86.ActiveCfg = Release|Win32 + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4}.Release|x86.Build.0 = Release|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|ARM64.Build.0 = Release|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x64.ActiveCfg = Release|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x64.Build.0 = Release|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x86.ActiveCfg = Release|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.AuditMode|x86.Build.0 = Release|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|ARM64.Build.0 = Debug|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x64.ActiveCfg = Debug|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x64.Build.0 = Debug|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x86.ActiveCfg = Debug|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Debug|x86.Build.0 = Debug|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|ARM64.ActiveCfg = Release|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|ARM64.Build.0 = Release|ARM64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x64.ActiveCfg = Release|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x64.Build.0 = Release|x64 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x86.ActiveCfg = Release|Win32 + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB}.Release|x86.Build.0 = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|ARM64.Build.0 = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x64.ActiveCfg = Release|x64 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x64.Build.0 = Release|x64 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x86.ActiveCfg = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.AuditMode|x86.Build.0 = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|ARM64.Build.0 = Debug|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|x64.ActiveCfg = Debug|x64 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|x64.Build.0 = Debug|x64 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|x86.ActiveCfg = Debug|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.Debug|x86.Build.0 = Debug|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.Release|ARM64.ActiveCfg = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.Release|ARM64.Build.0 = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11842}.Release|x64.ActiveCfg = Release|x64 + {990F2657-8580-4828-943F-5DD657D11842}.Release|x64.Build.0 = Release|x64 + {990F2657-8580-4828-943F-5DD657D11842}.Release|x86.ActiveCfg = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11842}.Release|x86.Build.0 = Release|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.Build.0 = Release|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x64.ActiveCfg = Release|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x64.Build.0 = Release|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x86.ActiveCfg = Release|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.AuditMode|x86.Build.0 = Release|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|ARM64.Build.0 = Debug|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x64.ActiveCfg = Debug|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x64.Build.0 = Debug|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x86.ActiveCfg = Debug|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.Debug|x86.Build.0 = Debug|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|ARM64.ActiveCfg = Release|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|ARM64.Build.0 = Release|ARM64 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|x64.ActiveCfg = Release|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|x64.Build.0 = Release|x64 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|x86.ActiveCfg = Release|Win32 + {814DBDDE-894E-4327-A6E1-740504850098}.Release|x86.Build.0 = Release|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|ARM64.Build.0 = Release|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x64.ActiveCfg = Release|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x64.Build.0 = Release|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x86.ActiveCfg = Release|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.AuditMode|x86.Build.0 = Release|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|ARM64.Build.0 = Debug|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x64.ActiveCfg = Debug|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x64.Build.0 = Debug|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x86.ActiveCfg = Debug|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.Debug|x86.Build.0 = Debug|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|ARM64.ActiveCfg = Release|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|ARM64.Build.0 = Release|ARM64 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|x64.ActiveCfg = Release|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|x64.Build.0 = Release|x64 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|x86.ActiveCfg = Release|Win32 + {814CBEEE-894E-4327-A6E1-740504850098}.Release|x86.Build.0 = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x64.Build.0 = AuditMode|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.AuditMode|x86.Build.0 = AuditMode|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|ARM64.Build.0 = Debug|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.ActiveCfg = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x64.Build.0 = Debug|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x86.ActiveCfg = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Debug|x86.Build.0 = Debug|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.ActiveCfg = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|ARM64.Build.0 = Release|ARM64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.ActiveCfg = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x64.Build.0 = Release|x64 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x86.ActiveCfg = Release|Win32 + {18D09A24-8240-42D6-8CB6-236EEE820263}.Release|x86.Build.0 = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|ARM64.Build.0 = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x64.ActiveCfg = Release|x64 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x64.Build.0 = Release|x64 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x86.ActiveCfg = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.AuditMode|x86.Build.0 = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|ARM64.Build.0 = Debug|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|x64.ActiveCfg = Debug|x64 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|x64.Build.0 = Debug|x64 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|x86.ActiveCfg = Debug|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.Debug|x86.Build.0 = Debug|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.Release|ARM64.ActiveCfg = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.Release|ARM64.Build.0 = Release|ARM64 + {990F2657-8580-4828-943F-5DD657D11843}.Release|x64.ActiveCfg = Release|x64 + {990F2657-8580-4828-943F-5DD657D11843}.Release|x64.Build.0 = Release|x64 + {990F2657-8580-4828-943F-5DD657D11843}.Release|x86.ActiveCfg = Release|Win32 + {990F2657-8580-4828-943F-5DD657D11843}.Release|x86.Build.0 = Release|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x64.Build.0 = AuditMode|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.AuditMode|x86.Build.0 = AuditMode|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|ARM64.Build.0 = Debug|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x64.ActiveCfg = Debug|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x64.Build.0 = Debug|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x86.ActiveCfg = Debug|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Debug|x86.Build.0 = Debug|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|ARM64.ActiveCfg = Release|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|ARM64.Build.0 = Release|ARM64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x64.ActiveCfg = Release|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x64.Build.0 = Release|x64 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x86.ActiveCfg = Release|Win32 + {0CF235BD-2DA0-407E-90EE-C467E8BBC714}.Release|x86.Build.0 = Release|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x64.Build.0 = AuditMode|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.AuditMode|x86.Build.0 = AuditMode|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|ARM64.Build.0 = Debug|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x64.ActiveCfg = Debug|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x64.Build.0 = Debug|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x86.ActiveCfg = Debug|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.Debug|x86.Build.0 = Debug|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|ARM64.ActiveCfg = Release|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|ARM64.Build.0 = Release|ARM64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x64.ActiveCfg = Release|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x64.Build.0 = Release|x64 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x86.ActiveCfg = Release|Win32 + {48D21369-3D7B-4431-9967-24E81292CF62}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-ABCD-429C-B551-8562EC954746}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|ARM64.Deploy.0 = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x64.Deploy.0 = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x86.ActiveCfg = Release|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x86.Build.0 = Release|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.AuditMode|x86.Deploy.0 = Release|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x64.Deploy.0 = Debug|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.ActiveCfg = Debug|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.Build.0 = Debug|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Debug|x86.Deploy.0 = Debug|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|ARM64.Deploy.0 = Release|ARM64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x64.Deploy.0 = Release|x64 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x86.ActiveCfg = Release|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x86.Build.0 = Release|x86 + {CA5CAD1A-224A-4171-B13A-F16E576FDD12}.Release|x86.Deploy.0 = Release|x86 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12}.Release|x86.Build.0 = Release|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x64.ActiveCfg = Release|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x64.Build.0 = Release|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.AuditMode|x86.Build.0 = Release|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|ARM64.Build.0 = Debug|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x64.ActiveCfg = Debug|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x64.Build.0 = Debug|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x86.ActiveCfg = Debug|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Debug|x86.Build.0 = Debug|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|ARM64.ActiveCfg = Release|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|ARM64.Build.0 = Release|ARM64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x64.ActiveCfg = Release|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x64.Build.0 = Release|x64 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x86.ActiveCfg = Release|Win32 + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907}.Release|x86.Build.0 = Release|Win32 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|ARM64.Build.0 = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|ARM64.Deploy.0 = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x64.ActiveCfg = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x64.Build.0 = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x64.Deploy.0 = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x86.ActiveCfg = Release|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x86.Build.0 = Release|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.AuditMode|x86.Deploy.0 = Release|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|ARM64.Build.0 = Debug|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x64.ActiveCfg = Debug|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x64.Build.0 = Debug|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x64.Deploy.0 = Debug|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x86.ActiveCfg = Debug|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x86.Build.0 = Debug|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Debug|x86.Deploy.0 = Debug|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|ARM64.ActiveCfg = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|ARM64.Build.0 = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|ARM64.Deploy.0 = Release|ARM64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x64.ActiveCfg = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x64.Build.0 = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x64.Deploy.0 = Release|x64 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x86.ActiveCfg = Release|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x86.Build.0 = Release|x86 + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310}.Release|x86.Deploy.0 = Release|x86 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|ARM64.ActiveCfg = Release|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|ARM64.Build.0 = Release|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|x64.ActiveCfg = Release|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|x64.Build.0 = Release|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|x86.ActiveCfg = Release|Win32 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.AuditMode|x86.Build.0 = Release|Win32 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|ARM64.Build.0 = Debug|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|x64.ActiveCfg = Debug|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|x64.Build.0 = Debug|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|x86.ActiveCfg = Debug|Win32 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Debug|x86.Build.0 = Debug|Win32 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|ARM64.ActiveCfg = Release|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|ARM64.Build.0 = Release|ARM64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|x64.ActiveCfg = Release|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|x64.Build.0 = Release|x64 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|x86.ActiveCfg = Release|Win32 + {015A0047-772D-4F1A-88C9-45C18F0ADFB6}.Release|x86.Build.0 = Release|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x64.Build.0 = AuditMode|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.AuditMode|x86.Build.0 = AuditMode|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|ARM64.Build.0 = Debug|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x64.ActiveCfg = Debug|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x64.Build.0 = Debug|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x86.ActiveCfg = Debug|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Debug|x86.Build.0 = Debug|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.ActiveCfg = Release|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|ARM64.Build.0 = Release|ARM64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x64.ActiveCfg = Release|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x64.Build.0 = Release|x64 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x86.ActiveCfg = Release|Win32 + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9}.Release|x86.Build.0 = Release|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|ARM64.Build.0 = AuditMode|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x64.ActiveCfg = AuditMode|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x64.Build.0 = AuditMode|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x86.ActiveCfg = AuditMode|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.AuditMode|x86.Build.0 = AuditMode|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|ARM64.Build.0 = Debug|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x64.ActiveCfg = Debug|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x64.Build.0 = Debug|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x86.ActiveCfg = Debug|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Debug|x86.Build.0 = Debug|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|ARM64.ActiveCfg = Release|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|ARM64.Build.0 = Release|ARM64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x64.ActiveCfg = Release|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x64.Build.0 = Release|x64 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x86.ActiveCfg = Release|Win32 + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {4C8E6BB0-4713-4ADB-BD04-81628ECEAF20} = {81C352DB-1818-45B7-A284-18E259F1CC87} + {D57841D1-8294-4F2B-BB8B-D2A35738DECD} = {81C352DB-1818-45B7-A284-18E259F1CC87} + {2FD12FBB-1DDB-46D8-B818-1023C624CACA} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {3AE13314-1939-4DFA-9C14-38CA0834050C} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {DCF55140-EF6A-4736-A403-957E4F7430BB} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {1CF55140-EF6A-4736-A403-957E4F7430BB} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} = {05500DEF-2294-41E3-AF9A-24E580B82836} + {1C959542-BAC2-4E55-9A6D-13251914CBB9} = {05500DEF-2294-41E3-AF9A-24E580B82836} + {06EC74CB-9A12-429C-B551-8562EC954746} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {06EC74CB-9A12-429C-B551-8562EC954747} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {531C23E7-4B76-4C08-8AAD-04164CB628C9} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {531C23E7-4B76-4C08-8BBD-04164CB628C9} = {1E4A062E-293B-4817-B20D-BF16B979E350} + {8CDB8850-7484-4EC7-B45B-181F85B2EE54} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {12144E07-FE63-4D33-9231-748B8D8C3792} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {6AF01638-84CF-4B65-9870-484DFFCAC772} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {F210A4AE-E02A-4BFC-80BB-F50A672FE763} = {F1995847-4AE5-479A-BBAF-382E51A63532} + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {18D09A24-8240-42D6-8CB6-236EEE820262} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {C17E1BF3-9D34-4779-9458-A8EF98CC5662} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {099193A0-1E43-4BBC-BA7F-7B351E1342DF} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {FC802440-AD6A-4919-8F2C-7701F2B38D79} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {919544AC-D39B-463F-8414-3C3C67CF727C} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {ED82003F-FC5D-4E94-8B36-F480018ED064} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {06EC74CB-9A12-429C-B551-8532EC964726} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {ED82003F-FC5D-4E94-8B47-F480018ED064} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {06EC74CB-9A12-429C-B551-8562EC964846} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {D3B92829-26CB-411A-BDA2-7F5DA3D25DD4} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {990F2657-8580-4828-943F-5DD657D11842} = {05500DEF-2294-41E3-AF9A-24E580B82836} + {814DBDDE-894E-4327-A6E1-740504850098} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {814CBEEE-894E-4327-A6E1-740504850098} = {A10C4720-DCA4-4640-9749-67F4314F527C} + {18D09A24-8240-42D6-8CB6-236EEE820263} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} + {990F2657-8580-4828-943F-5DD657D11843} = {05500DEF-2294-41E3-AF9A-24E580B82836} + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} = {1E4A062E-293B-4817-B20D-BF16B979E350} + {48D21369-3D7B-4431-9967-24E81292CF62} = {05500DEF-2294-41E3-AF9A-24E580B82836} + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-ABCD-429C-B551-8562EC954746} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-224A-4171-B13A-F16E576FDD12} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12} = {59840756-302F-44DF-AA47-441A9D673202} + {CA5CAD1A-D7EC-4107-B7C6-79CB77AE2907} = {59840756-302F-44DF-AA47-441A9D673202} + {2D310963-F3E0-4EE5-8AC6-FBC94DCC3310} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {015A0047-772D-4F1A-88C9-45C18F0ADFB6} = {59840756-302F-44DF-AA47-441A9D673202} + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9} = {59840756-302F-44DF-AA47-441A9D673202} + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB} + {16376381-CE22-42BE-B667-C6B35007008D} = {81C352DB-1818-45B7-A284-18E259F1CC87} + {F1995847-4AE5-479A-BBAF-382E51A63532} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} + {05500DEF-2294-41E3-AF9A-24E580B82836} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} + {1E4A062E-293B-4817-B20D-BF16B979E350} = {89CDCC5C-9F53-4054-97A4-639D99F169CD} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 000000000..d3aa5883d --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Welcome to the Console Project! + +This project is currently controlled by the Windows Developer Platform Tools & Runtimes' Open Source Software team (*WDG > DEP > DART > OSS*). + +Our team can be reached at `dartcon@microsoft.com`. + +The code is stored at . + +The area path within the Microsoft.VisualStudio.com database for our Work Items is `OS\CORE-OS Core\DEP-Developer Ecosystem Platform\DART-Developer Tools and Runtimes\Open Source Software\Console`. + +## Jumping In + +To get started, feel free to read up on some of our documentation on the way we get things done and hop in. + +Make a branch off of `dev/main` for yourself of the pattern `dev/myalias/foo` and feel free to push it to the server to get automatic builds and unit test runs. + +Choose a bit of code to clean up, try to add a new feature, or improve something that you try to use every day. + +When you are ready, use the [web portal](https://microsoft.visualstudio.com/Dart/_git/OpenConsole/pullrequests) to send a pull request into our `dev/main` branch and we'll be happy to help you get your code in line with the rest of the console. + +## Building + +OpenConsole uses submodules for some of its dependencies. To make sure submodules are restored or updated: + +``` +git submodule update --init --recursive +``` + +OpenConsole.sln may be built from within Visual Studio or from the command line using msbuild. To build from the command line: + +``` +nuget.exe restore OpenConsole.sln +msbuild.exe OpenConsole.sln +``` + +We provide a set of convienence scripts in the /tools directory to help automate the process of building and running tests. + +## Assorted Notes + +Here's some assorted notes on the way we do things. If you learn something about how we do things, feel free to contribute to any of our documentation files anywhere in the repository (or make some new ones!) This is a work in progress as we try to learn what we'll need to train people on in order to be effective contributors to our project. We're pretty blind to these things after staring at this code for so long... so mind the gaps and ask us plenty of questions! + +* [Coding Style](./doc/STYLE.md) +* [Code Organization](./doc/ORGANIZATION.md) +* [Exceptions in our legacy codebase](./doc/EXCEPTIONS.md) +* [Helpful smart pointers and macros for interfacing with Windows in WIL](./doc/WIL.md) diff --git a/build/config/SignConfig.WindowsTerminal.xml b/build/config/SignConfig.WindowsTerminal.xml new file mode 100644 index 000000000..56f595638 --- /dev/null +++ b/build/config/SignConfig.WindowsTerminal.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/build/scripts/Create-AppxBundle.ps1 b/build/scripts/Create-AppxBundle.ps1 new file mode 100644 index 000000000..8dd29b2a5 --- /dev/null +++ b/build/scripts/Create-AppxBundle.ps1 @@ -0,0 +1,57 @@ +Param( + [Parameter(Mandatory, + HelpMessage="Base name for input .appx files")] + [string] + $ProjectName, + + [Parameter(Mandatory, + HelpMessage="Appx Bundle Version")] + [version] + $BundleVersion, + + [Parameter(Mandatory, + HelpMessage="Path under which to locate appx/msix files")] + [string] + $InputPath, + + [Parameter(Mandatory, + HelpMessage="Output Path")] + [string] + $OutputPath, + + [Parameter(HelpMessage="Path to makeappx.exe")] + [ValidateScript({Test-Path $_ -Type Leaf})] + [string] + $MakeAppxPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\MakeAppx.exe" +) + +If ($null -Eq (Get-Item $MakeAppxPath -EA:SilentlyContinue)) { + Write-Error "Could not find MakeAppx.exe at `"$MakeAppxPath`".`nMake sure that -MakeAppxPath points to a valid SDK." + Exit 1 +} + +# Enumerates a set of appx files beginning with a project name +# and generates a temporary file containing a bundle content map. +Function Create-AppxBundleMapping { + Param( + [Parameter(Mandatory)] + [string] + $InputPath, + + [Parameter(Mandatory)] + [string] + $ProjectName + ) + + $lines = @("[Files]") + Get-ChildItem -Path:$InputPath -Recurse -Filter:*$ProjectName* -Include *.appx, *.msix | % { + $lines += ("`"{0}`" `"{1}`"" -f ($_.FullName, $_.Name)) + } + $outputFile = New-TemporaryFile + $lines | Out-File -Encoding:ASCII $outputFile + $outputFile +} + +$NewMapping = Create-AppxBundleMapping -InputPath:$InputPath -ProjectName:$ProjectName + +& $MakeAppxPath bundle /v /bv $BundleVersion.ToString() /f $NewMapping.FullName /p $OutputPath diff --git a/common.openconsole.props b/common.openconsole.props new file mode 100644 index 000000000..e521ad619 --- /dev/null +++ b/common.openconsole.props @@ -0,0 +1,13 @@ + + + + + + $(MSBuildThisFileDirectory) + + + diff --git a/consolegit2gitfilters.json b/consolegit2gitfilters.json new file mode 100644 index 000000000..24d2a503d --- /dev/null +++ b/consolegit2gitfilters.json @@ -0,0 +1,31 @@ +{ + "PrefixFilters": [ + "." + ], + "ContainsFilters": [ + "/.", + "/.git/", + "/obj/", + "/bin/", + "/TestResults/", + "/packages/", + "/ipch/", + "/dep/", + "/.vs/" + ], + "SuffixFilters": [ + ".dbb", + ".evt", + ".log", + ".metadata", + ".prf", + ".trc", + ".user", + ".tmp", + ".TMP", + ".db", + ".wrn", + ".rec", + ".err" + ] +} diff --git a/dep/Console/conapi.h b/dep/Console/conapi.h new file mode 100644 index 000000000..84535ec68 --- /dev/null +++ b/dep/Console/conapi.h @@ -0,0 +1,24 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- conapi.h + +Abstract: +- This module contains the internal structures and definitions used by the console server. + +Author: +- Therese Stowell (ThereseS) 12-Nov-1990 + +Revision History: +--*/ + +#pragma once +// these should be in precomp but aren't being picked up... +#include +#define STATUS_SHARING_VIOLATION ((NTSTATUS)0xC0000043L) + +#include "conmsgl1.h" +#include "conmsgl2.h" +#include "conmsgl3.h" + diff --git a/dep/Console/condrv.h b/dep/Console/condrv.h new file mode 100644 index 000000000..b5654d98b --- /dev/null +++ b/dep/Console/condrv.h @@ -0,0 +1,214 @@ +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + +Module Name: + + condrv.h + +Abstract: + + This module contains the declarations shared by the console driver and the + user-mode components that use it. + +Author: + + Wedson Almeida Filho (wedsonaf) 24-Sep-2009 + +Environment: + + Kernel and user modes. + +--*/ + +#pragma once + +#include "..\NT\ntioapi_x.h" + +// +// Messages that can be received by servers, used in CD_IO_DESCRIPTOR::Function. +// + +#define CONSOLE_IO_CONNECT 0x01 +#define CONSOLE_IO_DISCONNECT 0x02 +#define CONSOLE_IO_CREATE_OBJECT 0x03 +#define CONSOLE_IO_CLOSE_OBJECT 0x04 +#define CONSOLE_IO_RAW_WRITE 0x05 +#define CONSOLE_IO_RAW_READ 0x06 +#define CONSOLE_IO_USER_DEFINED 0x07 +#define CONSOLE_IO_RAW_FLUSH 0x08 + +// +// Header of all IOs submitted to a server. +// + +typedef struct _CD_IO_DESCRIPTOR { + LUID Identifier; + ULONG_PTR Process; + ULONG_PTR Object; + ULONG Function; + ULONG InputSize; + ULONG OutputSize; + ULONG Reserved; +} CD_IO_DESCRIPTOR, *PCD_IO_DESCRIPTOR; + +// +// Types of objects, used in CREATE_OBJECT_INFORMATION::ObjectType. +// + +#define CD_IO_OBJECT_TYPE_CURRENT_INPUT 0x01 +#define CD_IO_OBJECT_TYPE_CURRENT_OUTPUT 0x02 +#define CD_IO_OBJECT_TYPE_NEW_OUTPUT 0x03 +#define CD_IO_OBJECT_TYPE_GENERIC 0x04 + +// +// Payload of the CONSOLE_IO_CREATE_OBJECT io. +// + +typedef struct _CD_CREATE_OBJECT_INFORMATION { + ULONG ObjectType; + ULONG ShareMode; + ACCESS_MASK DesiredAccess; +} CD_CREATE_OBJECT_INFORMATION, *PCD_CREATE_OBJECT_INFORMATION; + +// +// Create EA buffers. +// + +#define CD_BROKER_EA_NAME "broker" +#define CD_SERVER_EA_NAME "server" +#define CD_ATTACH_EA_NAME "attach" + +typedef struct _CD_CREATE_SERVER { + HANDLE BrokerHandle; + LUID BrokerRequest; +} CD_CREATE_SERVER, *PCD_CREATE_SERVER; + +typedef struct _CD_ATTACH_INFORMATION { + HANDLE ProcessId; +} CD_ATTACH_INFORMATION, *PCD_ATTACH_INFORMATION; + +typedef struct _CD_ATTACH_INFORMATION64 { + PVOID64 ProcessId; +} CD_ATTACH_INFORMATION64, *PCD_ATTACH_INFORMATION64; + +// +// Information passed to the driver by a server when a connection is accepted. +// + +typedef struct _CD_CONNECTION_INFORMATION { + ULONG_PTR Process; + ULONG_PTR Input; + ULONG_PTR Output; +} CD_CONNECTION_INFORMATION, *PCD_CONNECTION_INFORMATION; + +// +// Ioctls. +// + +typedef struct _CD_IO_BUFFER { + ULONG Size; + PVOID Buffer; +} CD_IO_BUFFER, *PCD_IO_BUFFER; + +typedef struct _CD_IO_BUFFER64 { + ULONG Size; + PVOID64 Buffer; +} CD_IO_BUFFER64, *PCD_IO_BUFFER64; + +typedef struct _CD_USER_DEFINED_IO { + HANDLE Client; + ULONG InputCount; + ULONG OutputCount; + CD_IO_BUFFER Buffers[ANYSIZE_ARRAY]; +} CD_USER_DEFINED_IO, *PCD_USER_DEFINED_IO; + +typedef struct _CD_USER_DEFINED_IO64 { + PVOID64 Client; + ULONG InputCount; + ULONG OutputCount; + CD_IO_BUFFER64 Buffers[ANYSIZE_ARRAY]; +} CD_USER_DEFINED_IO64, *PCD_USER_DEFINED_IO64; + +typedef struct _CD_IO_BUFFER_DESCRIPTOR { + PVOID Data; + ULONG Size; + ULONG Offset; +} CD_IO_BUFFER_DESCRIPTOR, *PCD_IO_BUFFER_DESCRIPTOR; + +typedef struct _CD_IO_COMPLETE { + LUID Identifier; + IO_STATUS_BLOCK IoStatus; + CD_IO_BUFFER_DESCRIPTOR Write; +} CD_IO_COMPLETE, *PCD_IO_COMPLETE; + +typedef struct _CD_IO_OPERATION { + LUID Identifier; + CD_IO_BUFFER_DESCRIPTOR Buffer; +} CD_IO_OPERATION, *PCD_IO_OPERATION; + +typedef struct _CD_IO_SERVER_INFORMATION { + HANDLE InputAvailableEvent; +} CD_IO_SERVER_INFORMATION, *PCD_IO_SERVER_INFORMATION; + +typedef struct _CD_IO_DISPLAY_SIZE { + ULONG Width; + ULONG Height; +} CD_IO_DISPLAY_SIZE, *PCD_IO_DISPLAY_SIZE; + +typedef struct _CD_IO_CHARACTER { + WCHAR Character; + USHORT Atribute; +} CD_IO_CHARACTER, *PCD_IO_CHARACTER; + +typedef struct _CD_IO_ROW_INFORMATION { + SHORT Index; + PCD_IO_CHARACTER Old; + PCD_IO_CHARACTER New; +} CD_IO_ROW_INFORMATION, *PCD_IO_ROW_INFORMATION; + +typedef struct _CD_IO_CURSOR_INFORMATION { + USHORT Column; + USHORT Row; + ULONG Height; + BOOLEAN IsVisible; +} CD_IO_CURSOR_INFORMATION, *PCD_IO_CURSOR_INFORMATION; + +#define IOCTL_CONDRV_READ_IO \ + CTL_CODE(FILE_DEVICE_CONSOLE, 1, METHOD_OUT_DIRECT, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_COMPLETE_IO \ + CTL_CODE(FILE_DEVICE_CONSOLE, 2, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_READ_INPUT \ + CTL_CODE(FILE_DEVICE_CONSOLE, 3, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_WRITE_OUTPUT \ + CTL_CODE(FILE_DEVICE_CONSOLE, 4, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_ISSUE_USER_IO \ + CTL_CODE(FILE_DEVICE_CONSOLE, 5, METHOD_OUT_DIRECT, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_DISCONNECT_PIPE \ + CTL_CODE(FILE_DEVICE_CONSOLE, 6, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_SET_SERVER_INFORMATION \ + CTL_CODE(FILE_DEVICE_CONSOLE, 7, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_GET_SERVER_PID \ + CTL_CODE(FILE_DEVICE_CONSOLE, 8, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_GET_DISPLAY_SIZE \ + CTL_CODE(FILE_DEVICE_CONSOLE, 9, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_UPDATE_DISPLAY \ + CTL_CODE(FILE_DEVICE_CONSOLE, 10, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_SET_CURSOR \ + CTL_CODE(FILE_DEVICE_CONSOLE, 11, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_ALLOW_VIA_UIACCESS \ + CTL_CODE(FILE_DEVICE_CONSOLE, 12, METHOD_NEITHER, FILE_ANY_ACCESS) + +#define IOCTL_CONDRV_LAUNCH_SERVER \ + CTL_CODE(FILE_DEVICE_CONSOLE, 13, METHOD_NEITHER, FILE_ANY_ACCESS) diff --git a/dep/Console/conmsgl1.h b/dep/Console/conmsgl1.h new file mode 100644 index 000000000..41ef9c741 --- /dev/null +++ b/dep/Console/conmsgl1.h @@ -0,0 +1,156 @@ +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + +Module Name: + + conmsgl1.h + +Abstract: + + This include file defines the layer 1 message formats used to communicate + between the client and server portions of the CONSOLE portion of the + Windows subsystem. + +Author: + + Therese Stowell (thereses) 10-Nov-1990 + +Revision History: + + Wedson Almeida Filho (wedsonaf) 23-May-2010 + Modified the messages for use with the console driver. + +--*/ + +#pragma once + +#define CONSOLE_FIRST_API_NUMBER(Layer) \ + (Layer << 24) \ + +typedef struct _CONSOLE_SERVER_MSG { + ULONG IconId; + ULONG HotKey; + ULONG StartupFlags; + USHORT FillAttribute; + USHORT ShowWindow; + COORD ScreenBufferSize; + COORD WindowSize; + COORD WindowOrigin; + ULONG ProcessGroupId; + BOOLEAN ConsoleApp; + BOOLEAN WindowVisible; + USHORT TitleLength; + WCHAR Title[MAX_PATH + 1]; + USHORT ApplicationNameLength; + WCHAR ApplicationName[128]; + USHORT CurrentDirectoryLength; + WCHAR CurrentDirectory[MAX_PATH + 1]; +} CONSOLE_SERVER_MSG, *PCONSOLE_SERVER_MSG; + +typedef struct _CONSOLE_BROKER_DATA { + WCHAR DesktopName[MAX_PATH]; +} CONSOLE_BROKER_MSG, *PCONSOLE_BROKER_MSG; + +typedef struct _CONSOLE_GETCP_MSG { + OUT ULONG CodePage; + IN BOOLEAN Output; +} CONSOLE_GETCP_MSG, *PCONSOLE_GETCP_MSG; + +typedef struct _CONSOLE_MODE_MSG { + IN OUT ULONG Mode; +} CONSOLE_MODE_MSG, *PCONSOLE_MODE_MSG; + +typedef struct _CONSOLE_GETNUMBEROFINPUTEVENTS_MSG { + OUT ULONG ReadyEvents; +} CONSOLE_GETNUMBEROFINPUTEVENTS_MSG, *PCONSOLE_GETNUMBEROFINPUTEVENTS_MSG; + +typedef struct _CONSOLE_GETCONSOLEINPUT_MSG { + OUT ULONG NumRecords; + IN USHORT Flags; + IN BOOLEAN Unicode; +} CONSOLE_GETCONSOLEINPUT_MSG, *PCONSOLE_GETCONSOLEINPUT_MSG; + +typedef struct _CONSOLE_READCONSOLE_MSG { + IN BOOLEAN Unicode; + IN BOOLEAN ProcessControlZ; + IN USHORT ExeNameLength; + IN ULONG InitialNumBytes; + IN ULONG CtrlWakeupMask; + OUT ULONG ControlKeyState; + OUT ULONG NumBytes; +} CONSOLE_READCONSOLE_MSG, *PCONSOLE_READCONSOLE_MSG; + +typedef struct _CONSOLE_WRITECONSOLE_MSG { + OUT ULONG NumBytes; + IN BOOLEAN Unicode; +} CONSOLE_WRITECONSOLE_MSG, *PCONSOLE_WRITECONSOLE_MSG; + +typedef struct _CONSOLE_LANGID_MSG { + OUT LANGID LangId; +} CONSOLE_LANGID_MSG, *PCONSOLE_LANGID_MSG; + +typedef struct _CONSOLE_MAPBITMAP_MSG { + OUT HANDLE Mutex; + OUT PVOID Bitmap; +} CONSOLE_MAPBITMAP_MSG, *PCONSOLE_MAPBITMAP_MSG; + +typedef struct _CONSOLE_MAPBITMAP_MSG64 { + OUT PVOID64 Mutex; + OUT PVOID64 Bitmap; +} CONSOLE_MAPBITMAP_MSG64, *PCONSOLE_MAPBITMAP_MSG64; + +typedef enum _CONSOLE_API_NUMBER_L1 { + ConsolepGetCP = CONSOLE_FIRST_API_NUMBER(1), + ConsolepGetMode, + ConsolepSetMode, + ConsolepGetNumberOfInputEvents, + ConsolepGetConsoleInput, + ConsolepReadConsole, + ConsolepWriteConsole, + ConsolepNotifyLastClose, + ConsolepGetLangId, + ConsolepMapBitmap, +} CONSOLE_API_NUMBER_L1, *PCONSOLE_API_NUMBER_L1; + +typedef struct _CONSOLE_MSG_HEADER { + ULONG ApiNumber; + ULONG ApiDescriptorSize; +} CONSOLE_MSG_HEADER, *PCONSOLE_MSG_HEADER; + +typedef union _CONSOLE_MSG_BODY_L1 { + CONSOLE_GETCP_MSG GetConsoleCP; + CONSOLE_MODE_MSG GetConsoleMode; + CONSOLE_MODE_MSG SetConsoleMode; + CONSOLE_GETNUMBEROFINPUTEVENTS_MSG GetNumberOfConsoleInputEvents; + CONSOLE_GETCONSOLEINPUT_MSG GetConsoleInput; + CONSOLE_READCONSOLE_MSG ReadConsole; + CONSOLE_WRITECONSOLE_MSG WriteConsole; + CONSOLE_LANGID_MSG GetConsoleLangId; + +#if defined(BUILD_WOW6432) && !defined(BUILD_WOW3232) + + CONSOLE_MAPBITMAP_MSG64 MapBitmap; + +#else + + CONSOLE_MAPBITMAP_MSG MapBitmap; + +#endif + +} CONSOLE_MSG_BODY_L1, *PCONSOLE_MSG_BODY_L1; + +#ifndef __cplusplus +typedef struct _CONSOLE_MSG_L1 { + CONSOLE_MSG_HEADER Header; + union { + CONSOLE_MSG_BODY_L1; + } u; +} CONSOLE_MSG_L1, *PCONSOLE_MSG_L1; +#else +typedef struct _CONSOLE_MSG_L1 : + public CONSOLE_MSG_HEADER +{ + CONSOLE_MSG_BODY_L1 u; +} CONSOLE_MSG_L1, *PCONSOLE_MSG_L1; +#endif // __cplusplus diff --git a/dep/Console/conmsgl2.h b/dep/Console/conmsgl2.h new file mode 100644 index 000000000..d1abb749a --- /dev/null +++ b/dep/Console/conmsgl2.h @@ -0,0 +1,207 @@ +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + +Module Name: + + conmsgl2.h + +Abstract: + + This include file defines the layer 2 message formats used to communicate + between the client and server portions of the CONSOLE portion of the + Windows subsystem. + +Author: + + Therese Stowell (thereses) 10-Nov-1990 + +Revision History: + + Wedson Almeida Filho (wedsonaf) 23-May-2010 + Modified the messages for use with the console driver. + +--*/ + +#pragma once + +typedef struct _CONSOLE_CREATESCREENBUFFER_MSG { + IN ULONG Flags; + IN ULONG BitmapInfoLength; + IN ULONG Usage; +} CONSOLE_CREATESCREENBUFFER_MSG, *PCONSOLE_CREATESCREENBUFFER_MSG; + +#define CONSOLE_ASCII 0x1 +#define CONSOLE_REAL_UNICODE 0x2 +#define CONSOLE_ATTRIBUTE 0x3 +#define CONSOLE_FALSE_UNICODE 0x4 + +typedef struct _CONSOLE_FILLCONSOLEOUTPUT_MSG { + IN COORD WriteCoord; + IN ULONG ElementType; + IN USHORT Element; + IN OUT ULONG Length; +} CONSOLE_FILLCONSOLEOUTPUT_MSG, *PCONSOLE_FILLCONSOLEOUTPUT_MSG; + +typedef struct _CONSOLE_CTRLEVENT_MSG { + IN ULONG CtrlEvent; + IN ULONG ProcessGroupId; +} CONSOLE_CTRLEVENT_MSG, *PCONSOLE_CTRLEVENT_MSG; + +typedef struct _CONSOLE_SETCP_MSG { + IN ULONG CodePage; + IN BOOLEAN Output; +} CONSOLE_SETCP_MSG, *PCONSOLE_SETCP_MSG; + +typedef struct _CONSOLE_GETCURSORINFO_MSG { + OUT ULONG CursorSize; + OUT BOOLEAN Visible; +} CONSOLE_GETCURSORINFO_MSG, *PCONSOLE_GETCURSORINFO_MSG; + +typedef struct _CONSOLE_SETCURSORINFO_MSG { + IN ULONG CursorSize; + IN BOOLEAN Visible; +} CONSOLE_SETCURSORINFO_MSG, *PCONSOLE_SETCURSORINFO_MSG; + +typedef struct _CONSOLE_SCREENBUFFERINFO_MSG { + IN OUT COORD Size; + IN OUT COORD CursorPosition; + IN OUT COORD ScrollPosition; + IN OUT USHORT Attributes; + IN OUT COORD CurrentWindowSize; + IN OUT COORD MaximumWindowSize; + IN OUT USHORT PopupAttributes; + IN OUT BOOLEAN FullscreenSupported; + IN OUT COLORREF ColorTable[16]; +} CONSOLE_SCREENBUFFERINFO_MSG, *PCONSOLE_SCREENBUFFERINFO_MSG; + +typedef struct _CONSOLE_SETSCREENBUFFERSIZE_MSG { + IN COORD Size; +} CONSOLE_SETSCREENBUFFERSIZE_MSG, *PCONSOLE_SETSCREENBUFFERSIZE_MSG; + +typedef struct _CONSOLE_SETCURSORPOSITION_MSG { + IN COORD CursorPosition; +} CONSOLE_SETCURSORPOSITION_MSG, *PCONSOLE_SETCURSORPOSITION_MSG; + +typedef struct _CONSOLE_GETLARGESTWINDOWSIZE_MSG { + OUT COORD Size; +} CONSOLE_GETLARGESTWINDOWSIZE_MSG, *PCONSOLE_GETLARGESTWINDOWSIZE_MSG; + +typedef struct _CONSOLE_SCROLLSCREENBUFFER_MSG { + IN SMALL_RECT ScrollRectangle; + IN SMALL_RECT ClipRectangle; + IN BOOLEAN Clip; + IN BOOLEAN Unicode; + IN COORD DestinationOrigin; + IN CHAR_INFO Fill; +} CONSOLE_SCROLLSCREENBUFFER_MSG, *PCONSOLE_SCROLLSCREENBUFFER_MSG; + +typedef struct _CONSOLE_SETTEXTATTRIBUTE_MSG { + IN USHORT Attributes; +} CONSOLE_SETTEXTATTRIBUTE_MSG, *PCONSOLE_SETTEXTATTRIBUTE_MSG; + +typedef struct _CONSOLE_SETWINDOWINFO_MSG { + IN BOOLEAN Absolute; + IN SMALL_RECT Window; +} CONSOLE_SETWINDOWINFO_MSG, *PCONSOLE_SETWINDOWINFO_MSG; + +typedef struct _CONSOLE_READCONSOLEOUTPUTSTRING_MSG { + IN COORD ReadCoord; + IN ULONG StringType; + OUT ULONG NumRecords; +} CONSOLE_READCONSOLEOUTPUTSTRING_MSG, *PCONSOLE_READCONSOLEOUTPUTSTRING_MSG; + +typedef struct _CONSOLE_WRITECONSOLEINPUT_MSG { + OUT ULONG NumRecords; + IN BOOLEAN Unicode; + IN BOOLEAN Append; +} CONSOLE_WRITECONSOLEINPUT_MSG, *PCONSOLE_WRITECONSOLEINPUT_MSG; + +typedef struct _CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG { + IN COORD WriteCoord; + IN ULONG StringType; + OUT ULONG NumRecords; +} CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG, *PCONSOLE_WRITECONSOLEOUTPUTSTRING_MSG; + +typedef struct _CONSOLE_WRITECONSOLEOUTPUT_MSG { + IN OUT SMALL_RECT CharRegion; + IN BOOLEAN Unicode; +} CONSOLE_WRITECONSOLEOUTPUT_MSG, *PCONSOLE_WRITECONSOLEOUTPUT_MSG; + +typedef struct _CONSOLE_READCONSOLEOUTPUT_MSG { + IN OUT SMALL_RECT CharRegion; + IN BOOLEAN Unicode; +} CONSOLE_READCONSOLEOUTPUT_MSG, *PCONSOLE_READCONSOLEOUTPUT_MSG; + +typedef struct _CONSOLE_GETTITLE_MSG { + OUT ULONG TitleLength; + IN BOOLEAN Unicode; + IN BOOLEAN Original; +} CONSOLE_GETTITLE_MSG, *PCONSOLE_GETTITLE_MSG; + +typedef struct _CONSOLE_SETTITLE_MSG { + IN BOOLEAN Unicode; +} CONSOLE_SETTITLE_MSG, *PCONSOLE_SETTITLE_MSG; + +typedef enum _CONSOLE_API_NUMBER_L2 { + ConsolepFillConsoleOutput = CONSOLE_FIRST_API_NUMBER(2), + ConsolepGenerateCtrlEvent, + ConsolepSetActiveScreenBuffer, + ConsolepFlushInputBuffer, + ConsolepSetCP, + ConsolepGetCursorInfo, + ConsolepSetCursorInfo, + ConsolepGetScreenBufferInfo, + ConsolepSetScreenBufferInfo, + ConsolepSetScreenBufferSize, + ConsolepSetCursorPosition, + ConsolepGetLargestWindowSize, + ConsolepScrollScreenBuffer, + ConsolepSetTextAttribute, + ConsolepSetWindowInfo, + ConsolepReadConsoleOutputString, + ConsolepWriteConsoleInput, + ConsolepWriteConsoleOutput, + ConsolepWriteConsoleOutputString, + ConsolepReadConsoleOutput, + ConsolepGetTitle, + ConsolepSetTitle, +} CONSOLE_API_NUMBER_L2, *PCONSOLE_API_NUMBER_L2; + +typedef union _CONSOLE_MSG_BODY_L2 { + CONSOLE_CTRLEVENT_MSG GenerateConsoleCtrlEvent; + CONSOLE_FILLCONSOLEOUTPUT_MSG FillConsoleOutput; + CONSOLE_SETCP_MSG SetConsoleCP; + CONSOLE_GETCURSORINFO_MSG GetConsoleCursorInfo; + CONSOLE_SETCURSORINFO_MSG SetConsoleCursorInfo; + CONSOLE_SCREENBUFFERINFO_MSG GetConsoleScreenBufferInfo; + CONSOLE_SCREENBUFFERINFO_MSG SetConsoleScreenBufferInfo; + CONSOLE_SETSCREENBUFFERSIZE_MSG SetConsoleScreenBufferSize; + CONSOLE_SETCURSORPOSITION_MSG SetConsoleCursorPosition; + CONSOLE_GETLARGESTWINDOWSIZE_MSG GetLargestConsoleWindowSize; + CONSOLE_SCROLLSCREENBUFFER_MSG ScrollConsoleScreenBuffer; + CONSOLE_SETTEXTATTRIBUTE_MSG SetConsoleTextAttribute; + CONSOLE_SETWINDOWINFO_MSG SetConsoleWindowInfo; + CONSOLE_READCONSOLEOUTPUTSTRING_MSG ReadConsoleOutputString; + CONSOLE_WRITECONSOLEINPUT_MSG WriteConsoleInput; + CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG WriteConsoleOutputString; + CONSOLE_WRITECONSOLEOUTPUT_MSG WriteConsoleOutput; + CONSOLE_READCONSOLEOUTPUT_MSG ReadConsoleOutput; + CONSOLE_SETTITLE_MSG SetConsoleTitle; + CONSOLE_GETTITLE_MSG GetConsoleTitle; +} CONSOLE_MSG_BODY_L2, *PCONSOLE_MSG_BODY_L2; + +#ifndef __cplusplus +typedef struct _CONSOLE_MSG_L2 { + CONSOLE_MSG_HEADER Header; + union { + CONSOLE_MSG_BODY_L2; + } u; +} CONSOLE_MSG_L2, *PCONSOLE_MSG_L2; +#else +typedef struct _CONSOLE_MSG_L2 : + public CONSOLE_MSG_HEADER +{ + CONSOLE_MSG_BODY_L2 u; +} CONSOLE_MSG_L2, *PCONSOLE_MSG_L2; +#endif // __cplusplus diff --git a/dep/Console/conmsgl3.h b/dep/Console/conmsgl3.h new file mode 100644 index 000000000..9e3d8e654 --- /dev/null +++ b/dep/Console/conmsgl3.h @@ -0,0 +1,392 @@ +/*++ + +Copyright (c) 1985 - 1999, Microsoft Corporation + +Module Name: + + conmsgl3.h + +Abstract: + + This include file defines the message formats used to communicate + between the client and server portions of the CONSOLE portion of the + Windows subsystem. + +Author: + + Therese Stowell (thereses) 10-Nov-1990 + +Revision History: + + Wedson Almeida Filho (wedsonaf) 23-May-2010 + Modified the messages for use with the console driver. + +--*/ + +#pragma once + +#include // need FONT_SELECT + +typedef struct _CONSOLE_GETNUMBEROFFONTS_MSG { + OUT ULONG NumberOfFonts; +} CONSOLE_GETNUMBEROFFONTS_MSG, *PCONSOLE_GETNUMBEROFFONTS_MSG; + +typedef struct _CONSOLE_GETSELECTIONINFO_MSG { + OUT CONSOLE_SELECTION_INFO SelectionInfo; +} CONSOLE_GETSELECTIONINFO_MSG, *PCONSOLE_GETSELECTIONINFO_MSG; + +typedef struct _CONSOLE_GETMOUSEINFO_MSG { + OUT ULONG NumButtons; +} CONSOLE_GETMOUSEINFO_MSG, *PCONSOLE_GETMOUSEINFO_MSG; + +typedef struct _CONSOLE_GETFONTINFO_MSG { + IN BOOLEAN MaximumWindow; + OUT ULONG NumFonts; // this value is valid even for error cases +} CONSOLE_GETFONTINFO_MSG, *PCONSOLE_GETFONTINFO_MSG; + +typedef struct _CONSOLE_GETFONTSIZE_MSG { + IN ULONG FontIndex; + OUT COORD FontSize; +} CONSOLE_GETFONTSIZE_MSG, *PCONSOLE_GETFONTSIZE_MSG; + +typedef struct _CONSOLE_CURRENTFONT_MSG { + IN BOOLEAN MaximumWindow; + IN OUT ULONG FontIndex; + IN OUT COORD FontSize; + IN OUT ULONG FontFamily; + IN OUT ULONG FontWeight; + IN OUT WCHAR FaceName[LF_FACESIZE]; +} CONSOLE_CURRENTFONT_MSG, *PCONSOLE_CURRENTFONT_MSG; + +typedef struct _CONSOLE_SETFONT_MSG { + IN ULONG FontIndex; +} CONSOLE_SETFONT_MSG, *PCONSOLE_SETFONT_MSG; + +typedef struct _CONSOLE_SETICON_MSG { + IN HICON hIcon; +} CONSOLE_SETICON_MSG, *PCONSOLE_SETICON_MSG; + +typedef struct _CONSOLE_SETICON_MSG64 { + IN PVOID64 hIcon; +} CONSOLE_SETICON_MSG64, *PCONSOLE_SETICON_MSG64; + +typedef struct _CONSOLE_ADDALIAS_MSG { + IN USHORT SourceLength; + IN USHORT TargetLength; + IN USHORT ExeLength; + IN BOOLEAN Unicode; +} CONSOLE_ADDALIAS_MSG, *PCONSOLE_ADDALIAS_MSG; + +typedef struct _CONSOLE_GETALIAS_MSG { + IN USHORT SourceLength; + OUT USHORT TargetLength; + IN USHORT ExeLength; + IN BOOLEAN Unicode; +} CONSOLE_GETALIAS_MSG, *PCONSOLE_GETALIAS_MSG; + +typedef struct _CONSOLE_GETALIASESLENGTH_MSG { + OUT ULONG AliasesLength; + IN BOOLEAN Unicode; +} CONSOLE_GETALIASESLENGTH_MSG, *PCONSOLE_GETALIASESLENGTH_MSG; + +typedef struct _CONSOLE_GETALIASEXESLENGTH_MSG { + OUT ULONG AliasExesLength; + IN BOOLEAN Unicode; +} CONSOLE_GETALIASEXESLENGTH_MSG, *PCONSOLE_GETALIASEXESLENGTH_MSG; + +typedef struct _CONSOLE_GETALIASES_MSG { + IN BOOLEAN Unicode; + OUT ULONG AliasesBufferLength; +} CONSOLE_GETALIASES_MSG, *PCONSOLE_GETALIASES_MSG; + +typedef struct _CONSOLE_GETALIASEXES_MSG { + OUT ULONG AliasExesBufferLength; + IN BOOLEAN Unicode; +} CONSOLE_GETALIASEXES_MSG, *PCONSOLE_GETALIASEXES_MSG; + +typedef struct _CONSOLE_EXPUNGECOMMANDHISTORY_MSG { + IN BOOLEAN Unicode; +} CONSOLE_EXPUNGECOMMANDHISTORY_MSG, *PCONSOLE_EXPUNGECOMMANDHISTORY_MSG; + +typedef struct _CONSOLE_SETNUMBEROFCOMMANDS_MSG { + IN ULONG NumCommands; + IN BOOLEAN Unicode; +} CONSOLE_SETNUMBEROFCOMMANDS_MSG, *PCONSOLE_SETNUMBEROFCOMMANDS_MSG; + +typedef struct _CONSOLE_GETCOMMANDHISTORYLENGTH_MSG { + OUT ULONG CommandHistoryLength; + IN BOOLEAN Unicode; +} CONSOLE_GETCOMMANDHISTORYLENGTH_MSG, *PCONSOLE_GETCOMMANDHISTORYLENGTH_MSG; + +typedef struct _CONSOLE_GETCOMMANDHISTORY_MSG { + OUT ULONG CommandBufferLength; + IN BOOLEAN Unicode; +} CONSOLE_GETCOMMANDHISTORY_MSG, *PCONSOLE_GETCOMMANDHISTORY_MSG; + +typedef struct _CONSOLE_INVALIDATERECT_MSG { + IN SMALL_RECT Rect; +} CONSOLE_INVALIDATERECT_MSG, *PCONSOLE_INVALIDATERECT_MSG; + +typedef struct _CONSOLE_VDM_MSG { + IN ULONG iFunction; + OUT BOOLEAN Bool; + IN OUT POINT Point; + OUT RECT Rect; +} CONSOLE_VDM_MSG, *PCONSOLE_VDM_MSG; + +typedef struct _CONSOLE_SETCURSOR_MSG { + IN HCURSOR CursorHandle; +} CONSOLE_SETCURSOR_MSG, *PCONSOLE_SETCURSOR_MSG; + +typedef struct _CONSOLE_SETCURSOR_MSG64 { + IN PVOID64 CursorHandle; +} CONSOLE_SETCURSOR_MSG64, *PCONSOLE_SETCURSOR_MSG64; + +typedef struct _CONSOLE_SHOWCURSOR_MSG { + IN BOOLEAN bShow; + OUT ULONG DisplayCount; +} CONSOLE_SHOWCURSOR_MSG, *PCONSOLE_SHOWCURSOR_MSG; + +typedef struct _CONSOLE_MENUCONTROL_MSG { + IN ULONG CommandIdLow; + IN ULONG CommandIdHigh; + OUT HMENU hMenu; +} CONSOLE_MENUCONTROL_MSG, *PCONSOLE_MENUCONTROL_MSG; + +typedef struct _CONSOLE_MENUCONTROL_MSG64 { + IN ULONG CommandIdLow; + IN ULONG CommandIdHigh; + OUT PVOID64 hMenu; +} CONSOLE_MENUCONTROL_MSG64, *PCONSOLE_MENUCONTROL_MSG64; + +typedef struct _CONSOLE_SETPALETTE_MSG { + IN HPALETTE hPalette; + IN ULONG dwUsage; +} CONSOLE_SETPALETTE_MSG, *PCONSOLE_SETPALETTE_MSG; + +typedef struct _CONSOLE_SETPALETTE_MSG64 { + IN PVOID64 hPalette; + IN ULONG dwUsage; +} CONSOLE_SETPALETTE_MSG64, *PCONSOLE_SETPALETTE_MSG64; + +typedef struct _CONSOLE_SETDISPLAYMODE_MSG { + IN ULONG dwFlags; + OUT COORD ScreenBufferDimensions; +} CONSOLE_SETDISPLAYMODE_MSG, *PCONSOLE_SETDISPLAYMODE_MSG; + +typedef struct _CONSOLE_REGISTERVDM_MSG { + IN ULONG RegisterFlags; + IN HANDLE StartEvent; + IN HANDLE EndEvent; + IN HANDLE ErrorEvent; + OUT ULONG StateLength; + OUT PVOID StateBuffer; + OUT PVOID VDMBuffer; +} CONSOLE_REGISTERVDM_MSG, *PCONSOLE_REGISTERVDM_MSG; + +typedef struct _CONSOLE_REGISTERVDM_MSG64 { + IN ULONG RegisterFlags; + IN PVOID64 StartEvent; + IN PVOID64 EndEvent; + IN PVOID64 ErrorEvent; + OUT ULONG StateLength; + OUT PVOID64 StateBuffer; + OUT PVOID64 VDMBuffer; +} CONSOLE_REGISTERVDM_MSG64, *PCONSOLE_REGISTERVDM_MSG64; + +typedef struct _CONSOLE_GETHARDWARESTATE_MSG { + OUT COORD Resolution; + OUT COORD FontSize; +} CONSOLE_GETHARDWARESTATE_MSG, *PCONSOLE_GETHARDWARESTATE_MSG; + +typedef struct _CONSOLE_SETHARDWARESTATE_MSG { + IN COORD Resolution; + IN COORD FontSize; +} CONSOLE_SETHARDWARESTATE_MSG, *PCONSOLE_SETHARDWARESTATE_MSG; + +typedef struct _CONSOLE_GETDISPLAYMODE_MSG { + OUT ULONG ModeFlags; +} CONSOLE_GETDISPLAYMODE_MSG, *PCONSOLE_GETDISPLAYMODE_MSG; + +typedef struct _CONSOLE_GETKEYBOARDLAYOUTNAME_MSG { + union { + WCHAR awchLayout[9]; + char achLayout[9]; + }; + BOOLEAN bAnsi; +} CONSOLE_GETKEYBOARDLAYOUTNAME_MSG, *PCONSOLE_GETKEYBOARDLAYOUTNAME_MSG; + +typedef struct _CONSOLE_SETKEYSHORTCUTS_MSG { + IN BOOLEAN Set; + IN BYTE ReserveKeys; +} CONSOLE_SETKEYSHORTCUTS_MSG, *PCONSOLE_SETKEYSHORTCUTS_MSG; + +typedef struct _CONSOLE_SETMENUCLOSE_MSG { + IN BOOLEAN Enable; +} CONSOLE_SETMENUCLOSE_MSG, *PCONSOLE_SETMENUCLOSE_MSG; + +typedef struct _CONSOLE_CHAR_TYPE_MSG { + IN COORD coordCheck; + OUT ULONG dwType; +} CONSOLE_CHAR_TYPE_MSG, *PCONSOLE_CHAR_TYPE_MSG; + +typedef struct _CONSOLE_LOCAL_EUDC_MSG { + IN USHORT CodePoint; + IN COORD FontSize; +} CONSOLE_LOCAL_EUDC_MSG, *PCONSOLE_LOCAL_EUDC_MSG; + +typedef struct _CONSOLE_CURSOR_MODE_MSG { + IN OUT BOOLEAN Blink; + IN OUT BOOLEAN DBEnable; +} CONSOLE_CURSOR_MODE_MSG, *PCONSOLE_CURSOR_MODE_MSG; + +typedef struct _CONSOLE_REGISTEROS2_MSG { + IN BOOLEAN fOs2Register; +} CONSOLE_REGISTEROS2_MSG, *PCONSOLE_REGISTEROS2_MSG; + +typedef struct _CONSOLE_SETOS2OEMFORMAT_MSG { + IN BOOLEAN fOs2OemFormat; +} CONSOLE_SETOS2OEMFORMAT_MSG, *PCONSOLE_SETOS2OEMFORMAT_MSG; + +typedef struct _CONSOLE_NLS_MODE_MSG { + IN OUT BOOLEAN Ready; + IN ULONG NlsMode; +} CONSOLE_NLS_MODE_MSG, *PCONSOLE_NLS_MODE_MSG; + +typedef struct _CONSOLE_GETCONSOLEWINDOW_MSG { + OUT HWND hwnd; +} CONSOLE_GETCONSOLEWINDOW_MSG, *PCONSOLE_GETCONSOLEWINDOW_MSG; + +typedef struct _CONSOLE_GETCONSOLEWINDOW_MSG64 { + OUT PVOID64 hwnd; +} CONSOLE_GETCONSOLEWINDOW_MSG64, *PCONSOLE_GETCONSOLEWINDOW_MSG64; + +typedef struct _CONSOLE_GETPROCESSLIST_MSG { + OUT ULONG dwProcessCount; +} CONSOLE_GETCONSOLEPROCESSLIST_MSG, *PCONSOLE_GETCONSOLEPROCESSLIST_MSG; + +typedef struct _CONSOLE_GETHISTORY_MSG { + OUT ULONG HistoryBufferSize; + OUT ULONG NumberOfHistoryBuffers; + OUT ULONG dwFlags; +} CONSOLE_HISTORY_MSG, *PCONSOLE_HISTORY_MSG; + +typedef enum _CONSOLE_API_NUMBER_L3 { + ConsolepGetNumberOfFonts = CONSOLE_FIRST_API_NUMBER(3), + ConsolepGetMouseInfo, + ConsolepGetFontInfo, + ConsolepGetFontSize, + ConsolepGetCurrentFont, + ConsolepSetFont, + ConsolepSetIcon, + ConsolepInvalidateBitmapRect, + ConsolepVDMOperation, + ConsolepSetCursor, + ConsolepShowCursor, + ConsolepMenuControl, + ConsolepSetPalette, + ConsolepSetDisplayMode, + ConsolepRegisterVDM, + ConsolepGetHardwareState, + ConsolepSetHardwareState, + ConsolepGetDisplayMode, + ConsolepAddAlias, + ConsolepGetAlias, + ConsolepGetAliasesLength, + ConsolepGetAliasExesLength, + ConsolepGetAliases, + ConsolepGetAliasExes, + ConsolepExpungeCommandHistory, + ConsolepSetNumberOfCommands, + ConsolepGetCommandHistoryLength, + ConsolepGetCommandHistory, + ConsolepSetKeyShortcuts, + ConsolepSetMenuClose, + ConsolepGetKeyboardLayoutName, + ConsolepGetConsoleWindow, + ConsolepCharType, + ConsolepSetLocalEUDC, + ConsolepSetCursorMode, + ConsolepGetCursorMode, + ConsolepRegisterOS2, + ConsolepSetOS2OemFormat, + ConsolepGetNlsMode, + ConsolepSetNlsMode, + ConsolepGetSelectionInfo, + ConsolepGetConsoleProcessList, + ConsolepGetHistory, + ConsolepSetHistory, + ConsolepSetCurrentFont, +} CONSOLE_API_NUMBER_L3, *PCONSOLE_API_NUMBER_L3; + +typedef union _CONSOLE_MSG_BODY_L3 { + CONSOLE_GETNUMBEROFFONTS_MSG GetNumberOfConsoleFonts; + CONSOLE_GETMOUSEINFO_MSG GetConsoleMouseInfo; + CONSOLE_GETFONTINFO_MSG GetConsoleFontInfo; + CONSOLE_GETFONTSIZE_MSG GetConsoleFontSize; + CONSOLE_CURRENTFONT_MSG GetCurrentConsoleFont; + CONSOLE_SETFONT_MSG SetConsoleFont; + CONSOLE_INVALIDATERECT_MSG InvalidateConsoleBitmapRect; + CONSOLE_VDM_MSG VDMConsoleOperation; + CONSOLE_SHOWCURSOR_MSG ShowConsoleCursor; + CONSOLE_SETDISPLAYMODE_MSG SetConsoleDisplayMode; +#ifdef BUILD_WOW6432 + CONSOLE_REGISTERVDM_MSG64 RegisterConsoleVDM; + CONSOLE_SETCURSOR_MSG64 SetConsoleCursor; + CONSOLE_SETICON_MSG64 SetConsoleIcon; + CONSOLE_MENUCONTROL_MSG64 ConsoleMenuControl; + CONSOLE_SETPALETTE_MSG64 SetConsolePalette; + CONSOLE_GETCONSOLEWINDOW_MSG64 GetConsoleWindow; +#else + CONSOLE_REGISTERVDM_MSG RegisterConsoleVDM; + CONSOLE_SETCURSOR_MSG SetConsoleCursor; + CONSOLE_SETICON_MSG SetConsoleIcon; + CONSOLE_MENUCONTROL_MSG ConsoleMenuControl; + CONSOLE_SETPALETTE_MSG SetConsolePalette; + CONSOLE_GETCONSOLEWINDOW_MSG GetConsoleWindow; +#endif + CONSOLE_GETHARDWARESTATE_MSG GetConsoleHardwareState; + CONSOLE_SETHARDWARESTATE_MSG SetConsoleHardwareState; + CONSOLE_GETDISPLAYMODE_MSG GetConsoleDisplayMode; + CONSOLE_ADDALIAS_MSG AddConsoleAliasW; + CONSOLE_GETALIAS_MSG GetConsoleAliasW; + CONSOLE_GETALIASESLENGTH_MSG GetConsoleAliasesLengthW; + CONSOLE_GETALIASEXESLENGTH_MSG GetConsoleAliasExesLengthW; + CONSOLE_GETALIASES_MSG GetConsoleAliasesW; + CONSOLE_GETALIASEXES_MSG GetConsoleAliasExesW; + CONSOLE_EXPUNGECOMMANDHISTORY_MSG ExpungeConsoleCommandHistoryW; + CONSOLE_SETNUMBEROFCOMMANDS_MSG SetConsoleNumberOfCommandsW; + CONSOLE_GETCOMMANDHISTORYLENGTH_MSG GetConsoleCommandHistoryLengthW; + CONSOLE_GETCOMMANDHISTORY_MSG GetConsoleCommandHistoryW; + CONSOLE_SETKEYSHORTCUTS_MSG SetConsoleKeyShortcuts; + CONSOLE_SETMENUCLOSE_MSG SetConsoleMenuClose; + CONSOLE_GETKEYBOARDLAYOUTNAME_MSG GetKeyboardLayoutName; + CONSOLE_CHAR_TYPE_MSG GetConsoleCharType; + CONSOLE_LOCAL_EUDC_MSG SetConsoleLocalEUDC; + CONSOLE_CURSOR_MODE_MSG SetConsoleCursorMode; + CONSOLE_CURSOR_MODE_MSG GetConsoleCursorMode; + CONSOLE_REGISTEROS2_MSG RegisterConsoleOS2; + CONSOLE_SETOS2OEMFORMAT_MSG SetConsoleOS2OemFormat; + CONSOLE_NLS_MODE_MSG GetConsoleNlsMode; + CONSOLE_NLS_MODE_MSG SetConsoleNlsMode; + CONSOLE_GETSELECTIONINFO_MSG GetConsoleSelectionInfo; + CONSOLE_GETCONSOLEPROCESSLIST_MSG GetConsoleProcessList; + CONSOLE_CURRENTFONT_MSG SetCurrentConsoleFont; + CONSOLE_HISTORY_MSG SetConsoleHistory; + CONSOLE_HISTORY_MSG GetConsoleHistory; +} CONSOLE_MSG_BODY_L3, *PCONSOLE_MSG_BODY_L3; + +#ifndef __cplusplus +typedef struct _CONSOLE_MSG_L3 { + CONSOLE_MSG_HEADER Header; + union { + CONSOLE_MSG_BODY_L3; + } u; +} CONSOLE_MSG_L3, *PCONSOLE_MSG_L3; +#else +typedef struct _CONSOLE_MSG_L3 : + public CONSOLE_MSG_HEADER +{ + CONSOLE_MSG_BODY_L3 u; +} CONSOLE_MSG_L3, *PCONSOLE_MSG_L3; +#endif // __cplusplus diff --git a/dep/Console/ntcon.h b/dep/Console/ntcon.h new file mode 100644 index 000000000..9b1966728 --- /dev/null +++ b/dep/Console/ntcon.h @@ -0,0 +1,29 @@ +// +// Copyright (C) Microsoft. All rights reserved. +// +#ifndef _NTCON_ +#define _NTCON_ + +// +// originally in winconp.h +// +#define CONSOLE_DETACHED_PROCESS ((HANDLE)-1) +#define CONSOLE_NEW_CONSOLE ((HANDLE)-2) +#define CONSOLE_CREATE_NO_WINDOW ((HANDLE)-3) + +#define SYSTEM_ROOT_CONSOLE_EVENT 3 + +#define CONSOLE_READ_NOREMOVE 0x0001 +#define CONSOLE_READ_NOWAIT 0x0002 + +#define CONSOLE_READ_VALID (CONSOLE_READ_NOREMOVE | CONSOLE_READ_NOWAIT) + +#define CONSOLE_GRAPHICS_BUFFER 2 + +// +// These are flags stored in PEB::ProcessParameters::ConsoleFlags. +// +#define CONSOLE_IGNORE_CTRL_C 0x1 + + +#endif //_NTCON_ \ No newline at end of file diff --git a/dep/Console/winconp.h b/dep/Console/winconp.h new file mode 100644 index 000000000..eb05f3293 --- /dev/null +++ b/dep/Console/winconp.h @@ -0,0 +1,669 @@ + +#ifndef _WINCONP_ +#define _WINCONP_ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#if _MSC_VER >= 1200 +#pragma warning(push) +#pragma warning(disable:4820) // padding added after data member +#endif + +#include + +// +// History flags (internal) +// + +#define CHI_VALID_FLAGS (HISTORY_NO_DUP_FLAG) + +// +// Selection flags (internal) +// + +#define CONSOLE_SELECTION_INVERTED 0x0010 // selection is inverted (turned off) +#define CONSOLE_SELECTION_VALID (CONSOLE_SELECTION_IN_PROGRESS | \ + CONSOLE_SELECTION_NOT_EMPTY | \ + CONSOLE_MOUSE_SELECTION | \ + CONSOLE_MOUSE_DOWN) + + +WINBASEAPI +BOOL +WINAPI +GetConsoleKeyboardLayoutNameA( + _Out_writes_(KL_NAMELENGTH) LPSTR pszLayout); +WINBASEAPI +BOOL +WINAPI +GetConsoleKeyboardLayoutNameW( + _Out_writes_(KL_NAMELENGTH) LPWSTR pszLayout); +#ifdef UNICODE +#define GetConsoleKeyboardLayoutName GetConsoleKeyboardLayoutNameW +#else +#define GetConsoleKeyboardLayoutName GetConsoleKeyboardLayoutNameA +#endif // !UNICODE + +// +// Registry strings +// + +#define CONSOLE_REGISTRY_STRING L"Console" +#define CONSOLE_REGISTRY_FONTSIZE L"FontSize" +#define CONSOLE_REGISTRY_FONTFAMILY L"FontFamily" +#define CONSOLE_REGISTRY_BUFFERSIZE L"ScreenBufferSize" +#define CONSOLE_REGISTRY_CURSORSIZE L"CursorSize" +#define CONSOLE_REGISTRY_WINDOWMAXIMIZED L"WindowMaximized" +#define CONSOLE_REGISTRY_WINDOWSIZE L"WindowSize" +#define CONSOLE_REGISTRY_WINDOWPOS L"WindowPosition" +#define CONSOLE_REGISTRY_WINDOWALPHA L"WindowAlpha" +#define CONSOLE_REGISTRY_FILLATTR L"ScreenColors" +#define CONSOLE_REGISTRY_POPUPATTR L"PopupColors" +#define CONSOLE_REGISTRY_FULLSCR L"FullScreen" +#define CONSOLE_REGISTRY_QUICKEDIT L"QuickEdit" +#define CONSOLE_REGISTRY_FACENAME L"FaceName" +#define CONSOLE_REGISTRY_FONTWEIGHT L"FontWeight" +#define CONSOLE_REGISTRY_INSERTMODE L"InsertMode" +#define CONSOLE_REGISTRY_HISTORYSIZE L"HistoryBufferSize" +#define CONSOLE_REGISTRY_HISTORYBUFS L"NumberOfHistoryBuffers" +#define CONSOLE_REGISTRY_HISTORYNODUP L"HistoryNoDup" +#define CONSOLE_REGISTRY_COLORTABLE L"ColorTable%02u" +#define CONSOLE_REGISTRY_EXTENDEDEDITKEY L"ExtendedEditKey" +#define CONSOLE_REGISTRY_EXTENDEDEDITKEY_CUSTOM L"ExtendedEditkeyCustom" +#define CONSOLE_REGISTRY_WORD_DELIM L"WordDelimiters" +#define CONSOLE_REGISTRY_TRIMZEROHEADINGS L"TrimLeadingZeros" +#define CONSOLE_REGISTRY_LOAD_CONIME L"LoadConIme" +#define CONSOLE_REGISTRY_ENABLE_COLOR_SELECTION L"EnableColorSelection" +#define CONSOLE_REGISTRY_SCROLLSCALE L"ScrollScale" + +// V2 console settings +#define CONSOLE_REGISTRY_FORCEV2 L"ForceV2" +#define CONSOLE_REGISTRY_LINESELECTION L"LineSelection" +#define CONSOLE_REGISTRY_FILTERONPASTE L"FilterOnPaste" +#define CONSOLE_REGISTRY_LINEWRAP L"LineWrap" +#define CONSOLE_REGISTRY_CTRLKEYSHORTCUTS_DISABLED L"CtrlKeyShortcutsDisabled" +#define CONSOLE_REGISTRY_ALLOW_ALTF4_CLOSE L"AllowAltF4Close" +#define CONSOLE_REGISTRY_VIRTTERM_LEVEL L"VirtualTerminalLevel" + +#define CONSOLE_REGISTRY_CURSORTYPE L"CursorType" +#define CONSOLE_REGISTRY_CURSORCOLOR L"CursorColor" + +#define CONSOLE_REGISTRY_INTERCEPTCOPYPASTE L"InterceptCopyPaste" + +#define CONSOLE_REGISTRY_COPYCOLOR L"CopyColor" +#define CONSOLE_REGISTRY_USEDX L"UseDx" + +#define CONSOLE_REGISTRY_DEFAULTFOREGROUND L"DefaultForeground" +#define CONSOLE_REGISTRY_DEFAULTBACKGROUND L"DefaultBackground" +#define CONSOLE_REGISTRY_TERMINALSCROLLING L"TerminalScrolling" +// end V2 console settings + + /* + * Starting code page + */ +#define CONSOLE_REGISTRY_CODEPAGE (L"CodePage") + +// +// registry strings on HKEY_LOCAL_MACHINE +// +#define MACHINE_REGISTRY_CONSOLE (L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Console") +#define MACHINE_REGISTRY_CONSOLEIME (L"ConsoleIME") +#define MACHINE_REGISTRY_ENABLE_CONIME_ON_SYSTEM_PROCESS (L"EnableConImeOnSystemProcess") + + +#define MACHINE_REGISTRY_CONSOLE_TTFONT (L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont") +#define MACHINE_REGISTRY_CONSOLE_TTFONT_WIN32_PATH (L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont") + + +#define MACHINE_REGISTRY_CONSOLE_NLS (L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\Nls") + + +#define MACHINE_REGISTRY_CONSOLE_FULLSCREEN (L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Console\\FullScreen") +#define MACHINE_REGISTRY_INITIAL_PALETTE (L"InitialPalette") +#define MACHINE_REGISTRY_COLOR_BUFFER (L"ColorBuffer") +#define MACHINE_REGISTRY_COLOR_BUFFER_NO_TRANSLATE (L"ColorBufferNoTranslate") +#define MACHINE_REGISTRY_MODE_FONT_PAIRS (L"ModeFontPairs") +#define MACHINE_REGISTRY_FS_CODEPAGE (L"CodePage") + + +#define MACHINE_REGISTRY_EUDC (L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage\\EUDCCodeRange") + + +// +// TrueType font list +// + +// doesn't available bold when add BOLD_MARK on first of face name. +#define BOLD_MARK (L'*') + +typedef struct _TT_FONT_LIST { + SINGLE_LIST_ENTRY List; + UINT CodePage; + BOOL fDisableBold; + TCHAR FaceName1[LF_FACESIZE]; + TCHAR FaceName2[LF_FACESIZE]; +} TTFONTLIST, *LPTTFONTLIST; + +// +// registry strings on HKEY_CURRENT_USER +// +#define PRELOAD_REGISTRY_STRING (L"Keyboard Layout\\Preload") + + +// +// Special key for previous word erase +// +#define EXTKEY_ERASE_PREV_WORD (0x7f) + +#ifndef NOGDI + +typedef struct _CONSOLE_GRAPHICS_BUFFER_INFO { + DWORD dwBitMapInfoLength; + LPBITMAPINFO lpBitMapInfo; + DWORD dwUsage; + HANDLE hMutex; + PVOID lpBitMap; +} CONSOLE_GRAPHICS_BUFFER_INFO, *PCONSOLE_GRAPHICS_BUFFER_INFO; + +#endif + +BOOL +WINAPI +InvalidateConsoleDIBits( + _In_ HANDLE hConsoleOutput, + _In_ PSMALL_RECT lpRect); + +VOID +WINAPI +SetLastConsoleEventActive( + VOID); + +#define VDM_HIDE_WINDOW 1 +#define VDM_IS_ICONIC 2 +#define VDM_CLIENT_RECT 3 +#define VDM_CLIENT_TO_SCREEN 4 +#define VDM_SCREEN_TO_CLIENT 5 +#define VDM_IS_HIDDEN 6 +#define VDM_FULLSCREEN_NOPAINT 7 +#define VDM_SET_VIDEO_MODE 8 + +BOOL +WINAPI +VDMConsoleOperation( + _In_ DWORD iFunction, + _Inout_opt_ LPVOID lpData); + + +BOOL +WINAPI +SetConsoleIcon( + _In_ HICON hIcon); + +// +// These console font APIs don't appear to be used anywhere. Maybe they +// should be removed. +// + +BOOL +WINAPI +SetConsoleFont( + _In_ HANDLE hConsoleOutput, + _In_ DWORD nFont); + +DWORD +WINAPI +GetConsoleFontInfo( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _In_ DWORD nLength, + _Out_ PCONSOLE_FONT_INFO lpConsoleFontInfo); + +DWORD +WINAPI +GetNumberOfConsoleFonts( + VOID); + +BOOL +WINAPI +SetConsoleCursor( + _In_ HANDLE hConsoleOutput, + _In_ HCURSOR hCursor); + +int +WINAPI +ShowConsoleCursor( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bShow); + +HMENU +APIENTRY +ConsoleMenuControl( + _In_ HANDLE hConsoleOutput, + _In_ UINT dwCommandIdLow, + _In_ UINT dwCommandIdHigh); + +BOOL +SetConsolePalette( + _In_ HANDLE hConsoleOutput, + _In_ HPALETTE hPalette, + _In_ UINT dwUsage); + +#define CONSOLE_UNREGISTER_VDM 0 +#define CONSOLE_REGISTER_VDM 1 +#define CONSOLE_REGISTER_WOW 2 + +BOOL +APIENTRY +RegisterConsoleVDM( + _In_ DWORD dwRegisterFlags, + _In_ HANDLE hStartHardwareEvent, + _In_ HANDLE hEndHardwareEvent, + _In_ HANDLE hErrorhardwareEvent, + _Reserved_ DWORD Reserved, + _Out_ LPDWORD lpStateLength, + _Outptr_ PVOID *lpState, + _In_opt_ COORD VDMBufferSize, + _Outptr_ PVOID *lpVDMBuffer); + +BOOL +APIENTRY +GetConsoleHardwareState( + _In_ HANDLE hConsoleOutput, + _Out_ PCOORD lpResolution, + _Out_ PCOORD lpFontSize); + +BOOL +APIENTRY +SetConsoleHardwareState( + _In_ HANDLE hConsoleOutput, + _In_ COORD dwResolution, + _In_ COORD dwFontSize); + +#define CONSOLE_NOSHORTCUTKEY 0x00000000 /* no shortcut key */ +#define CONSOLE_ALTTAB 0x00000001 /* Alt + Tab */ +#define CONSOLE_ALTESC 0x00000002 /* Alt + Escape */ +#define CONSOLE_ALTSPACE 0x00000004 /* Alt + Space */ +#define CONSOLE_ALTENTER 0x00000008 /* Alt + Enter */ +#define CONSOLE_ALTPRTSC 0x00000010 /* Alt Print screen */ +#define CONSOLE_PRTSC 0x00000020 /* Print screen */ +#define CONSOLE_CTRLESC 0x00000040 /* Ctrl + Escape */ + +typedef struct _APPKEY { + WORD Modifier; + WORD ScanCode; +} APPKEY, *LPAPPKEY; + +#define CONSOLE_MODIFIER_SHIFT 0x0003 // Left shift key +#define CONSOLE_MODIFIER_CONTROL 0x0004 // Either Control shift key +#define CONSOLE_MODIFIER_ALT 0x0008 // Either Alt shift key + +BOOL +APIENTRY +SetConsoleKeyShortcuts( + _In_ BOOL bSet, + _In_ BYTE bReserveKeys, + _In_reads_(dwNumAppKeys) LPAPPKEY lpAppKeys, + _In_ DWORD dwNumAppKeys); + +BOOL +APIENTRY +SetConsoleMenuClose( + _In_ BOOL bEnable); + +DWORD +GetConsoleInputExeNameA( + _In_ DWORD nBufferLength, + _Out_writes_(nBufferLength) LPSTR lpBuffer); +DWORD +GetConsoleInputExeNameW( + _In_ DWORD nBufferLength, + _Out_writes_(nBufferLength) LPWSTR lpBuffer); +#ifdef UNICODE +#define GetConsoleInputExeName GetConsoleInputExeNameW +#else +#define GetConsoleInputExeName GetConsoleInputExeNameA +#endif // !UNICODE + +BOOL +SetConsoleInputExeNameA( + _In_ LPSTR lpExeName); +BOOL +SetConsoleInputExeNameW( + _In_ LPWSTR lpExeName); +#ifdef UNICODE +#define SetConsoleInputExeName SetConsoleInputExeNameW +#else +#define SetConsoleInputExeName SetConsoleInputExeNameA +#endif // !UNICODE + +BOOL +WINAPI +ReadConsoleInputExA( + _In_ HANDLE hConsoleInput, + _Out_writes_(nLength) PINPUT_RECORD lpBuffer, + _In_ DWORD nLength, + _Out_ LPDWORD lpNumberOfEventsRead, + _In_ USHORT wFlags); +BOOL +WINAPI +ReadConsoleInputExW( + _In_ HANDLE hConsoleInput, + _Out_writes_(nLength) PINPUT_RECORD lpBuffer, + _In_ DWORD nLength, + _Out_ LPDWORD lpNumberOfEventsRead, + _In_ USHORT wFlags); +#ifdef UNICODE +#define ReadConsoleInputEx ReadConsoleInputExW +#else +#define ReadConsoleInputEx ReadConsoleInputExA +#endif // !UNICODE + +BOOL +WINAPI +WriteConsoleInputVDMA( + _In_ HANDLE hConsoleInput, + _In_reads_(nLength) PINPUT_RECORD lpBuffer, + _In_ DWORD nLength, + _Out_ LPDWORD lpNumberOfEventsWritten); +BOOL +WINAPI +WriteConsoleInputVDMW( + _In_ HANDLE hConsoleInput, + _In_reads_(nLength) PINPUT_RECORD lpBuffer, + _In_ DWORD nLength, + _Out_ LPDWORD lpNumberOfEventsWritten); +#ifdef UNICODE +#define WriteConsoleInputVDM WriteConsoleInputVDMW +#else +#define WriteConsoleInputVDM WriteConsoleInputVDMA +#endif // !UNICODE + + +BOOL +APIENTRY +GetConsoleNlsMode( + _In_ HANDLE hConsole, + _Out_ PDWORD lpdwNlsMode); + +BOOL +APIENTRY +SetConsoleNlsMode( + _In_ HANDLE hConsole, + _In_ DWORD fdwNlsMode); + +BOOL +APIENTRY +GetConsoleCharType( + _In_ HANDLE hConsole, + _In_ COORD coordCheck, + _Out_ PDWORD pdwType); + +#define CHAR_TYPE_SBCS 0 // Displayed SBCS character +#define CHAR_TYPE_LEADING 2 // Displayed leading byte of DBCS +#define CHAR_TYPE_TRAILING 3 // Displayed trailing byte of DBCS + +BOOL +APIENTRY +SetConsoleLocalEUDC( + _In_ HANDLE hConsoleHandle, + _In_ WORD wCodePoint, + _In_ COORD cFontSize, + _In_ PCHAR lpSB); + +BOOL +APIENTRY +SetConsoleCursorMode( + _In_ HANDLE hConsoleHandle, + _In_ BOOL Blink, + _In_ BOOL DBEnable); + +BOOL +APIENTRY +GetConsoleCursorMode( + _In_ HANDLE hConsoleHandle, + _Out_ PBOOL pbBlink, + _Out_ PBOOL pbDBEnable); + +BOOL +APIENTRY +RegisterConsoleOS2( + _In_ BOOL fOs2Register); + +BOOL +APIENTRY +SetConsoleOS2OemFormat( + _In_ BOOL fOs2OemFormat); + +BOOL +IsConsoleFullWidth( + _In_ HDC hDC, + _In_ DWORD CodePage, + _In_ WCHAR wch); + +#if defined(FE_IME) +BOOL +APIENTRY +RegisterConsoleIME( + _In_ HWND hWndConsoleIME, + _Out_opt_ DWORD *lpdwConsoleThreadId); + +BOOL +APIENTRY +UnregisterConsoleIME( + VOID); +#endif // FE_IME + +// +// These bits are always on for console handles and are used for routing +// by windows. +// + +#define CONSOLE_HANDLE_SIGNATURE 0x00000003 +#define CONSOLE_HANDLE_NEVERSET 0x10000000 +#define CONSOLE_HANDLE_MASK (CONSOLE_HANDLE_SIGNATURE | CONSOLE_HANDLE_NEVERSET) + +#define CONSOLE_HANDLE(HANDLE) (((ULONG_PTR)(HANDLE) & CONSOLE_HANDLE_MASK) == CONSOLE_HANDLE_SIGNATURE) + +// +// These strings are used to open console input or output. +// + +#define CONSOLE_INPUT_STRING L"CONIN$" +#define CONSOLE_OUTPUT_STRING L"CONOUT$" +#define CONSOLE_GENERIC L"CON" + +// +// this string is used to call RegisterWindowMessage to get +// progman's handle. +// + +#define CONSOLE_PROGMAN_HANDLE_MESSAGE "ConsoleProgmanHandle" + + +// +// stream API definitions. these API are only supposed to be used by +// subsystems (i.e. OpenFile routes to OpenConsoleW). +// + +HANDLE +APIENTRY +OpenConsoleW( + _In_ LPWSTR lpConsoleDevice, + _In_ DWORD dwDesiredAccess, + _In_ BOOL bInheritHandle, + _In_ DWORD dwShareMode); + +HANDLE +APIENTRY +DuplicateConsoleHandle( + _In_ HANDLE hSourceHandle, + _In_ DWORD dwDesiredAccess, + _In_ BOOL bInheritHandle, + _In_ DWORD dwOptions); + +BOOL +APIENTRY +GetConsoleHandleInformation( + _In_ HANDLE hObject, + _Out_ LPDWORD lpdwFlags); + +BOOL +APIENTRY +SetConsoleHandleInformation( + _In_ HANDLE hObject, + _In_ DWORD dwMask, + _In_ DWORD dwFlags); + +BOOL +APIENTRY +CloseConsoleHandle( + _In_ HANDLE hConsole); + +BOOL +APIENTRY +VerifyConsoleIoHandle( + _In_ HANDLE hIoHandle); + +HANDLE +APIENTRY +GetConsoleInputWaitHandle( + VOID); + +typedef struct _CONSOLE_STATE_INFO { + /* BEGIN V1 CONSOLE_STATE_INFO */ + COORD ScreenBufferSize; + COORD WindowSize; + INT WindowPosX; + INT WindowPosY; + COORD FontSize; + UINT FontFamily; + UINT FontWeight; + WCHAR FaceName[LF_FACESIZE]; + UINT CursorSize; + UINT FullScreen : 1; + UINT QuickEdit : 1; + UINT AutoPosition : 1; + UINT InsertMode : 1; + UINT HistoryNoDup : 1; + UINT FullScreenSupported : 1; + UINT UpdateValues : 1; + UINT Defaults : 1; + WORD ScreenAttributes; + WORD PopupAttributes; + UINT HistoryBufferSize; + UINT NumberOfHistoryBuffers; + COLORREF ColorTable[16]; + HWND hWnd; + HICON hIcon; + LPWSTR OriginalTitle; + LPWSTR LinkTitle; + + /* + * Starting code page + */ + UINT CodePage; + + /* END V1 CONSOLE_STATE_INFO */ + + /* BEGIN V2 CONSOLE_STATE_INFO */ + BOOL fIsV2Console; + BOOL fWrapText; + BOOL fFilterOnPaste; + BOOL fCtrlKeyShortcutsDisabled; + BOOL fLineSelection; + BYTE bWindowTransparency; + BOOL fWindowMaximized; + + unsigned int CursorType; + COLORREF CursorColor; + + BOOL InterceptCopyPaste; + + COLORREF DefaultForeground; + COLORREF DefaultBackground; + BOOL TerminalScrolling; + /* END V2 CONSOLE_STATE_INFO */ + +} CONSOLE_STATE_INFO, *PCONSOLE_STATE_INFO; + + +#ifdef DEFINE_CONSOLEV2_PROPERTIES +#define PID_CONSOLE_FORCEV2 1 +#define PID_CONSOLE_WRAPTEXT 2 +#define PID_CONSOLE_FILTERONPASTE 3 +#define PID_CONSOLE_CTRLKEYSDISABLED 4 +#define PID_CONSOLE_LINESELECTION 5 +#define PID_CONSOLE_WINDOWTRANSPARENCY 6 +#define PID_CONSOLE_WINDOWMAXIMIZED 7 +#define PID_CONSOLE_CURSOR_TYPE 8 +#define PID_CONSOLE_CURSOR_COLOR 9 +#define PID_CONSOLE_INTERCEPT_COPY_PASTE 10 +#define PID_CONSOLE_DEFAULTFOREGROUND 11 +#define PID_CONSOLE_DEFAULTBACKGROUND 12 +#define PID_CONSOLE_TERMINALSCROLLING 13 + +#define CONSOLE_PROPKEY(name, id) \ +DEFINE_PROPERTYKEY(name, 0x0C570607, 0x0396, 0x43DE, 0x9D, 0x61, 0xE3, 0x21, 0xD7, 0xDF, 0x50, 0x26, id); + +CONSOLE_PROPKEY(PKEY_Console_ForceV2, PID_CONSOLE_FORCEV2); +CONSOLE_PROPKEY(PKEY_Console_WrapText, PID_CONSOLE_WRAPTEXT); +CONSOLE_PROPKEY(PKEY_Console_FilterOnPaste, PID_CONSOLE_FILTERONPASTE); +CONSOLE_PROPKEY(PKEY_Console_CtrlKeyShortcutsDisabled, PID_CONSOLE_CTRLKEYSDISABLED); +CONSOLE_PROPKEY(PKEY_Console_LineSelection, PID_CONSOLE_LINESELECTION); +CONSOLE_PROPKEY(PKEY_Console_WindowTransparency, PID_CONSOLE_WINDOWTRANSPARENCY); +CONSOLE_PROPKEY(PKEY_Console_WindowMaximized, PID_CONSOLE_WINDOWMAXIMIZED); +CONSOLE_PROPKEY(PKEY_Console_CursorType, PID_CONSOLE_CURSOR_TYPE); +CONSOLE_PROPKEY(PKEY_Console_CursorColor, PID_CONSOLE_CURSOR_COLOR); +CONSOLE_PROPKEY(PKEY_Console_InterceptCopyPaste, PID_CONSOLE_INTERCEPT_COPY_PASTE); +CONSOLE_PROPKEY(PKEY_Console_DefaultForeground, PID_CONSOLE_DEFAULTFOREGROUND); +CONSOLE_PROPKEY(PKEY_Console_DefaultBackground, PID_CONSOLE_DEFAULTBACKGROUND); +CONSOLE_PROPKEY(PKEY_Console_TerminalScrolling, PID_CONSOLE_TERMINALSCROLLING); +#endif + + +// +// Ensure the alignment is WORD boundary +// + +#include + +typedef struct { + WORD wMod; + WORD wVirKey; + WCHAR wUnicodeChar; +} ExtKeySubst; + +typedef struct { + ExtKeySubst keys[3]; // 0: Ctrl + // 1: Alt + // 2: Ctrl+Alt +} ExtKeyDef; + +typedef ExtKeyDef ExtKeyDefTable['Z' - 'A' + 1]; + +typedef struct { + DWORD dwVersion; + DWORD dwCheckSum; + ExtKeyDefTable table; +} ExtKeyDefBuf; + +// +// Restore the previous alignment +// + +#include + + +#if _MSC_VER >= 1200 +#pragma warning(pop) +#endif + +#ifdef __cplusplus +} +#endif + +#endif // _WINCONP_ diff --git a/dep/NT/ntioapi_x.h b/dep/NT/ntioapi_x.h new file mode 100644 index 000000000..d89e0fc88 --- /dev/null +++ b/dep/NT/ntioapi_x.h @@ -0,0 +1,3 @@ +#pragma once + +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 diff --git a/dep/Win32K/winuserp.h b/dep/Win32K/winuserp.h new file mode 100644 index 000000000..faea9854f --- /dev/null +++ b/dep/Win32K/winuserp.h @@ -0,0 +1,12 @@ +/* + * Reserved console space. + * + * This was moved from the console code so that we can localize it + * in one place. This was necessary for dealing with the background + * color, which we need to have for the hungapp drawing. These are + * stored in the extra-window-bytes of each console. + */ +#define GWL_CONSOLE_WNDALLOC (3 * sizeof(DWORD)) +#define GWL_CONSOLE_PID 0 +#define GWL_CONSOLE_TID 4 +#define GWL_CONSOLE_BKCOLOR 8 \ No newline at end of file diff --git a/dep/WinAppDriver/EULA.rtf b/dep/WinAppDriver/EULA.rtf new file mode 100644 index 000000000..59009f028 --- /dev/null +++ b/dep/WinAppDriver/EULA.rtf @@ -0,0 +1,96 @@ +{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{\fonttbl{\f0\fswiss\fprq2\fcharset0 Tahoma;}{\f1\fswiss\fprq2\fcharset0 Arial;}{\f2\fnil\fcharset0 Calibri;}} +{\colortbl ;\red0\green0\blue255;} +{\stylesheet{ Normal;}{\s1 heading 1;}{\s2 heading 2;}} +{\*\generator Riched20 10.0.10586}\viewkind4\uc1 +\pard\widctlpar\sb120\sa120\b\f0\fs28 MICROSOFT SOFTWARE LICENSE TERMS\par + +\pard\brdrb\brdrs\brdrw10\brsp20 \widctlpar\sb120\sa120 Pre-Release and Evaluation EULA \endash Windows Application Driver\par + +\pard\widctlpar\sb120\sa120\b0\fs20 IF YOU LIVE IN (OR ARE A BUSINESS WITH YOUR PRINCIPAL PLACE OF BUSINESS IN) THE UNITED STATES, PLEASE READ THE \ldblquote BINDING ARBITRATION AND CLASS ACTION WAIVER\rdblquote SECTION BELOW. IT AFFECTS HOW DISPUTES ARE RESOLVED.\lang9\par + +\pard\brdrt\brdrs\brdrw10\brsp20 \widctlpar\sb120\sa120 These license terms are an agreement between you and Microsoft Corporation (or one of its affiliates). They apply to the software named above and any Microsoft services or software updates (except to the extent such services or updates are accompanied by new or \lang1033 additional terms, in which case those different terms apply prospectively and do not alter your or Microsoft\rquote s rights relating to pre-updated software or services\lang9 ). IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW.\par + +\pard +{\pntext\f0 1.\tab}{\*\pn\pnlvlbody\pnf0\pnindent360\pnstart1\pndec{\pntxta.}} +\widctlpar\s1\fi-357\li357\sb120\sa120\b\lang1033 INSTALLATION AND USE RIGHTS.\par + +\pard +{\pntext\f0 a)\tab}{\*\pn\pnlvlbody\pnf0\pnindent0\pnstart1\pnlcltr{\pntxta)}} +\widctlpar\s2\fi-360\li717\sb120\sa120 General. \b0 You may install and use any number of copies of the software on your devices, solely to i) evaluate for internal business purposes; and ii) design, develop and test your applications. \par +{\pntext\f0 b)\tab}\b Included Microsoft Applications. \b0 The software may include other Microsoft applications. These license terms apply to those included applications, if any, unless other license terms are provided with the other Microsoft applications.\par +{\pntext\f0 c)\tab}\b Third Party Applications. \b0 The software may include third party applications that Microsoft, not the third party, licenses to you under this agreement. Any included notices for third party applications are for your information only and are listed in Exhibit A to these license terms.\par +{\pntext\f0 d)\tab}\b No Distribution Rights. \b0 This agreement does not grant you a license to distribute nor sublicense all or part of the software to any third party.\par +{\pntext\f0 e)\tab}\b\lang9 Package Managers. \b0\lang1033 The software may include package managers, like NuGet, that give you the option to download other Microsoft and third party software packages to use with your application. Those packages are under their own licenses, and not this agreement. Microsoft does not distribute, license or provide any warranties for any of the third party packages\lang9 .\lang1033\par + +\pard\widctlpar\s1\fi-357\li357\sb120\sa120\b 2.\tab TIME-SENSITIVE SOFTWARE.\par + +\pard +{\pntext\f0 a)\tab}{\*\pn\pnlvlbody\pnf0\pnindent0\pnstart1\pnlcltr{\pntxta)}} +\widctlpar\s2\fi-360\li717\sb120\sa120 Period. \b0 The software is time-sensitive and may stop running on a date that is defined in the software.\par +{\pntext\f0 b)\tab}\b Notice. \b0 You may receive periodic reminder notices of this date through the software.\par +{\pntext\f0 c)\tab}\b Access to data. \b0 You may not be able to access data used in the software when it stops running.\par + +\pard\widctlpar\s1\fi-357\li357\sb120\sa120\b 3.\tab PRE-RELEASE SOFTWARE.\b0 The software is a pre-release version. It may not operate correctly. It may be different from the commercially released version.\par +\b 4.\tab FEEDBACK.\b0 If you give feedback about the software to Microsoft, you give to Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because Microsoft includes your feedback in them. These rights survive this agreement.\par +\b 5.\tab DATA.\b0 The software may collect information about you and your use of the software and send that to Microsoft. Microsoft may use this information to provide services and improve Microsoft\rquote s products and services. Your opt-out rights, if any, are described in\b \b0 the product documentation. Some features in the software may enable collection of data from users of your applications that access or use the software. If you use these features to enable data collection in your applications, you must comply with applicable law, including getting any required user consent, and maintain a prominent privacy policy that accurately informs users about how you use, collect, and share their data. You can learn more about Microsoft\rquote s data collection and use in the product documentation and the Microsoft Privacy Statement at {{\field{\*\fldinst{HYPERLINK http://go.microsoft.com/fwlink/?LinkID=521839 }}{\fldrslt{http://go.microsoft.com/fwlink/?LinkID=521839\ul0\cf0}}}}\f0\fs20 . You agree to comply with all applicable provisions of the Microsoft Privacy Statement.\par +\b 6.\tab SCOPE OF LICENSE.\b0 The software is licensed, not sold. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you will not (and have no right to):\par + +\pard +{\pntext\f0 a)\tab}{\*\pn\pnlvlbody\pnf0\pnindent0\pnstart1\pnlcltr{\pntxta)}} +\widctlpar\s2\fi-360\li717\sb120\sa120 work around any technical limitations in the software that only allow you to use it in certain ways;\par +{\pntext\f0 b)\tab}reverse engineer, decompile or disassemble the software;\par +{\pntext\f0 c)\tab}remove, minimize, block, or modify any notices of Microsoft or its suppliers in the software;\par +{\pntext\f0 d)\tab}use the software in any way that is against the law or to create or propagate malware; or\par +{\pntext\f0 e)\tab}share, publish, or lend the software (except for any distributable code, and then subject to the applicable terms above), provide the software as a stand-alone hosted solution for others to use, or transfer the software or this agreement to any third party.\par + +\pard\widctlpar\s1\fi-357\li357\sb120\sa120\b 7.\tab EXPORT RESTRICTIONS.\b0 You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit (aka.ms/exporting).\par +\b 8.\tab SUPPORT SERVICES.\b0 Microsoft is not obligated under this agreement to provide any support services for the software. Any support provided is \ldblquote as is\rdblquote , \ldblquote with all faults\rdblquote , and without warranty of any kind.\par +\b 9.\tab UPDATES.\b0 The software may periodically check for updates, and download and install them for you. You may obtain updates only from Microsoft or authorized sources. Microsoft may need to update your system to provide you with updates. You agree to receive these automatic updates without any additional notice. Updates may not include or support all existing software features, services, or peripheral devices.\par +\b 10.\tab BINDING ARBITRATION AND CLASS ACTION WAIVER.\b0 This Section applies if you live in (or, if a business, your principal place of business is in) the United States. If you and Microsoft have a dispute, you and Microsoft agree to try for 60 days to resolve it informally. If you and Microsoft can\rquote t, you and Microsoft agree to binding individual arbitration before the American Arbitration Association under the Federal Arbitration Act (\ldblquote FAA\rdblquote ), and not to sue in court in front of a judge or jury. Instead, a neutral arbitrator will decide. Class action lawsuits, class-wide arbitrations, private attorney-general actions, and any other proceeding where someone acts in a representative capacity are not allowed; nor is combining individual proceedings without the consent of all parties. The complete Arbitration Agreement contains more terms and is at aka.ms/arb-agreement-1. You and Microsoft agree to these terms.\par +\b 11.\tab ENTIRE AGREEMENT.\b0 This agreement, and any other terms Microsoft may provide for supplements, updates, or third-party applications, is the entire agreement for the software.\par +\b 12.\tab APPLICABLE LAW AND PLACE TO RESOLVE DISPUTES.\b0 If you acquired the software in the United States or Canada, the laws of the state or province where you live (or, if a business, where your principal place of business is located) govern the interpretation of this agreement, claims for its breach, and all other claims (including consumer protection, unfair competition, and tort claims), regardless of conflict of laws principles, except that the FAA governs everything related to arbitration. If you acquired the software in any other country, its laws apply, except that the FAA governs everything related to arbitration. If U.S. federal jurisdiction exists, you and Microsoft consent to exclusive jurisdiction and venue in the federal court in King County, Washington for all disputes heard in court (excluding arbitration). If not, you and Microsoft consent to exclusive jurisdiction and venue in the Superior Court of King County, Washington for all disputes heard in court (excluding arbitration).\par +\b 13.\tab CONSUMER RIGHTS; REGIONAL VARIATIONS\b0 . This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state, province, or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state, province, or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you:\par + +\pard +{\pntext\f0 a)\tab}{\*\pn\pnlvlbody\pnf0\pnindent0\pnstart1\pnlcltr{\pntxta)}} +\widctlpar\s2\fi-360\li717\sb120\sa120\b Australia.\b0 You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights.\par +{\pntext\f0 b)\tab}\b Canada.\b0 If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software.\par +{\pntext\f0 c)\tab}\b Germany and Austria\b0 .\par + +\pard\widctlpar\li717\sb120\sa120\b (i)\b0\tab\b Warranty.\b0 The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software.\par +\b (ii)\b0\tab\b Limitation of Liability\b0 . In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law.\par + +\pard\widctlpar\s1\li717\sb120\sa120 Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence.\par + +\pard\widctlpar\s1\fi-357\li357\sb120\sa120\b 14.\tab DISCLAIMER OF WARRANTY.\b0 \b THE SOFTWARE IS LICENSED \ldblquote AS IS.\rdblquote YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES, OR CONDITIONS. TO THE EXTENT PERMITTED UNDER APPLICABLE LAWS, MICROSOFT EXCLUDES ALL IMPLIED WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.\b0\par +\b 15.\tab LIMITATION ON AND EXCLUSION OF DAMAGES. IF YOU HAVE ANY BASIS FOR RECOVERING DAMAGES DESPITE THE PRECEDING DISCLAIMER OF WARRANTY, YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.\par + +\pard\widctlpar\li357\sb120\sa120\b0 This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, warranty, guarantee, or condition; strict liability, negligence, or other tort; or any other claim; in each case to the extent permitted by applicable law.\par +It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your state, province, or country may not allow the exclusion or limitation of incidental, consequential, or other damages.\par + +\pard\widctlpar\sb120\sa120\page\par + +\pard\widctlpar\sb120\sa120\qc EXHIBIT A\par +THIRD PARTY NOTICES AND INFORMATION\par +FOR\par +MICROSOFT WINDOWS APPLICATION DRIVER\par + +\pard\widctlpar\sb120\sa120\par +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION\line\line Note: While Microsoft is not the author of the files below, Microsoft is offering you a license subject to the terms of the Microsoft Software License Terms for Microsoft Windows Application Driver (the \ldblquote Microsoft Program\rdblquote ). Microsoft reserves all other rights. The notices below are provided for informational purposes only and are not the license terms under which Microsoft distributes these files.\par +The Microsoft Program includes the following third-party software:\par +1.\tab Newtonsoft.json version 7.0. ({\b{\field{\*\fldinst{HYPERLINK http://www.newtonsoft.com/json }}{\fldrslt{http://www.newtonsoft.com/json\ul0\cf0}}}}\f0\fs20 )\par +2.\tab Casablanca ({{\field{\*\fldinst{HYPERLINK http://casablanca.codeplex.com/ }}{\fldrslt{http://casablanca.codeplex.com/\ul0\cf0}}}}\f0\fs20 )\par +As the recipient of the above third-party software, Microsoft sets forth a copy of the notices and other information below.\par +1. Newtonsoft.json version 7.0.1 ({{\field{\*\fldinst{HYPERLINK http://www.newtonsoft.com/json }}{\fldrslt{http://www.newtonsoft.com/json\ul0\cf0}}}}\f0\fs20 )\par +NEWTONSOFT.JSON NOTICES AND INFORMATION BEGIN HERE\line =========================================\par +The MIT License (MIT)\line\line Copyright (c) 2007 James Newton-King\line\line Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\line\line The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\line\line THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\par +NEWTONSOFT.JSON NOTICES AND INFORMATION END HERE\line =========================================\par +2. Casablanca ({{\field{\*\fldinst{HYPERLINK http://casablanca.codeplex.com/ }}{\fldrslt{http://casablanca.codeplex.com/\ul0\cf0}}}}\f0\fs20 )\par +CASABLANCA NOTICES AND INFORMATION BEGIN HERE\line =========================================\par +Copyright (c) Microsoft Corporation. All rights reserved. \line Licensed under the Apache License, Version 2.0 (the "License");\line you may not use this file except in compliance with the License.\line You may obtain a copy of the License at\line {{\field{\*\fldinst{HYPERLINK http://www.apache.org/licenses/LICENSE-2.0 }}{\fldrslt{http://www.apache.org/licenses/LICENSE-2.0\ul0\cf0}}}}\f0\fs20\line\line Unless required by applicable law or agreed to in writing, software\line\f1\fs19 distributed under the License is distributed on an "AS IS" BASIS,\line WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\line See the License for the specific language governing permissions and\line limitations under the License.\fs20\par +\f0 CASABLANCA NOTICES AND INFORMATION END HERE\line =========================================\par +\par + +\pard\sa200\sl276\slmult1\f2\fs22\lang9\par +} + \ No newline at end of file diff --git a/dep/WinAppDriver/Microsoft.Win32.Primitives.dll b/dep/WinAppDriver/Microsoft.Win32.Primitives.dll new file mode 100644 index 000000000..88ef2df59 Binary files /dev/null and b/dep/WinAppDriver/Microsoft.Win32.Primitives.dll differ diff --git a/dep/WinAppDriver/Microsoft.Win32.Registry.dll b/dep/WinAppDriver/Microsoft.Win32.Registry.dll new file mode 100644 index 000000000..397e3c6e4 Binary files /dev/null and b/dep/WinAppDriver/Microsoft.Win32.Registry.dll differ diff --git a/dep/WinAppDriver/MitaBroker.dll b/dep/WinAppDriver/MitaBroker.dll new file mode 100644 index 000000000..3e562b3aa Binary files /dev/null and b/dep/WinAppDriver/MitaBroker.dll differ diff --git a/dep/WinAppDriver/MitaLite.Foundation.dll b/dep/WinAppDriver/MitaLite.Foundation.dll new file mode 100644 index 000000000..ab8ad1589 Binary files /dev/null and b/dep/WinAppDriver/MitaLite.Foundation.dll differ diff --git a/dep/WinAppDriver/MitaLite.Localization.dll b/dep/WinAppDriver/MitaLite.Localization.dll new file mode 100644 index 000000000..fac24db21 Binary files /dev/null and b/dep/WinAppDriver/MitaLite.Localization.dll differ diff --git a/dep/WinAppDriver/MitaLite.UIAutomationAdapter.dll b/dep/WinAppDriver/MitaLite.UIAutomationAdapter.dll new file mode 100644 index 000000000..bdfaa17ef Binary files /dev/null and b/dep/WinAppDriver/MitaLite.UIAutomationAdapter.dll differ diff --git a/dep/WinAppDriver/MitaLite.UIAutomationClient.dll b/dep/WinAppDriver/MitaLite.UIAutomationClient.dll new file mode 100644 index 000000000..7eb568781 Binary files /dev/null and b/dep/WinAppDriver/MitaLite.UIAutomationClient.dll differ diff --git a/dep/WinAppDriver/Newtonsoft.Json.dll b/dep/WinAppDriver/Newtonsoft.Json.dll new file mode 100644 index 000000000..be6558d2d Binary files /dev/null and b/dep/WinAppDriver/Newtonsoft.Json.dll differ diff --git a/dep/WinAppDriver/Readme.txt b/dep/WinAppDriver/Readme.txt new file mode 100644 index 000000000..954b4cbe8 --- /dev/null +++ b/dep/WinAppDriver/Readme.txt @@ -0,0 +1,7 @@ +Windows Application Driver (Beta) + +For documentation, sample code, and logging issues: +https://github.com/Microsoft/WinAppDriver + +To request new features and upvote requests filed by others: +https://wpdev.uservoice.com diff --git a/dep/WinAppDriver/System.Diagnostics.Process.dll b/dep/WinAppDriver/System.Diagnostics.Process.dll new file mode 100644 index 000000000..b98ff5369 Binary files /dev/null and b/dep/WinAppDriver/System.Diagnostics.Process.dll differ diff --git a/dep/WinAppDriver/System.Threading.Thread.dll b/dep/WinAppDriver/System.Threading.Thread.dll new file mode 100644 index 000000000..6c4083137 Binary files /dev/null and b/dep/WinAppDriver/System.Threading.Thread.dll differ diff --git a/dep/WinAppDriver/WinAppDriver.exe b/dep/WinAppDriver/WinAppDriver.exe new file mode 100644 index 000000000..03b810a8d Binary files /dev/null and b/dep/WinAppDriver/WinAppDriver.exe differ diff --git a/dep/WinAppDriver/WinAppDriverCore.dll b/dep/WinAppDriver/WinAppDriverCore.dll new file mode 100644 index 000000000..29f3b9279 Binary files /dev/null and b/dep/WinAppDriver/WinAppDriverCore.dll differ diff --git a/dep/WinAppDriver/cpprest140_2_8.dll b/dep/WinAppDriver/cpprest140_2_8.dll new file mode 100644 index 000000000..e6bf31dec Binary files /dev/null and b/dep/WinAppDriver/cpprest140_2_8.dll differ diff --git a/dep/gsl b/dep/gsl new file mode 160000 index 000000000..b74b286d5 --- /dev/null +++ b/dep/gsl @@ -0,0 +1 @@ +Subproject commit b74b286d5e333561b0f1ef1abd18de2606624455 diff --git a/dep/nuget/nuget.exe b/dep/nuget/nuget.exe new file mode 100644 index 000000000..856263ded Binary files /dev/null and b/dep/nuget/nuget.exe differ diff --git a/dep/packages/README.md b/dep/packages/README.md new file mode 100644 index 000000000..c1d56fb56 --- /dev/null +++ b/dep/packages/README.md @@ -0,0 +1,7 @@ +These packages are redistributed inside this folder because they are not yet available on a public NuGet feed. + +## Microsoft.UI.XAML +This package is a custom development build fork to help us light up tab support. It will eventually go onto the same public feed as the existing `Microsoft.UI.XAML` package that's currently available on NuGet.org + +## TAEF.Redist.WLK +This package is vetted for public redistribution and release, but the TAEF team hasn't set up a public feed to consume it yet. If/when they do, we'll move to that. diff --git a/dep/packages/microsoft.ui.xaml.2.1.190405001-prerelease.nupkg b/dep/packages/microsoft.ui.xaml.2.1.190405001-prerelease.nupkg new file mode 100644 index 000000000..dbc54406a Binary files /dev/null and b/dep/packages/microsoft.ui.xaml.2.1.190405001-prerelease.nupkg differ diff --git a/dep/packages/taef.redist.wlk.10.30.180808002.nupkg b/dep/packages/taef.redist.wlk.10.30.180808002.nupkg new file mode 100644 index 000000000..8b863b5d4 Binary files /dev/null and b/dep/packages/taef.redist.wlk.10.30.180808002.nupkg differ diff --git a/dep/telemetry/ProjectTelemetry.h b/dep/telemetry/ProjectTelemetry.h new file mode 100644 index 000000000..23c5839f5 --- /dev/null +++ b/dep/telemetry/ProjectTelemetry.h @@ -0,0 +1,17 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ProjectTelemetry.h + +Abstract: +- This module is used for basic definitions for telemetry for the entire project +--*/ + +#define TraceLoggingOptionMicrosoftTelemetry() TraceLoggingOptionGroup(0x9aa7a361, 0x583f, 0x4c09, 0xb1, 0xf1, 0xce, 0xa1, 0xef, 0x58, 0x63, 0xb0) +#define TelemetryPrivacyDataTag(tag) TraceLoggingUInt64((tag), "PartA_PrivTags") +#define PDT_ProductAndServicePerformance 0x0u +#define PDT_ProductAndServiceUsage 0x0u +#define MICROSOFT_KEYWORD_TELEMETRY 0x0 +#define MICROSOFT_KEYWORD_MEASURES 0x0 \ No newline at end of file diff --git a/dep/wil b/dep/wil new file mode 160000 index 000000000..fbcd1d2ab --- /dev/null +++ b/dep/wil @@ -0,0 +1 @@ +Subproject commit fbcd1d2abb558da4564ce343b688f7a658f51318 diff --git a/dirs b/dirs new file mode 100644 index 000000000..1af18876a --- /dev/null +++ b/dirs @@ -0,0 +1,3 @@ +DIRS=\ + src + diff --git a/doc/AddASetting.md b/doc/AddASetting.md new file mode 100644 index 000000000..64d93399c --- /dev/null +++ b/doc/AddASetting.md @@ -0,0 +1,40 @@ +# Adding a Settings Property + +1. Add to wincon.w + * THIS IS NOT IN OPENCONSOLE. Make sure you update + `.../console/published/wincon.w` in the OS repo when you submit the PR. + The branch won't build without it. + * For now, you can update winconp.h with your consumable changes. + * define registry name (ex `CONSOLE_REGISTRY_CURSORCOLOR`) + * add the setting to `CONSOLE_STATE_INFO` + * define the property key ID and the property key itself + - Yes, the large majority of the `DEFINE_PROPERTYKEY` defs are the same, it's only the last byte of the guid that changes + +2. Add matching fields to Settings.hpp + - add getters, setters, the whole drill. + +3. Add to the propsheet. + - We need to add it to *reading and writing* the registry from the propsheet, and *reading* the link from the propsheet. Yes, that's weird, but the propsheet is smart enough to re-use ShortcutSerialization::s_SetLinkValues, but not smart enough to do the same with RegistrySerialization. + - `src/propsheet/registry.cpp` + - `propsheet/registry.cpp@InitRegistryValues` should initialize the default value for the property. + - `propsheet/registry.cpp@GetRegistryValues` should make sure to read the property from the registry + +4. Add the field to the propslib registry map + +5. Add the value to `ShortcutSerialization.cpp` + - Read the value in `ShortcutSerialization::s_PopulateV2Properties` + - Write the value in `ShortcutSerialization::s_SetLinkValues` + +6. Add the setting to `Menu::s_GetConsoleState`, and `Menu::s_PropertiesUpdate` +Now, your new setting should be stored just like all the other properties. + +7. Update the feature test properties to get add the setting as well. + - `ft_uia/Common/NativeMethods.cs@WinConP`: + - `Wtypes.PROPERTYKEY PKEY_Console_` + - `NT_CONSOLE_PROPS` + +8. Add the default value for the setting to `win32k-settings.man` + - If the setting shouldn't default to 0 or `nullptr`, then you'll need to set the default value of the setting in `win32k-settings.man`. + +9. Update `Settings::InitFromStateInfo` and `Settings::CreateConsoleStateInfo` to get/set the value in a CONSOLE_STATE_INFO appropriately. + diff --git a/doc/ConsoleCtrlEvent.md b/doc/ConsoleCtrlEvent.md new file mode 100644 index 000000000..14dd95e91 --- /dev/null +++ b/doc/ConsoleCtrlEvent.md @@ -0,0 +1,22 @@ +# Console Control Events + +## Generation + +conhost requests that user32 inject a thread into the attached console application. +See ntuser's exitwin.c for `CreateCtrlThread`. + +## Timeouts + +_Sourced from ntuser's **exitwin.c**, **user.h**_ + +| Event | Circumstances | Timeout | +|------------------------|---------------------------------|-------------------------------------------------------------| +| `CTRL_CLOSE_EVENT` | _any_ | system parameter `SPI_GETHUNGAPPTIMEOUT`, 5000ms | +| `CTRL_LOGOFF_EVENT` | `CONSOLE_QUICK_RESOLVE_FLAG`[1] | registry key `CriticalAppShutdownTimeout` or 500ms | +| `CTRL_LOGOFF_EVENT` | _none of the above_ | system parameter `SPI_GETWAITTOKILLTIMEOUT`, 5000ms | +| `CTRL_SHUTDOWN_EVENT` | **service process** | system parameter `SPI_GETWAITTOKILLSERVICETIMEOUT`, 20000ms | +| `CTRL_SHUTDOWN_EVENT` | `CONSOLE_QUICK_RESOLVE_FLAG`[1] | registry key `CriticalAppShutdownTimeout` or 500ms | +| `CTRL_SHUTDOWN_EVENT` | _none of the above_ | system parameter `SPI_GETWAITTOKILLTIMEOUT`, 5000ms | +| `CTRL_C`, `CTRL_BREAK` | _any_ | **no timeout** | + +_[1]: nobody sets `CONSOLE_QUICK_RESOLVE_FLAG`._ diff --git a/doc/ConsoleHostSettings.md b/doc/ConsoleHostSettings.md new file mode 100644 index 000000000..31b056e7e --- /dev/null +++ b/doc/ConsoleHostSettings.md @@ -0,0 +1,70 @@ +# Understanding Console Host Settings + +Settings in the Windows Console Host can be a bit tricky to understand. This is mostly because the settings system evolved over the course of decades. Before we dig into the details of how settings are persisted, it's probably worth a quick look at what these settings are. + +## Settings Description + +|Name |Type |Description | +|---------------------------|-----------------------|--------------------------------------| +|`FontSize` |Coordinate (REG_DWORD) |Size of font in pixels | +|`FontFamily` |REG_DWORD |GDI Font family | +|`ScreenBufferSize` |Coordinate (REG_DWORD) |Size of the screen buffer in WxH characters | +|`CursorSize` |REG_DWORD |Cursor height as percentage of a single character | +|`WindowSize` |Coordinate (REG_DWORD) |Initial size of the window in WxH characters | +|`WindowPosition` |Coordinate (REG_DWORD) |Initial position of the window in WxH pixels (if not set, use auto-positioning) | +|`WindowAlpha` |REG_DWORD |Opacity of the window (valid range: 0x4D-0xFF) | +|`ScreenColors` |REG_DWORD |Default foreground and background colors | +|`PopupColors` |REG_DWORD |FG and BG colors used when displaying a popup window (e.g. when F2 is pressed in CMD.exe) | +|`QuickEdit` |REG_DWORD |Whether QuickEdit is on by default or not | +|`FaceName` |REG_SZ |Name of font to use (or "__DefaultTTFont__", which defaults to whichever font is deemed most appropriate for your codepage) | +|`FontWeight` |REG_DWORD |GDI font weight | +|`InsertMode` |REG_DWORD |Whether Insert mode is on by default or not | +|`HistoryBufferSize` |REG_DWORD |Number of history entries to retain | +|`NumberOfHistoryBuffers` |REG_DWORD |Number of history buffers to retain | +|`HistoryNoDup` |REG_DWORD |Whether to retain duplicate history entries or not | +|`ColorTable%%` |REG_DWORD |For each of the 16 colors in the palette, the RGB value of the color to use | +|`ExtendedEditKey` |REG_DWORD |Whether to allow the use of extended edit keys or not | +|`WordDelimiters` |REG_SZ |A list of characters that are considered as delimiting words (e.g. `' .-/\=|,()[]{}'`) | +|`TrimLeadingZeros` |REG_DWORD |Whether to remove zeroes from the beginning of a selected string on copy (e.g. `00000001` becomes `1`) | +|`EnableColorSelection` |REG_DWORD |Whether to allow selection colorization or not | +|`ScrollScale` |REG_DWORD |How many lines to scroll when using `SHIFT|Scroll Wheel` | +|`CodePage` |REG_DWORD |The default codepage to use | +|`ForceV2` |REG_DWORD |Whether to use the improved version of the Windows Console Host | +|`LineSelection`* |REG_DWORD |Whether to use wrapped text selection | +|`FilterOnPaste`* |REG_DWORD |Whether to replace characters on paste (e.g. Word "smart quotes" are replaced with regular quotes) | +|`LineWrap`* |REG_DWORD |Whether to have the Windows Console Host break long lines into multiple rows | +|`CtrlKeyShortcutsDisabled`*|REG_DWORD |Disables new control key shortcuts | +|`AllowAltF4Close`* |REG_DWORD |Allows the user to disable the Alt-F4 hotkey | +|`VirtualTerminalLevel`* |REG_DWORD |The level of VT support provided by the Windows Console Host | + +*: Only applies to the improved version of the Windows Console Host + +## The Settings Hierarchy + +Settings are persisted to a variety of locations depending on how they are modified and how the Windows Console Host was invoked: +* Hardcoded settings in conhostv2.dll +* User's configured defaults (stored as values in `HKCU\Console`) +* Per-console-application storage (stored as subkeys of `HKCU\Console`). Subkey names: + * Console application path (with `\` replaced with `_`) + * Console title +* Windows shortcut (.lnk) files + +To modify the defaults, invoke the `Defaults` titlebar menu option on a Windows Console Host window. Any changes made in the resulting dialog will be persisted to the registry location mentioned above. + +To modify settings specific to the current application, invoke the `Properties` titlebar menu option on a Windows Console Host window. If the application was launched directly (e.g. via the Windows run dialog), changes made in the dialog will be persisted in the per-application storage location mentioned above. If the application was launched via a Windows shortcut file, changes made in the settings dialog will be persisted directly into the .lnk file. For console applications with a shortcut, you can also right-click on the shortcut file and choose `Properties` to access the settings dialog. + +When console applications are launched, the Windows Console Host determines which settings to use by overlaying settings from the above locations. + +1. Initialize settings based on hardcoded defaults +2. Overlay settings specified by the user's configured defaults +3. Overlay application-specific settings from either the registry or the shortcut file, depending on how the application was launched + +Note that the registry settings are "sparse" settings repositories, meaning that if a setting isn't present, then whatever value that is already in use remains unchanged. This allows users to have some settings shared amongst all console applications and other settings be specific. Shortcut files, however, store each setting regardless of whether it was a default setting or not. + +## Known Issues + +* Modifications to system-created Start Menu and Win-X menu console applications are not kept during upgrade. + +## Adding settings + +Adding a setting involves a bunch of steps - see [AddASetting.md](AddASetting.md). diff --git a/doc/Debugging.md b/doc/Debugging.md new file mode 100644 index 000000000..e4f84613e --- /dev/null +++ b/doc/Debugging.md @@ -0,0 +1,10 @@ +# Debugging Miscellanea + +This file contains notes about debugging various items in the repository. + +## Setting breakpoints in Visual Studio for Cascadia (packaged) application + +If you want to debug code in the Cascadia package via Visual Studio, your breakpoints will not be hit by default. A tweak is required to the *CascadiaPackage* project in order to enable this. + +1. Right-click on *CascadiaPackage* in Solution Explorer and select Properties +2. Change the *Application process* type from *Mixed (Managed and Native)* to *Native Only*. \ No newline at end of file diff --git a/doc/EXCEPTIONS.md b/doc/EXCEPTIONS.md new file mode 100644 index 000000000..0161451f2 --- /dev/null +++ b/doc/EXCEPTIONS.md @@ -0,0 +1,36 @@ +# Using Exceptions + +## Philosophy +Introducing exceptions to an existing non-exception-based codebase can be perilous. The console was originally written +in C at a time when C++ was relatively unused in the Windows operating system. As part of our project to modernize the +Windows console, we converted to use C++, but still had an aversion to using exception-based error handling in +our code for fear that it introduce unexpected failures. However, the STL and other libraries like it are so useful that +sometimes it's significantly simpler to use them. Given that, we have a set of rules that we follow when considering +exception use. + +## Rules +1. **DO NOT** allow exceptions to leak out of new code into new code +1. **DO** use NTSTATUS or HRESULT as return values as appropriate +1. **DO** Encapsulate all exception behaviors within implementing classes +1. **DO NOT** introduce modern exception throwing code into old code. Instead, refactor as needed to allow encapsulation or + use non-exception based code +1. **DO** use WIL as an alternative for non-throwing modern facilities (e.g. wil::unique_ptr<>) + +## Examples + +### Encapsulating exception behaviors in a class + + class ExceptionsDoNotLeak + { + public: + HRESULT SomePublicFunction(); + int iPublic; + + private: + void _SomePrivateFunction(); + int _iPrivate; + }; + +### Using WIL for non-throwing modern facilities + +### \ No newline at end of file diff --git a/doc/ORGANIZATION.md b/doc/ORGANIZATION.md new file mode 100644 index 000000000..f7780d6d0 --- /dev/null +++ b/doc/ORGANIZATION.md @@ -0,0 +1,123 @@ +# Code Organization + +## Rules + +- **Follow the pattern of what you already see in the code** +- Try to package new ideas/components into libraries that have nicely defined interfaces +- Package new ideas into classes or refactor existing ideas into a class as you extend. +- Each project should have a Unit test in a ut_ folder in its subdirectory (like `ut_host`) +- Functional tests should be in ft_ subdirectories (like `ft_api`) +- Build scripts are generally in subdirectories with their type of output (like `/dll` or `/exe`) +- Try to place interfaces in an `inc` folder in an appropriate location +- Structure related libraries together (`/terminal/parser` and `/terminal/adapter`) + +## Code Overview +* `/` - root is where solution files, root MD documentation, SD replication artifacts go. +* `/bin` – not checked in is where binaries will be generated by the MSBuild system +* `/dep` – dependencies that aren’t a part of the SDK + * `/dep/console` – files that are currently in the internal-only private SDK for the console but we’re working on opening + * `/dep/DDK` – files lifted wholesale from the public Microsoft DDK so you don’t have to install that. We’re reducing dependencies on these but we still use TAEF (included in here) as our test runner engine + * `/dep/NT` – some more structures from the DDK, sometimes internal-only/non-public we’re trying to remove. + * `/dep/telemetry` – Private Microsoft telemetry headers + * `/dep/wil` – Windows Internal Library – extremely useful for interfacing with Win32/NT/COM APIs. Contains tons of “unique pointer” like syntax for various Win32 APIs and a handful of useful macros to enable cleaner code writing (RETURN_HR_IF, LOG_IF_WIN32_ERROR, etc.) + * `/dep/win32k` – private headers from the Windows windowing system that we’re trying to migrate off of +* `/ipch` – not checked in is where intellisense data will be generated if you use Visual Studio 2015 +* `/obj` – not checked in is where objects will be generated by the MSBuild system +* `/src` – This is the fun one. In the root is common build system data. + * `/src/host` – The meat of the windows console host. This includes buffer, input, output, windowing, server management, clipboard, and most interactions with the console host window that aren’t stated anywhere else. We’re trying to pull things out that are reusable into other libraries, but it’s a work in progress + * `/src/host/lib` – Builds the reusable LIB copy of the host + * `/src/host/dll` – Packages LIB into conhostv2.dll to be put into the OS C:\windows\system32\ + * `/src/host/exe` – Packages LIB into OpenConsole.exe currently used for testing without replacing your system32 copy + * `/src/host/tools` – Random odds and ends that make testing/debugging/development a bit easier + * ...to be doc’d, each of what these are + * `/src/host/ut_host` – Contains complete unit test library for everything we’ve managed to get unit tests on. We’d like all new code to contribute appropriate unit tests in here + * `/src/host/ft_api` – Feature level tests for anything that changes the way we interact with the outside world via the API. Building these up as we work as well + * `/src/host/ft_cjk` – Double-wide/double-byte specific Chinese/Japanese/Korean language tests that previously had to be run in a different environment. To be merged into ft_api one day + * `/src/host/ft_resize` – Special test for resizing/reflowing the buffer window + * `/src/host/ft_uia` – Currently disabled (for not being very reliable) UI Automation tests that we are looking to re-enable and expand to do UI Automation coverage of various human interactions + * `/src/host/...` - The files I’ll list out below + * `/src/inc` – Include files that are shared between the host and some of the other libraries. This is only some of them. The include story is kind of a mess right now, but we’d like to clean it up at some point + * `/src/propslib` – Library shared between console host and the OS shell “right click a shortcut file and modify console properties” page to read/write user settings to and from the registry and embedded within shortcut LNK data + * `/src/renderer` – Refactored extraction of all activities related to rendering the text in the buffers onto the screen + * `/src/renderer/base` – Base interface layer providing non-engine-specific rendering things like choosing the data from the console buffer, deciding how to lay out or transform that data, then dispatching commands to a specific final display engine + * `/src/renderer/gdi` – The GDI implementation of rendering to the screen. Takes commands to “draw a line” or “fill the background” or “select a region” from the base and turns them into GDI calls to the screen. Extracted from original console host code. + * `/src/renderer/inc – Interface definitions for all renderer communication + * `/src/terminal` – Virtual terminal support for the console. This is the sequences that are found in-band with other text on STDIN/STDOUT that command the display to do things. This is the *nix way of controlling a console. + * `/src/terminal/parser` – This contains a state machine and sorting engine for feeding in individual characters from STDOUT or STDIN and decoding them into the appropriate verbs that should be performed + * `/src/terminal/adapter` – This converts the verbs from the interface into calls on the console API. It doesn’t actually call through the API (for performance reasons since it lives inside the same binary), but it tries to remain as close to an API call as possible. There are some private extensions to the API for behaviors that didn’t exist before this was written that we’ve not made public. We don’t know if we will yet or force people to use VT to get at them. + * `/src/tsf` – Text Services Foundation. This provides IME input services to the console. This was historically used for only Chinese, Japanese, and Korean IMEs specifically on OS installations with those as the primary language. It was in the summer of 2016 unrestricted to be able to be used on any OS installation with any IME (whether or not it will display correctly is a different story). It also was unrestricted to allow things like Pen and Touch input (which are routed via IME messages) to display properly inside the console from the TabTip window (the little popup that helps you insert pen/touch writing/keyboard candidates into an application) + +## Host File Overview + +* Generally related to handling input/output data and sometimes intertwined with the actual service calls + * `_output.cpp` + * `_stream.cpp` +* Handles copy/paste/etc. + * `clipboard.cpp` +* Handles the command prompt line as you see in CMD.exe (known as the processed input line… most other shells handle this themselves with raw input and don’t use ours. This is a legacy of bad architectural design, putting stuff in conhost not in CMD) + * `cmdline.cpp` +* Handles shunting IME data back and forth to the TSF library and to and from the various buffers + * `Conimeinfo.cpp` + * `Convarea.cpp` +* Contains the global state for the entire console application + * `consoleInformation.cpp` +* Stuff related to the low-level server communication over our protocol with the driver + * `Csrutil.cpp` + * `Srvinit.cpp` + * `Handle.cpp` +* Routines related to startup of the application + * `Srvinit.cpp` +* Routines related to the API calls (and the servicing thereof, muxes a bit with the server protocol) + * `Directio.cpp` + * `Getset.cpp` + * `Srvinit.cpp` +* Extra stuff strapped onto the buffer to enable CJK languages + * `Dbcs.cpp` +* Attempted class-ification of existing Cursor structures to take them out of Screen Info/Text Info + * `Cursor.cpp` +* Related to searching through the back buffer of the console (the output scroll buffer as defined in screeninfo/textinfo) + * `Find.cpp` +* Contains global state data + * `Globals.cpp` +* Attempted class-ification of existing Icon manipulation to take them out of ConsoleInformation/Settings + * `Icon.cpp` +* Contains all keyboard/mouse input handling, capture of keys, conversion of keys, and some manipulation of the input buffer + * `Input.cpp` + * `Inputkeyinfo.cpp` + * `Inputreadhandledata.cpp` +* Main entry point used ONLY by the OS to send a pre-configured driver handle to conhostv2.dll + * `Main.cpp` +* Assorted utilities and stuff + * `Misc.cpp` (left for us by previous eras of random console devs) + * `Util.cpp` (created in our era) +* Custom zeroing and non-throwing allocator + * `Newdelete.cpp` +* Related to inserting text into the TextInfo buffer + * `Output.cpp` + * `Stream.cpp` +* Connects to interfaces in the PropsLib to manipulate persistent settings state + * `Registry.cpp` +* Connects to our relatively recently extracted renderer LIB to give it data about console state and user prefs + * `renderData.cpp` + * `renderFontDefaults.cpp` +* Maintains most of the information about what we should present inside the window on the screen (sizes, dimensions, also holds a text buffer instance and a cursor instance and a selection instance) + * `screenInfo.cpp` +* Handles some aspects of scrolling with the mouse and keyboard + * `Scrolling.cpp` +* Handles the click-and-drag highlighting of text on the screen to select (or the keyboard-based Mark mode selection where you can enter the mode and select around). Often calls clipboard when done + * `Selection.cpp` + * `selectionInput.cpp` + * `selectionState.cpp` +* Handles all user preferences and state. Was extracted from consoleInformation and CI subclasses it still (because it was hard to break the association out) + * `Settings.cpp` +* Good ol’ Windows 10 telemetry pipeline & ETW events as debugging aids (they use the same channel with a different flag) + * `Telemetry.cpp` + * `Tracing.cpp` +* Private calls into the Windows Window Manager to perform privileged actions related to the console process (working to eliminate) or for High DPI stuff (also working to eliminate) + * `Userprivapi.cpp` + * `Windowdpiapi.cpp` +* New UTF8 state machine in progress to improve Bash (and other apps) support for UTF-8 in console + * `Utf8ToWideCharParser.cpp` +* Window resizing/layout/management/window messaging loops and all that other stuff that has us interact with Windows to create a visual display surface and control the user interaction entry point + * `Window.cpp` + * `Windowproc.cpp` diff --git a/doc/STYLE.md b/doc/STYLE.md new file mode 100644 index 000000000..5deb15d6b --- /dev/null +++ b/doc/STYLE.md @@ -0,0 +1,7 @@ +# Coding Style + +## Philosophy +1. If it's inserting something into the existing classes/functions, try to follow the existing style as closely as possible. +1. If it's brand new code or refactoring a complete class or area of the code, please follow as Modern C++ of a style as you can and reference the [C++ Core Guidelines](https://github.com/isocpp/CppCoreGuidelines) as much as you possibly can. +1. When working with any Win32 or NT API, please try to use the [Windows Internal Library](./WIL.md) smart pointers and result handlers. +1. The use of NTSTATUS as a result code is discouraged, HRESULT or exceptions are preferred. Functions should not return a status code if they would always return a successful status code. Any function that returns a status code should be marked `noexcept` and have the `nodiscard` attribute. diff --git a/doc/TAEF.md b/doc/TAEF.md new file mode 100644 index 000000000..65d06c2f8 --- /dev/null +++ b/doc/TAEF.md @@ -0,0 +1,20 @@ +### TAEF ### +TAEF, the Test Authoring and Execution Framework, is used extensively within the Windows organization to test the operating system code in a unified manner for system, driver, and application code. As the console is a Windows OS Component, we strive to continue using the same system such that tests can be ran in a unified manner both externally to Microsoft as well as inside the official OS Build/Test system. + +The [official documentation](https://msdn.microsoft.com/en-us/library/windows/hardware/hh439725\(v=vs.85\).aspx) for TAEF describes the basic architecture, usage, and functionality of the test system. It is similar to Visual Studio test, but a bit more comprehensive and flexible. + +For the purposes of the console project, you can run the tests using the *TE.exe* that matches the architecture for which the test was build (x86/x64) in the pattern + + te.exe Console.Unit.Tests.dll + +Replacing the binary name with any other test binary name that might need running. Wildcard patterns or multiple binaries can be specified and all found tests will be executed. + +Limiting the tests to be run is also useful with: + + te.exe Console.Unit.Tests.dll /name:*BufferTests* + +Any pattern of class/method names can be specified after the */name:* flag with wildcard patterns. + +For any further details on the functionality of the TAEF test runner, *TE.exe*, please see the documentation above or run the embedded help with + + te.exe /! diff --git a/doc/UniversalTest.md b/doc/UniversalTest.md new file mode 100644 index 000000000..04fa44d83 --- /dev/null +++ b/doc/UniversalTest.md @@ -0,0 +1,117 @@ +# Universal Testing for Console + +## Overview + +Universal Testing is the Microsoft framework for creating and deploying test packages onto just about any device through just about any process. We use it for packaging up all sorts of test resources and sending it into our automated test labs no matter what the source of the content or the engineering system involved. + +It involves several parts: +- TESTMD + - These define a package unit for deployment to the test device. This usually includes the test binaries and any dependent data that it will need to execute. + - There can also be a hierarchy where one package can depend on another such that packages can be re-used + +- TESTLIST + - This defines a batch of TESTMD packages that should be executed together. + +- TESTPASSES + - This defines a list of tests via a TESTLIST and a lab environment configuration on which the tests should be run + + These files can either include their child element as they're supposed to (TESTMDs included in TESTLISTs) or they can often include themselves to provide chain structuring (one TESTLIST can reference another TESTLIST). + +- TREX + - This is the legacy configuration system that performed the same job as TESTPASSES, but not in source files. + +## Configuration + + This is a record of the current setup (as of Mar-1-2019) of the console's universal tests. This series of steps was created in conjunction with converting the console's testing from the legacy TREX dispatching mode to the new TESTPASSES dispatching mode for the Source Is Truth initiative (define all testing metadata in source next to the code being tested, instead of in a separate database somewhere else). + +1. Have some TestMDs. + - \onecore\windows\core\console\open\src\host\ut_host\testmd.definition + SOURCES file + - Generates “Microsoft-Console-Host-UnitTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.Host.UnitTests.testmd + 1. Microsoft-Console-Host-UnitTests.cab + 1. Microsoft-Console-Host-UnitTests.man.dsm.xml + - \onecore\windows\core\console\open\src\host\ft_host\testmd.definition + SOURCES file + - Generates “Microsoft-Console-Host-FeatureTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.Host.FeatureTests.testmd + 1. Microsoft-Console-Host-FeatureTests.cab + 1. Microsoft-Console-Host-FeatureTests.man.dsm.xml + - \onecore\windows\core\console\open\src\buffer\out\ut_textbuffer\testmd.definition + SOURCES file + - Generates “Microsoft-Console-TextBuffer-UnitTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.TextBuffer.UnitTests.testmd + 1. Microsoft-Console-TextBuffer-UnitTests.cab + 1. Microsoft-Console-TextBuffer-UnitTests.man.dsm.xml + - \minkernel\console\client\ut_conpty\testmd.definition + SOURCES file + - Generates “Microsoft-Console-ConPty-UnitTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.ConPty.UnitTests.testmd + 1. Microsoft-Console-ConPty-UnitTests.cab + 1. Microsoft-Console-ConPty-UnitTests.man.dsm.xml + - \onecore\windows\core\console\open\src\terminal\parser\ut_parser\testmd.definition + SOURCES file + - Generates “Microsoft-Console-VirtualTerminal-Parser-UnitTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.VirtualTerminal.Parser.UnitTests.testmd + 1. Microsoft-Console-VirtualTerminal-Parser-UnitTests.cab + 1. Microsoft-Console-VirtualTerminal-Parser-UnitTests.man.dsm.xml + - \onecore\windows\core\console\open\src\terminal\adapter\ut_adapter\testmd.definition + SOURCES file + - Generates “Microsoft-Console-VirtualTerminal-Adapter-UnitTests” TESTMD and package + - Binplaces to prebuilt\test\\\ + 1. Microsoft.Console.VirtualTerminal.Adapter.UnitTests.testmd + 1. Microsoft-Console-VirtualTerminal-Adapter-UnitTests.cab + 1. Microsoft-Console-VirtualTerminal-Adapter-UnitTests.man.dsm.xml +1. Have some TESTLISTs that refer to the TESTMDs + - \onecore\windows\core\console\open\src\testlist\Microsoft.Console.Tests.testlist + SOURCES file + - Includes + 1. Microsoft.Console.Host.UnitTests.testmd + 1. Microsoft.Console.Host.FeatureTests.testmd + 1. Microsoft.Console.TextBuffer.UnitTests.testmd + 1. Microsoft.Console.Conpty.UnitTests.testmd + 1. Microsoft.Console.VirtualTerminal.Parser.UnitTests.testmd + 1. Microsoft.Console.VirtualTerminal.Adapter.UnitTests.testmd + - Binplaces to prebuilt\test\\\ + - Microsoft.Console.Tests.testlist + - \onecore\windows\core\console\open\src\testlist\Microsoft.Console.TestLab.Desktop.testlist + SOURCES file + - Includes + 1. Microsoft.Console.Tests.testlist + - Binplaces to prebuilt\test\\\ + - Microsoft.Console.TestLab.Desktop.testlist + - Is currently the subject of TREX IDs + 1. 153251 – TESTLIST based AMD64 Desktop VM testpass (to be offboarded when done) + 1. 153252 – TESTLIST based X86 Desktop VM testpass (to be offboarded when done) + - \onecore\windows\core\console\open\src\testlist\Microsoft.Console.TestLab.OneCoreUap.testlist + SOURCES file + - Includes + 1. Microsoft.Console.Tests.testlist + - Binplaces to prebuilt\test\\\ + - Microsoft.Console.TestLab.OneCoreUAP.testlist + - Is currently the subject of TREX IDs + 1. 153253 – TESTLIST based AMD64 OneCoreUAP VM testpass (to be offboarded when done) + 1. 153254 – TESTLIST based X86 OneCoreUAP VM testpass (to be offboarded when done) +1. Create some TESTPASSES + - For the existing OneCoreUAP ones… + - Create directory \onecoreuap\testpasses\local\console\ + - Create file console_onecoreuap.testpasses + 1. Create AMD64 pass + - Name it similarly to the existing name + - Use the environment $(TESTPASSES_ONECOREUAP)/standard_testenvs/OneCoreUAP-amd64-VM.testenv + - Connect to the testlist $(TESTLIST_SEARCH_PATHS)/Microsoft.Console.TestLab.OneCoreUAP.testlist + 1. Create X86 pass + - Name it similarly to the existing name + - Use the environment $(TESTPASSES_ONECOREUAP)/standard_testenvs/OneCoreUAP-x86-VM.testenv + - Connect to the testlist $(TESTLIST_SEARCH_PATHS)/Microsoft.Console.TestLab.OneCoreUAP.testlist + - For the Desktop ones… + - Create directory \pcshell\testpasses\local\console\ + - Create file console_desktop.testpasses + 1. Create AMD64 pass + - Name it similarly to the existing name + - Use the environment $(TESTPASSES_PCSHELL)/standard_testenvs/Enterprise-amd64-VM.testenv + - Connect to the testlist $(TESTLIST_SEARCH_PATHS)/Microsoft.Console.TestLab.Desktop.testlist + 1. Create X86 pass + - Name it similarly to the existing name + - Use the environment $(TESTPASSES_PCSHELL)/standard_testenvs/Enterprise-x86-VM.testenv + - Connect to the testlist $(TESTLIST_SEARCH_PATHS)/Microsoft.Console.TestLab.Desktop.testlist +1. Hook up the TESTPASSES into the official branch TESTPASSES file + - Open up \.branchconfig\official\rs_onecore_dep_acidev\official_build.testpasses + 1. Add TestpassReferences item targeting $(TESTPASSES_ONECOREUAP)/local/console/console_onecoreuap.testpasses + 1. Add TestpassReferences item targeting $(TESTPASSES_PCSHELL)/local/console/console_desktop.testpasses diff --git a/doc/WIL.md b/doc/WIL.md new file mode 100644 index 000000000..4a73fbb2a --- /dev/null +++ b/doc/WIL.md @@ -0,0 +1,27 @@ +# Windows Internal Library + +## Overview +Windows Internal Library, or WIL, is a header-only library created to help make working with the Windows API more predictable and (hopefully) bug free. + +A majority of functions are in either the `wil::` or `wistd::` namespace. `wistd::` is used for things that have an equivalent in STL's `std::` namespace but have some special functionality like being exception-free. Everything else is in `wil::` namespace. + +The primary usages of WIL in our code so far are... + +### Smart Pointers ### + +Inside [wil\resource.h](..\dep\wil\resource.h) are smart pointer like classes for many Windows OS resources like file handles, socket handles, process handles, and so on. They're of the form `wil::unique_handle` and call the appropriate/matching OS function (like `CloseHandle()` in this case) when they go out of scope. + +Another useful item is `wil::make_unique_nothrow()` which is analogous to `std::make_unique` (except without the exception which might help you integrate with existing exception-free code in the console.) This will return a `wistd::unique_ptr` (vs. a `std::unique_ptr`) which can be used in a similar manner. + +### Result Handling ### + +To manage the various types of result codes that come back from Windows APIs, the file [wil\result.h](..\dep\wil\result.h) provides a wealth of macros that can help. + +As an example, the method `DuplicateHandle()` returns a `BOOL` value that is `FALSE` under failure and would like you to `GetLastError()` from the operating system to find out what the actual result code is. In this circumstance, you could use the macro `RETURN_IF_WIN32_BOOL_FALSE` to wrap the call to `DuplicateHandle()` which would automatically handle this pattern for you and return the `HRESULT` equivalent on failure. + +This leads to nice patterns where you can set up all resources in a function as protected by `std::unique_ptr` or the various `wil::` smart pointers and smart handles then `RETURN_IF_*` on every call to a Windows API and be guaranteed that your resources will be cleaned up appropriately under any failure case. Do note that this generally requires you to return an `HRESULT` as your return code and use out pointer parameters for return data. There are exceptions to this... read the header for more details. + +The additional advantage to using this pattern is that failures at any point are logged to our global tracing/debugging channels to be viewed under the debugger output with the exact line number and function details for the error. + +Additionally, if you just want to make sure that a failure case is logged for debugging purposes, all of these macros have a `LOG_IF_*` equivalent that will simply log a failure and keep rolling. + diff --git a/doc/WindowsTestPasses.md b/doc/WindowsTestPasses.md new file mode 100644 index 000000000..eee4ec93f --- /dev/null +++ b/doc/WindowsTestPasses.md @@ -0,0 +1,42 @@ +# Windows Test Passes for Console + +## Overview + +Every night, we run a set of automated test passes in the Windows engineering system for the console host code. This process is orchestrated on our working branch, which at the time of this writing is `RS_ONECORE_DEP_ACIDEV` (and will soon switch back to `RS_ONECORE_DEP_ACIOSS` or something of that ilk). + +You can find the information about our nightly build, including these test passes, at the website [https://es.microsoft.com], choosing our branch, and then navigating to the `Execution Status` page. + +At the bottom of the page will be the test pass runs for our tests that night. You can find out more about how these are set up in the [UniversalTest.md] file. + +When a failure occurs in one of these passes, a bug will automatically be generated in the Azure DevOps project for the OS and assigned to our path. + +The next step would be investigating one of these failures... + +## Investigation + +A quick overview of investigation... normally you can just attempt to build and reproduce the failure locally with the `OpenConsole` project and it will happen the same way as it did on the nightly build in the lab. However, sometimes the failure will be exclusive to the lab or won't happen in the same way as it does on your local dev machine. At that point, you need to move into setting up the environment as it was during the testpass and figuring out what went wrong. + +You can try to do this all manually by pulling down a VM image from the release share for the nightly build, making a VM, deploying the test binaries and TAEF test runnner executables to the machine, installing the VS Remote Debugging or WinDBG tools on the VM, and then running the test and figuring out what's going wrong with the debuggers. + +Or you can use some of the Engineering Systems tools to make this easier. I'll detail how to do that below. + +Prerequisites: +- Visual Studio 2017 +- Install the TDP (Test Development Platform) plug-in (see: [https://osgwiki.com/wiki/Test_Development_Platform_(TDP)]). + +1. Open Visual Studio 2017 and use the TDP drop-down menu to open the `Device Manager` +1. In the pane that opens to the left, choose `Add` and then `Nebula VM Device`. Nebula is a cloud provider for VMs (like Azure but a more private instance for corporate work usage). +1. Name the machine and choose the build/branch/flavor/SKU from the drop downs at the bottom. It will find the VHD for you from the build shares. Hit `Add Device` to deploy to Nebula +1. Wait a few minutes. It took 5-10 for it to be deployed. +1. Right click the machine name in the `Device Manager` list and choose `Launch T-Shell`. You can also use `Connect via Console` to get a "remote desktop"-like session to the KVM port on the VM. +1. In T-shell, use `testd Microsoft.Console.TestLab.Desktop.testlist` or a command of that format with a different TESTLIST or TESTMD name from our project (see the [UniversalTest.md] documentation). The `testd` utility will automatically resolve the build/branch/flavor information, dig through the build shares for the matching TESTLIST/TESTMD metadata, and attempt to deploy all relevent packages and dependencies on the device. When it's successful, it will move onto running all the tests and giving you the results. On conclusion, the test results should pop up in the web browser or the `Hubble - Log Viewer` tool provided by the Engineering Systems team. + +If some of the above things do not work, go to [https://osgwiki.com] and type them into the Search bar. For instance, if T-Shell isn't found or working, you can find out where to get it or download it on `OSGWiki`. The same goes for the other commands besides `testd` to use in T-shell and more information on what `Hubble` or `Nebula` are. + +Presumably now you have a failure. Or a success. You can attempt to spelunk the logs in `Hubble` and you might come to a conclusion. Or you can move onto debugging directly. + +Now that you've relied on `testd` to get everything deployed and orchestrated and run once on the device, you can use `execd` to run things again or to run a smaller subset of things on the remote device through `T-Shell`. + +By default, in the `Universal Test` world, everything will be deployed onto the remote machine at `C:\data\test\bin`. In T-Shell, use `cdd C:\data\test\bin` to change to that directory and then `execd te.exe Microsoft.Console.Host.FeatureTests.dll /name:*TestReadFileEcho*` to run just one specific test. Of course you should substitute the file name and test name parameters as makes sense. And of course you can find out more about `cdd` and `execd` on the `T-shell` page of `OSGWiki`. + +Fortunately, running things through `T-shell` in this fashion is exactly the same way that the testlab orchestrates the tests. If you still don't get good data this way, you can use the `Connect via Console` mechanism way above to try to run things under `WinDBG` or the `Visual Studio Remote Debugger` manually on the machine to get them to repro or under the debugger more completely. diff --git a/doc/building.md b/doc/building.md new file mode 100644 index 000000000..53b69c9b7 --- /dev/null +++ b/doc/building.md @@ -0,0 +1,32 @@ + +# How to build Openconsole + +Openconsole can be built with Visual Studio or from the command line. There are build scripts for both cmd and powershell in /tools. + +## Building with cmd + +The cmd scripts are set up to emulate a portion of the OS razzle build environment. razzle.cmd is the first script that should be run. bcz.cmd will build clean and bz.cmd should build incrementally. + +There are also scripts for running the tests: +- runut.cmd - run the unit tests +- runft.cmd - run the feature tests +- runuia.cmd - run the UIA tests + +## Build with Powershell + +Openconsole.psm1 should be loaded with `Import-Module`. From there `Set-MsbuildDevEnvironment` will set up environment variables required to build. There are a few exported functions (look at their documentation for further details): + +- Invoke-OpenConsolebuild - builds the solution. Can be passed msbuild arguments. +- Invoke-OpenConsoleTests - runs the various tests. Will run the unit tests by default. +- Start-OpenConsole - starts Openconsole.exe from the output directory. x64 is run by default. +- Debug-OpenConsole - starts Openconsole.exe and attaches it to the default debugger. x64 is run by default. + +## Configuration Types + +Openconsole has three configuration types: + +- Debug +- Release +- AuditMode + +AuditMode is an experimental mode that enables some additional static analyis from CppCoreCheck. diff --git a/doc/cascadia/Keybindings-spec.md b/doc/cascadia/Keybindings-spec.md new file mode 100644 index 000000000..08da295d8 --- /dev/null +++ b/doc/cascadia/Keybindings-spec.md @@ -0,0 +1,108 @@ +# Keymapping spec + +* author: Mike Griese __migrie__ +* created on: 2018-Oct-23 + +## Abstract +It should be possible to configure the terminal so that it doesn't send certain keystrokes as input to the terminal, and instead triggers certain actions. Examples of these actions could be copy/pasting text, opening a new tab, or changing the font size. + +This spec describes a mechanism by which we could provide a common implementation of handling keyboard shortcuts like these. This common implementation could then be leveraged and extended by the UX implementation as to handle certain callbacks in the UX layer. For example, The TerminalCore doesn't have a concept of what a tab is, but the keymap abstraction could raise an event such that a WPF app could implement creating a new tab in it's idomatic way, and UWP could implement them in their own way. + +## Terminology +* **Key Chord**: This is any possible keystroke that a user can input + simultaneously, as a combination of a single character and any set of + (Ctrl, Alt and Shift). + For example, pressing Ctrl and C at the same time is the + key chord Ctrl+C. Pressing Ctrl+B, C are two separate + key chords. Trying to press them simultaneously (Ctrl+B+C) should + generate two separate key chords, with the order determined by the OS. + +## User Stories +1. The User should be able to press certain key-chords to trigger certain + actions in the frontend of the application, such as copying text, pasting, + opening new tabs, or switching focus between panes. +2. The user should be able to configure which key chords are bound to which actions. +3. If a key chord is not mapped to an action, it should be sent to the Terminal just as any other keypress. + +## Details + +When the UX frontend is created, it should instantiate a `IKeyBindings` object with the keybindings mapped as it would like. + +When it's creating it's platform-dependent terminal component, it can pass the `IKeyBindings` object to that component. The component will then be able to pass that object to the terminal instance. + +When the terminal component calls `ITerminalInput.SendKeyEvent(uint vkey, KeyModifiers modifiers)`, the terminal will use `IKeyBindings.TryKeyChord` to see if there are any bound actions to that input. If there are, the `IKeyBindings` implementation will either handle the event by interacting with the `ITerminalInput`, or it'll invoke an event that's been registered by the frontend + +```csharp +struct KeyChord +{ + KeyModifiers modifiers; + int vkey; +} + +interface IKeyBindings { + bool TryKeyChord(KeyChord kc); +} +``` + +Each frontend can implement the `IKeyBindings` interface however it so chooses. + +The `ITerminalInput` interface will be extended with the following method: +```csharp +public interface ITerminalInput +{ + ... + void SetKeyBindings(IKeyBindings bindings); + ... +} +``` +This will set the `IKeyBindings` object that the `ITerminalInput` implementation will use to filter key events. + +This method will be implemented by the `Terminal` object: +```csharp +partial class Terminal +{ + public void ITerminalInput.SetKeyBindings(IKeyBindings bindings); +} +``` +### Project Cascadia Sample + +Below is an example of how the Project Cascadia application might implement it's + keybindings. + +```csharp +enum ShortcutAction +{ + CopyText, + PasteText, + NewTab, + NewWindow, + CloseWindow, + CloseTab, + SwitchToTab, + NextTab, + PrevTab, + IncreaseFontSize, + DecreaseFontSize, + ... +} +public delegate bool NewTabEvent(object sender); +public delegate bool CloseTabEvent(object sender); +public delegate bool NewWindowEvent(object sender); +public delegate bool CloseWindowEvent(object sender); +public delegate bool CopyEvent(object sender); +public delegate bool PasteEvent(object sender); + +class KeyBindings : IKeyBindings +{ + private Dictionary keyShortcuts; + + public void SetKeyBinding(ShortcutAction action, KeyChord? chord); + public bool TryKeyChord(KeyChord chord); +} +``` + +### Copy/Paste +How does Copy/paste play into this? + +When Input is written to the terminal, and it tries the copy keybinding, what happens? +The Keybindings are global to the frontend, not local to the terminal. Copy/Paste events should also be delegates that get raised, and the frontend can then determine what to do with them. It'll probably query it's active/focused Terminal Component, then Get the `ITerminalInput` from that component, and use that to CopyText / PasteText from the Terminal as needed. diff --git a/doc/cascadia/TerminalSettings-spec.md b/doc/cascadia/TerminalSettings-spec.md new file mode 100644 index 000000000..a1f39f375 --- /dev/null +++ b/doc/cascadia/TerminalSettings-spec.md @@ -0,0 +1,233 @@ +# Terminal Settings + +* author: Mike Griese __migrie__ +* created on: 2018-Oct-23 + +## Abstract + +This spec will outline how various terminal frontends will be able to interact with the settings for the terminal. + +## Terminology +* **Frontend** or **Application Layer**: This is the end-user experience. This + could be a Terminal Application (ex. Project Cascadia) or something that's + embedding a terminal window inside of it (ex. Visual Studio). These frontends + consume the terminal component as an atomic unit. +* **Component Layer**: This is the UI framework-dependent implementation of the + Terminal component. As planned currently, this is either the UWP or WPF + Terminal component. +* **Terminal Layer**: This is the shared core implementation of the terminal. + This is the Terminal Connection, Parser/Adapter, Buffer, and Renderer (but not + the UX-dependant RenderEngine). + +## User Stories +1. "Project Cascadia" should be able to have both global settings (such as + scrollbar styling) and settings that are stored per-profile (such as + commandline, color scheme, etc.) +2. "Project Cascadia" should be able to load these settings at boot, use them to + create terminal instances, be able to edit them, and be able to save them + back. +3. "Project Cascadia" should be able to have terminal instances reflect the + changes to the settings when the settings are changed. +4. "Project Cascadia" should be able to host panes/tabs with different profiles + set at the same time. +5. Visual Studio should be able to persist and edit settings globally, without + the need for a globals/profiles structure. +6. The Terminal should be able to read information from a settings structure + that's independant of how it's persisted / implemented by the Application +7. The Component should be able to have it's own settings independent of the + application that's embedding it, such as font size and face, scrollbar + visibility, etc. These should be settings that are specific to the component, + and the Terminal should logically be unaffected by these settings. + +## Details + +Some settings will need to be Application-specific, some will need to be component-specific, and some are terminal-specific. For example: + +Terminal | Component | Frontend +--------------------|-----------------------|------------------ +Color Table | Font Face, size | Status Line Visibility, contents +Cursor Color | Scrollbar Visibility | ~~Window Size~~[1] +History Size | | +Buffer Size [1] | | + +* [1] I believe only the "Default" or "Initial" buffer size should be the one we truly store in the settings. When the app first boots up, it can use that value to with the font size to figure out how big its window should be. When additional tabs/panes are created, they should inherit the size of the existing window. Similarly, VS could first calculate how much space it has available, then override that value when creating the terminal. + +Project Cascadia needs to be able to persist settings as a bipartite globals-profiles structure. +VS needs to be able to persist settings just as a simple set of global settings. + +When the application needs to retrieve these settings, they need to use them as a tripartite structure: frontend-component-terminal settings. + +Each frontend will have it's own set of settings. +Each component implementation will also ned to have some settings that control it. +The terminal also will have some settings specific to the terminal. + +### Globals and Profiles +With \*nix-like terminals, settings are typically structured as two parts: + Globals and Profiles. + +Globals are settings that affect the entirety of the terminal application. They wouldn't be different from one pane to the next. An example is the Terminal KeyBindings - these should be the same for all tabs/panes that are running as a part of the terminal application. + +Profiles are what you might consider per-application settings. These are settings that can be different from one terminal instance to the next. One of the primary differentiators between profiles is the commandline used to start the terminal instance - this enables the user to have both a `cmd` profile and a `powershell` profile, for example. Things like the color table/scheme, font size, history length, these all change per-profile. + +Per-Profile | Globals +------------------------|------------------------ +Color Table | Keybindings +Cursor Color | Scrollbar Visibility +History Size | Status Line Visibility, contents +Font Face, size | Window Size +Shell Commandline | + +### Simple Settings + +An application like VS might not even care about settings profiles. They should be able to persist the settings as just a singular entity, and change those as needed, without the additional overhead. Profiles will be something that's more specifc to Project Cascadia. + +### Interface Descriptions + +```csharp +public class TerminalSettings +{ + Color DefaultForeground; + Color DefaultBackground; + Color[] ColorTable; + Coord? Dimensions; + int HistorySize; + Color CursorColor; + CursorShape CursorShape; +} + +public interface IComponentSettings +{ + TerminalSettings TerminalSettings { get; } +} + +public interface IApplicationSettings +{ + IComponentSettings ComponentSettings { get; } +} +``` + +The Application can store whatever settings it wants in it's implementation of `IApplicationSettings`. When it instantiates a Terminal Component, it will pass it's `IComponentSettings` to it. + +The component will retrieve whatever settings it wants from that object, and then pass the `TerminalSettings` to the Terminal it creates. + + +The frontend will be able to get/set it's settings from the `IApplicationSettings` implementation. +The frontend will be able to create components using the `IComponentSettings` in it's `IApplicationSettings`. +The Component will then create the Terminal using the `TerminalSettings`. + +#### Project Cascadia Settings Details + +The `CascadiaSettings` will store the settings as two parts: +* A set of global data & settings +* A list of Profiles, that each have more data + +When Cascadia starts up, it'll load all the settings, including the Globals and profiles. The Globals will also tell us which profile is the "Default" profile we should use to instantiate the terminal. +Using the globals and the Profile, it'll convert those to a `ApplicationSettings : IApplicationSettings`. +It'll read data from that `ApplicationSettings` to initialize things it needs to know. +* It'll determine whether or not to display the status line +* It'll query the Component settings for the default size of the component, so it knows how big of a space it needs to reserve for it + +It'll then instantiate a `UWPTerminalComponent` and pass it the `UWPComponentSettings`. + +This is a rough draft of what these members might all be like. +```csharp +class CascadiaSettings +{ + void LoadAll(); + void SaveAll(); + GlobalAppSettings Globals; + List Profiles; + ApplicationSettings ToSettings(GlobalAppSettings globals, Profile profile); + void Update(ApplicationSettings appSettings, GUID profileID); +} +class Profile +{ + GUID ProfileGuid; + string Name; + string Commandline; + TerminalSettings TerminalSettings; + string FontFace; + int FontSize; + float acrylicTransparency; + bool useAcrylic; +} +class GlobalAppSettings +{ + GUID defaultProfile; + Keybindings keybindings; + bool showScrollbars; + bool showStatusline; +} + +class ApplicationSettings : IApplicationSettings +{ + UWPComponentSettings ComponentSettings; + Keybindings keybindings; + bool showStatusline; +} +class UWPComponentSettings : IComponentSettings +{ + Point GetDefaultComponentSize(); + TerminalSettings TerminalSettings; + string FontFace; + int FontSize; + bool showScrollbars; + float acrylicTransparency; + bool useAcrylic; +} +``` + +### Updating Settings + +What happens when the user changes the application's settings? The App, + Component, and Terminal might all need to update their settings. +The component will expose a `UpdateSettings()` method that will cause the + Component and Terminal to reload the settings from their settings objects. + +```csharp +interface ITerminalComponent +{ + void UpdateSettings(IComponentSettings componentSettings); +} +partial class UWPTerminalComponent : ITerminalComponent +{ + void UpdateSettings(IComponentSettings componentSettings) + { + // Recalculate GlyphTypeFace + // Recalculate rows/cols using current geometry and typeface + // Update our terminal instance: + terminal.UpdateSettings(componentSettings.TerminalSettings); + } +} +``` +#### Updating settings in Project Cascadia + +However, when Cascadia's settings change, we're going to possibly change some global settings and possibly some profile settings. The profile's that are changed may or may not be currently active. + +> Say we have two different panes open with different profiles (A and B). +> What happens if we change the settings for one profile's font and not the other's? +> ~~We resize the height of the terminal to account for the change in height of the win~~ +> +> We should never change the window size in response to a settings change if there is more than one tab/pane open. +> > never? + +Cascadia would have to maintain a mapping of which components have which profiles: +```csharp +class CascadiaTerminalInstance +{ + GUID ProfileGuid; + UWPTerminalComponent component; +} +``` + +Then, when the settings are closed, it'll enumerate all of the components it has loaded, and apply the updated settings to them. It'll do this by looking up the profile GUID of the component, then getting the `ApplicationSettings` for the profile, then calling `UpdateSettings` on the component. + +~~We need to have a way so that only the currently foreground component can change the window size.~~ +I don't like that - if we change the font size, we should just recalculate how many characters can fit in the current window size. + +## Questions / TODO +* How does this interplay with setting properties of the terminal component in XAML? + * I would think that the component would load the XAML properties first, and if the controlling application calls `UpdateSettings` on the component, then those in-XAML properties would likely get overwritten. + * It's not necessary to create the component with a `IComponentSettings`, nor is it necessary to call `UpdateSettings`. If you wanted to create a trivial settings-less terminal component entriely in XAML, go right ahead. + * Any settings that *are* exposed through XAML properties *should* also be exposed in the component's settings implementation as well. + * Can that be enforced any way? I doubt it. diff --git a/doc/submitting_code.md b/doc/submitting_code.md new file mode 100644 index 000000000..24442bceb --- /dev/null +++ b/doc/submitting_code.md @@ -0,0 +1,26 @@ + +# Branches in Openconsole + +In Openconsole, `dev/main` is the master branch for the repo. + +Any branch that begins with `dev/` is recognized by our CI system and will automatically run x86 and amd64 builds and run our unit and feature tests. For feature branchs the pattern we use is `dev//`. ex. `dev/austdi/SomeCoolUnicodeFeature`. The important parts are the dev prefix and your alias. + +`inbox` is a special branch that coordinates Openconsole code to the main OS repo. + +The code will be checked into the OS repo at `/onecore/windows/core/console/open`. It would be prudent to make sure that directory builds in razzle with your submitted changes. + +# Code Submission Process + +Because we build outside of the OS repo, we need a way to get code back into it once it's been merged into `dev/main`. This is done by cherry-picking the PR to the `inbox` branch once it has been merged (and preferably squashed) into `dev/main`. We have a tool called Git2Git that listens for new merges into `inbox` and replicates the commits over to the OS repo. Feel free to approve and complete the `inbox` PR yourself. About a minute after the `inbox` PR is submitted, Git2Git will create a PR in the OS repo under the alias `miniksa`. It will automatically target the OS branch we're using at the time, it just needs you to go approve and complete it. Once that merge is completed it is a good idea to build the OS branch with the new code in it to make sure that the PR won't be the cause of a build break that evening. + +## What to do when cherry-picking to inbox fails + +Sometimes VSTS doesn't want to allow a cherry pick to the inbox branch. It might have a valid reason or it might just be finicky. You'll need to complete the merge manually on a local machine. The steps are: + +1. make sure you have pulled the latest commits for the `dev/main` and `inbox` branches +2. make a new branch from inbox +3. cherry-pick the commits from the PR to the newly created branch (this is easier if you squashed your commits when you merged into `dev/main` +4. fix any merge conficts and commit +5. push the new branch to the remote +6. create a new PR of that branch in `inbox` +7. complete PR and continue on to completing the auto-created PR in the OS repo diff --git a/doc/virtual-dtors.md b/doc/virtual-dtors.md new file mode 100644 index 000000000..1cb70cc1f --- /dev/null +++ b/doc/virtual-dtors.md @@ -0,0 +1,35 @@ +# Virtual Destructors for Interfaces + + +* author: Mike Griese __migrie__ +* created on: 2019-Feb-20 + +As you look through the code, you may come across patterns that look like the following: + +``` c++ + + class IRenderData + { + public: + virtual ~IRenderData() = 0; + // methods + }; + + inline IRenderData::~IRenderData() {} + +``` + +You may ask yourself, why is the destructor deleted, then later defined to the + default destructor? This is a good question, because it seems both unintuitive + and unnecessary. However, if you don't define your interfaces exactly like + this, then sometimes on object destruction, the interface's dtor will be + called instead of the destructor for the base class. There is other + strangeness that can occur as well, the details of which escape my memory from + when @austdi and I first investigaved this early 2018. + +The end result of not defining your interfaces exacly like this will be that + occasionally, when destructing objects, you'll get a segfault. + +To check that this behavior works, I direct your attention to the VtIoTests. + There are a bunch of tests in that module that create objects, then delete + them, to make sure that they won't ever crash. diff --git a/pkg/appx/OpenConsolePackage.wapproj b/pkg/appx/OpenConsolePackage.wapproj new file mode 100644 index 000000000..a2fbde89a --- /dev/null +++ b/pkg/appx/OpenConsolePackage.wapproj @@ -0,0 +1,86 @@ + + + + + + + 10.0.17763.0 + 10.0.17134.0 + + false + false + + + false + + + true + + + + 2D310963-F3E0-4EE5-8AC6-FBC94DCC3310 + + OpenConsole.exe + ..\..\src\host\exe\Host.EXE.vcxproj + + + + False + + + + false + Never + + + + true + False + OpenConsolePackage_TemporaryKey.pfx + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + <_TemporaryFilteredWapProjOutput Include="@(_FilteredNonWapProjProjectOutput)" /> + <_FilteredNonWapProjProjectOutput Remove="@(_FilteredNonWapProjProjectOutput)" /> + <_FilteredNonWapProjProjectOutput Include="@(_TemporaryFilteredWapProjOutput)"> + + + + + diff --git a/pkg/appx/Package.appxmanifest b/pkg/appx/Package.appxmanifest new file mode 100644 index 000000000..b738e97f0 --- /dev/null +++ b/pkg/appx/Package.appxmanifest @@ -0,0 +1,34 @@ + + + + + Windows Console (Preview) + Microsoft Corporation + images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/appx/images/LockScreenLogo.scale-200.png b/pkg/appx/images/LockScreenLogo.scale-200.png new file mode 100644 index 000000000..4796bcb9d Binary files /dev/null and b/pkg/appx/images/LockScreenLogo.scale-200.png differ diff --git a/pkg/appx/images/Square150x150Logo.scale-200.png b/pkg/appx/images/Square150x150Logo.scale-200.png new file mode 100644 index 000000000..3d951a40b Binary files /dev/null and b/pkg/appx/images/Square150x150Logo.scale-200.png differ diff --git a/pkg/appx/images/Square44x44Logo.png b/pkg/appx/images/Square44x44Logo.png new file mode 100644 index 000000000..5e907dddc Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.png differ diff --git a/pkg/appx/images/Square44x44Logo.scale-200.png b/pkg/appx/images/Square44x44Logo.scale-200.png new file mode 100644 index 000000000..2bad534c7 Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.scale-200.png differ diff --git a/pkg/appx/images/Square44x44Logo.targetsize-16_altform-unplated.png b/pkg/appx/images/Square44x44Logo.targetsize-16_altform-unplated.png new file mode 100644 index 000000000..e997ab77d Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.targetsize-16_altform-unplated.png differ diff --git a/pkg/appx/images/Square44x44Logo.targetsize-24_altform-unplated.png b/pkg/appx/images/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 000000000..bcb2132e4 Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/pkg/appx/images/Square44x44Logo.targetsize-256_altform-unplated.png b/pkg/appx/images/Square44x44Logo.targetsize-256_altform-unplated.png new file mode 100644 index 000000000..e640a18ad Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/pkg/appx/images/Square44x44Logo.targetsize-32_altform-unplated.png b/pkg/appx/images/Square44x44Logo.targetsize-32_altform-unplated.png new file mode 100644 index 000000000..a386eb03e Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.targetsize-32_altform-unplated.png differ diff --git a/pkg/appx/images/Square44x44Logo.targetsize-48_altform-unplated.png b/pkg/appx/images/Square44x44Logo.targetsize-48_altform-unplated.png new file mode 100644 index 000000000..f0cab939d Binary files /dev/null and b/pkg/appx/images/Square44x44Logo.targetsize-48_altform-unplated.png differ diff --git a/pkg/appx/images/StoreLogo.png b/pkg/appx/images/StoreLogo.png new file mode 100644 index 000000000..3e0e05677 Binary files /dev/null and b/pkg/appx/images/StoreLogo.png differ diff --git a/pkg/appx/images/Wide310x150Logo.scale-200.png b/pkg/appx/images/Wide310x150Logo.scale-200.png new file mode 100644 index 000000000..0a51cc975 Binary files /dev/null and b/pkg/appx/images/Wide310x150Logo.scale-200.png differ diff --git a/res/console.ico b/res/console.ico new file mode 100644 index 000000000..fc756afc1 Binary files /dev/null and b/res/console.ico differ diff --git a/res/truetype.bmp b/res/truetype.bmp new file mode 100644 index 000000000..7a4b74406 Binary files /dev/null and b/res/truetype.bmp differ diff --git a/src/ConsolePerf.regions.xml b/src/ConsolePerf.regions.xml new file mode 100644 index 000000000..f08b86548 --- /dev/null +++ b/src/ConsolePerf.regions.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + CPU + te.processhost.exe + + + + + + + + + + + + + + + + + + + + + + + CPU + + conhost.exe;openconsole.exe + + + + + Commit + conhost.exe;openconsole.exe + + + + + + diff --git a/src/ConsolePerf.wprp b/src/ConsolePerf.wprp new file mode 100644 index 000000000..1610514ef --- /dev/null +++ b/src/ConsolePerf.wprp @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/StaticAnalysis.ruleset b/src/StaticAnalysis.ruleset new file mode 100644 index 000000000..1eea1e89f --- /dev/null +++ b/src/StaticAnalysis.ruleset @@ -0,0 +1,4 @@ + + + + diff --git a/src/buffer/dirs b/src/buffer/dirs new file mode 100644 index 000000000..72492a461 --- /dev/null +++ b/src/buffer/dirs @@ -0,0 +1,3 @@ +DIRS=out \ + + diff --git a/src/buffer/out/AttrRow.cpp b/src/buffer/out/AttrRow.cpp new file mode 100644 index 000000000..e25816cf8 --- /dev/null +++ b/src/buffer/out/AttrRow.cpp @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "AttrRow.hpp" + + // Routine Description: + // - constructor + // Arguments: + // - cchRowWidth - the length of the default text attribute + // - attr - the default text attribute + // Return Value: + // - constructed object + // Note: will throw exception if unable to allocate memory for text attribute storage +ATTR_ROW::ATTR_ROW(const UINT cchRowWidth, const TextAttribute attr) +{ + _list.push_back(TextAttributeRun(cchRowWidth, attr)); + _cchRowWidth = cchRowWidth; +} + +// Routine Description: +// - Sets all properties of the ATTR_ROW to default values +// Arguments: +// - attr - The default text attributes to use on text in this row. +void ATTR_ROW::Reset(const TextAttribute attr) +{ + _list.clear(); + _list.push_back(TextAttributeRun(_cchRowWidth, attr)); +} + +// Routine Description: +// - Takes an existing row of attributes, and changes the length so that it fills the NewWidth. +// If the new size is bigger, then the last attr is extended to fill the NewWidth. +// If the new size is smaller, the runs are cut off to fit. +// Arguments: +// - oldWidth - The original width of the row. +// - newWidth - The new width of the row. +// Return Value: +// - , throws exceptions on failures. +void ATTR_ROW::Resize(const size_t newWidth) +{ + THROW_HR_IF(E_INVALIDARG, 0 == newWidth); + + // Easy case. If the new row is longer, increase the length of the last run by how much new space there is. + if (newWidth > _cchRowWidth) + { + // Get the attribute that covers the final column of old width. + const auto runPos = FindAttrIndex(_cchRowWidth - 1, nullptr); + auto& run = _list[runPos]; + + // Extend its length by the additional columns we're adding. + run.SetLength(run.GetLength() + newWidth - _cchRowWidth); + + // Store that the new total width we represent is the new width. + _cchRowWidth = newWidth; + } + // harder case: new row is shorter. + else + { + // Get the attribute that covers the final column of the new width + size_t CountOfAttr = 0; + const auto runPos = FindAttrIndex(newWidth - 1, &CountOfAttr); + auto& run = _list[runPos]; + + // CountOfAttr was given to us as "how many columns left from this point forward are covered by the returned run" + // So if the original run was B5 covering a 5 size OldWidth and we have a NewWidth of 3 + // then when we called FindAttrIndex, it returned the B5 as the pIndexedRun and a 2 for how many more segments it covers + // after and including the 3rd column. + // B5-2 = B3, which is what we desire to cover the new 3 size buffer. + run.SetLength(run.GetLength() - CountOfAttr + 1); + + // Store that the new total width we represent is the new width. + _cchRowWidth = newWidth; + + // Erase segments after the one we just updated. + _list.erase(_list.cbegin() + runPos + 1, _list.cend()); + + // NOTE: Under some circumstances here, we have leftover run segments in memory or blank run segments + // in memory. We're not going to waste time redimensioning the array in the heap. We're just noting that the useful + // portions of it have changed. + } +} + +// Routine Description: +// - returns a copy of the TextAttribute at the specified column +// Arguments: +// - column - the column to get the attribute for +// Return Value: +// - the text attribute at column +// Note: +// - will throw on error +TextAttribute ATTR_ROW::GetAttrByColumn(const size_t column) const +{ + return GetAttrByColumn(column, nullptr); +} + +// Routine Description: +// - returns a copy of the TextAttribute at the specified column +// Arguments: +// - column - the column to get the attribute for +// - pApplies - if given, fills how long this attribute will apply for +// Return Value: +// - the text attribute at column +// Note: +// - will throw on error +TextAttribute ATTR_ROW::GetAttrByColumn(const size_t column, + size_t* const pApplies) const +{ + THROW_HR_IF(E_INVALIDARG, column >= _cchRowWidth); + const auto runPos = FindAttrIndex(column, pApplies); + return _list[runPos].GetAttributes(); +} + +// Routine Description: +// - reports how many runs we have stored (to be used for some optimizations +// Return Value: +// - Count of runs. 1 means we have 1 color to represent the entire row. +size_t ATTR_ROW::GetNumberOfRuns() const noexcept +{ + return _list.size(); +} + +// Routine Description: +// - This routine finds the nth attribute in this ATTR_ROW. +// Arguments: +// - index - which attribute to find +// - applies - on output, contains corrected length of indexed attr. +// for example, if the attribute string was { 5, BLUE } and the requested +// index was 3, CountOfAttr would be 2. +// Return Value: +// - const reference to attribute run object +size_t ATTR_ROW::FindAttrIndex(const size_t index, size_t* const pApplies) const +{ + FAIL_FAST_IF(!(index < _cchRowWidth)); // The requested index cannot be longer than the total length described by this set of Attrs. + + size_t cTotalLength = 0; + + FAIL_FAST_IF(!(_list.size() > 0)); // There should be a non-zero and positive number of items in the array. + + // Scan through the internal array from position 0 adding up the lengths that each attribute applies to + auto runPos = _list.cbegin(); + do + { + cTotalLength += runPos->GetLength(); + + if (cTotalLength > index) + { + // If we've just passed up the requested index with the length we added, break early + break; + } + + runPos++; + } while (runPos < _list.cend()); + + // we should have broken before falling out the while case. + // if we didn't break, then this ATTR_ROW wasn't filled with enough attributes for the entire row of characters + FAIL_FAST_IF(runPos >= _list.cend()); + + // The remaining iterator position is the position of the attribute that is applicable at the position requested (index) + // Calculate its remaining applicability if requested + + // The length on which the found attribute applies is the total length seen so far minus the index we were searching for. + FAIL_FAST_IF(!(cTotalLength > index)); // The length of all attributes we counted up so far should be longer than the index requested or we'll underflow. + + if (nullptr != pApplies) + { + const auto attrApplies = cTotalLength - index; + FAIL_FAST_IF(!(attrApplies > 0)); // An attribute applies for >0 characters + // MSFT: 17130145 - will restore this and add a better assert to catch the real issue. + //FAIL_FAST_IF(!(attrApplies <= _cchRowWidth)); // An attribute applies for a maximum of the total length available to us + + *pApplies = attrApplies; + } + + return runPos - _list.cbegin(); +} + +// Routine Description: +// - Sets the attributes (colors) of all character positions from the given position through the end of the row. +// Arguments: +// - iStart - Starting index position within the row +// - attr - Attribute (color) to fill remaining characters with +// Return Value: +// - +bool ATTR_ROW::SetAttrToEnd(const UINT iStart, const TextAttribute attr) +{ + size_t const length = _cchRowWidth - iStart; + + const TextAttributeRun run(length, attr); + return SUCCEEDED(InsertAttrRuns({ &run, 1 }, iStart, _cchRowWidth - 1, _cchRowWidth)); +} + +// Routine Description: +// - Replaces all runs in the row with the given wToBeReplacedAttr with the new +// attribute wReplaceWith. This method is used for replacing specifically +// legacy attributes. +// Arguments: +// - wToBeReplacedAttr - the legacy attribute to replace in this row. +// - wReplaceWith - the new value for the matching runs' attributes. +// Return Value: +// +void ATTR_ROW::ReplaceLegacyAttrs(_In_ WORD wToBeReplacedAttr, _In_ WORD wReplaceWith) noexcept +{ + TextAttribute ToBeReplaced; + ToBeReplaced.SetFromLegacy(wToBeReplacedAttr); + + TextAttribute ReplaceWith; + ReplaceWith.SetFromLegacy(wReplaceWith); + + ReplaceAttrs(ToBeReplaced, ReplaceWith); +} + + +// Method Description: +// - Replaces all runs in the row with the given toBeReplacedAttr with the new +// attribute replaceWith. +// Arguments: +// - toBeReplacedAttr - the attribute to replace in this row. +// - replaceWith - the new value for the matching runs' attributes. +// Return Value: +// - +void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith) noexcept +{ + for (auto& run : _list) + { + if (run.GetAttributes() == toBeReplacedAttr) + { + run.SetAttributes(replaceWith); + } + } +} + + +// Routine Description: +// - Takes a array of attribute runs, and inserts them into this row from startIndex to endIndex. +// - For example, if the current row was was [{4, BLUE}], the merge string +// was [{ 2, RED }], with (StartIndex, EndIndex) = (1, 2), +// then the row would modified to be = [{ 1, BLUE}, {2, RED}, {1, BLUE}]. +// Arguments: +// - rgInsertAttrs - The array of attrRuns to merge into this row. +// - cInsertAttrs - The number of elements in rgInsertAttrs +// - iStart - The index in the row to place the array of runs. +// - iEnd - the final index of the merge runs +// - BufferWidth - the width of the row. +// Return Value: +// - STATUS_NO_MEMORY if there wasn't enough memory to insert the runs +// otherwise STATUS_SUCCESS if we were successful. +[[nodiscard]] +HRESULT ATTR_ROW::InsertAttrRuns(const std::basic_string_view newAttrs, + const size_t iStart, + const size_t iEnd, + const size_t cBufferWidth) +{ + // Definitions: + // Existing Run = The run length encoded color array we're already storing in memory before this was called. + // Insert Run = The run length encoded color array that someone is asking us to inject into our stored memory run. + // New Run = The run length encoded color array that we have to allocate and rebuild to store internally + // which will replace Existing Run at the end of this function. + // Example: + // cBufferWidth = 10. + // Existing Run: R3 -> G5 -> B2 + // Insert Run: Y1 -> N1 at iStart = 5 and iEnd = 6 + // (rgInsertAttrs is a 2 length array with Y1->N1 in it and cInsertAttrs = 2) + // Final Run: R3 -> G2 -> Y1 -> N1 -> G1 -> B2 + + // We'll need to know what the last valid column is for some calculations versus iEnd + // because iEnd is specified to us as an inclusive index value. + // Do the -1 math here now so we don't have to have -1s scattered all over this function. + const size_t iLastBufferCol = cBufferWidth - 1; + + // If the insertion size is 1, do some pre-processing to + // see if we can get this done quickly. + if (newAttrs.size() == 1) + { + // Get the new color attribute we're trying to apply + const TextAttribute NewAttr = newAttrs.at(0).GetAttributes(); + + // If the existing run was only 1 element... + // ...and the new color is the same as the old, we don't have to do anything and can exit quick. + if (_list.size() == 1 && _list.at(0).GetAttributes() == NewAttr) + { + return S_OK; + } + // .. otherwise if we internally have a list of 2 and we're about to insert a single color + // it's probable that we're just walking left-to-right through the row and changing each + // cell one at a time. + // e.g. + // AAAAABBBBBBB + // AAAAAABBBBBB + // AAAAAAABBBBB + // Check for that circumstance by seeing if we're inserting a single run of the + // left side color right at the boundary and just adjust the counts in the existing + // two elements in our internal list. + else if (_list.size() == 2 && newAttrs.at(0).GetLength() == 1) + { + auto left = _list.begin(); + if (iStart == left->GetLength() && NewAttr == left->GetAttributes()) + { + auto right = left + 1; + left->IncrementLength(); + right->DecrementLength(); + + // If we just reduced the right half to zero, just erase it out of the list. + if (right->GetLength() == 0) + { + _list.erase(right); + } + return S_OK; + } + } + } + + // If we're about to cover the entire existing run with a new one, we can also make an optimization. + if (iStart == 0 && iEnd == iLastBufferCol) + { + // Just dump what we're given over what we have and call it a day. + _list.assign(newAttrs.cbegin(), newAttrs.cend()); + + return S_OK; + } + + // In the worst case scenario, we will need a new run that is the length of + // The existing run in memory + The new run in memory + 1. + // This worst case occurs when we inject a new item in the middle of an existing run like so + // Existing R3->B5->G2, Insertion Y2 starting at 5 (in the middle of the B5) + // becomes R3->B2->Y2->B1->G2. + // The original run was 3 long. The insertion run was 1 long. We need 1 more for the + // fact that an existing piece of the run was split in half (to hold the latter half). + const size_t cNewRun = _list.size() + newAttrs.size() + 1; + std::vector newRun; + newRun.resize(cNewRun); + + // We will start analyzing from the beginning of our existing run. + // Use some pointers to keep track of where we are in walking through our runs. + + // Get the existing run that we'll be updating/manipulating. + const auto existingRun = _list.begin(); + auto pExistingRunPos = existingRun; + const auto pExistingRunEnd = existingRun + _list.size(); + auto pInsertRunPos = newAttrs.begin(); + size_t cInsertRunRemaining = newAttrs.size(); + auto pNewRunPos = newRun.begin(); + size_t iExistingRunCoverage = 0; + + // Copy the existing run into the new buffer up to the "start index" where the new run will be injected. + // If the new run starts at 0, we have nothing to copy from the beginning. + if (iStart != 0) + { + // While we're less than the desired insertion position... + while (iExistingRunCoverage < iStart) + { + // Add up how much length we can cover by copying an item from the existing run. + iExistingRunCoverage += pExistingRunPos->GetLength(); + + // Copy it to the new run buffer and advance both pointers. + *pNewRunPos++ = *pExistingRunPos++; + } + + // When we get to this point, we've copied full segments from the original existing run + // into our new run buffer. We will have 1 or more full segments of color attributes and + // we MIGHT have to cut the last copied segment's length back depending on where the inserted + // attributes will fall in the final/new run. + // Some examples: + // - Starting with the original string R3 -> G5 -> B2 + // - 1. If the insertion is Y5 at start index 3 + // We are trying to get a result/final/new run of R3 -> Y5 -> B2. + // We just copied R3 to the new destination buffer and we cang skip down and start inserting the new attrs. + // - 2. If the insertion is Y3 at start index 5 + // We are trying to get a result/final/new run of R3 -> G2 -> Y3 -> B2. + // We just copied R3 -> G5 to the new destination buffer with the code above. + // But the insertion is going to cut out some of the length of the G5. + // We need to fix this up below so it says G2 instead to leave room for the Y3 to fit in + // the new/final run. + + // Copying above advanced the pointer to an empty cell beyond what we copied. + // Back up one cell so we can manipulate the final item we copied from the existing run to the new run. + pNewRunPos--; + + // Fetch out the length so we can fix it up based on the below conditions. + size_t length = pNewRunPos->GetLength(); + + // If we've covered more cells already than the start of the attributes to be inserted... + if (iExistingRunCoverage > iStart) + { + // ..then subtract some of the length of the final cell we copied. + // We want to take remove the difference in distance between the cells we've covered in the new + // run and the insertion point. + // (This turns G5 into G2 from Example 2 just above) + length -= (iExistingRunCoverage - iStart); + } + + // Now we're still on that "last cell copied" into the new run. + // If the color of that existing copied cell matches the color of the first segment + // of the run we're about to insert, we can just increment the length to extend the coverage. + if (pNewRunPos->GetAttributes() == pInsertRunPos->GetAttributes()) + { + length += pInsertRunPos->GetLength(); + + // Since the color matched, we have already "used up" part of the insert run + // and can skip it in our big "memcopy" step below that will copy the bulk of the insert run. + cInsertRunRemaining--; + pInsertRunPos++; + } + + // We're done manipulating the length. Store it back. + pNewRunPos->SetLength(length); + + // Now that we're done adjusting the last copied item, advance the pointer into a fresh/blank + // part of the new run array. + pNewRunPos++; + } + + // Bulk copy the majority (or all, depending on circumstance) of the insert run into the final run buffer. + std::copy_n(pInsertRunPos, cInsertRunRemaining, pNewRunPos); + + // Advance the new run pointer into the position just after everything we copied. + pNewRunPos += cInsertRunRemaining; + + // We're technically done with the insert run now and have 0 remaining, but won't bother updating its pointers + // and counts any further because we won't use them. + + // Now we need to move our pointer for the original existing run forward and update our counts + // on how many cells we could have copied from the source before finishing off the new run. + while (iExistingRunCoverage <= iEnd) + { + FAIL_FAST_IF(!(pExistingRunPos != pExistingRunEnd)); + iExistingRunCoverage += pExistingRunPos->GetLength(); + pExistingRunPos++; + } + + // If we still have original existing run cells remaining, copy them into the final new run. + if (pExistingRunPos != pExistingRunEnd || iExistingRunCoverage != (iEnd + 1)) + { + // Back up one cell so we can inspect the most recent item copied into the new run for optimizations. + pNewRunPos--; + + // We advanced the existing run pointer and its count to on or past the end of what the insertion run filled in. + // If this ended up being past the end of what the insertion run covers, we have to account for the cells after + // the insertion run but before the next piece of the original existing run. + // The example in this case is if we had... + // Existing Run = R3 -> G5 -> B2 -> X5 + // Insert Run = Y2 @ iStart = 7 and iEnd = 8 + // ... then at this point in time, our states would look like... + // New Run so far = R3 -> G4 -> Y2 + // Existing Run Pointer is at X5 + // Existing run coverage count at 3 + 5 + 2 = 10. + // However, in order to get the final desired New Run + // (which is R3 -> G4 -> Y2 -> B1 -> X5) + // we would need to grab a piece of that B2 we already skipped past. + // iExistingRunCoverage = 10. iEnd = 8. iEnd+1 = 9. 10 > 9. So we skipped something. + if (iExistingRunCoverage > (iEnd + 1)) + { + // Back up the existing run pointer so we can grab the piece we skipped. + pExistingRunPos--; + + // If the color matches what's already in our run, just increment the count value. + // This case is slightly off from the example above. This case is for if the B2 above was actually Y2. + // That Y2 from the existing run is the same color as the Y2 we just filled a few columns left in the final run + // so we can just adjust the final run's column count instead of adding another segment here. + if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes()) + { + size_t length = pNewRunPos->GetLength(); + length += (iExistingRunCoverage - (iEnd + 1)); + pNewRunPos->SetLength(length); + } + else + { + // If the color didn't match, then we just need to copy the piece we skipped and adjust + // its length for the discrepency in columns not yet covered by the final/new run. + + // Move forward to a blank spot in the new run + pNewRunPos++; + + // Copy the existing run's color information to the new run + pNewRunPos->SetAttributes(pExistingRunPos->GetAttributes()); + + // Adjust the length of that copied color to cover only the reduced number of columns needed + // now that some have been replaced by the insert run. + pNewRunPos->SetLength(iExistingRunCoverage - (iEnd + 1)); + } + + // Now that we're done recovering a piece of the existing run we skipped, move the pointer forward again. + pExistingRunPos++; + } + + // OK. In this case, we didn't skip anything. The end of the insert run fell right at a boundary + // in columns that was in the original existing run. + // However, the next piece of the original existing run might happen to have the same color attribute + // as the final piece of what we just copied. + // As an example... + // Existing Run = R3 -> G5 -> B2. + // Insert Run = B5 @ iStart = 3 and iEnd = 7 + // New Run so far = R3 -> B5 + // New Run desired when done = R3 -> B7 + // Existing run pointer is on B2. + // We want to merge the 2 from the B2 into the B5 so we get B7. + else if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes()) + { + // Add the value from the existing run into the current new run position. + size_t length = pNewRunPos->GetLength(); + length += pExistingRunPos->GetLength(); + pNewRunPos->SetLength(length); + + // Advance the existing run position since we consumed its value and merged it in. + pExistingRunPos++; + } + + // OK. We're done inspecting the most recently copied cell for optimizations. + pNewRunPos++; + + // Now bulk copy any segments left in the original existing run + if (pExistingRunPos < pExistingRunEnd) + { + std::copy_n(pExistingRunPos, (pExistingRunEnd - pExistingRunPos), pNewRunPos); + + // Fix up the end pointer so we know where we are for counting how much of the new run's memory space we used. + pNewRunPos += (pExistingRunEnd - pExistingRunPos); + } + } + + // OK, phew. We're done. Now we just need to free the existing run, store the new run in its place, + // and update the count for the correct length of the new run now that we've filled it up. + + newRun.erase(pNewRunPos, newRun.end()); + _list.swap(newRun); + + return S_OK; +} + +// Routine Description: +// - packs a vector of TextAttribute into a vector of TextAttrbuteRun +// Arguments: +// - attrs - text attributes to pack +// Return Value: +// - packed text attribute run +std::vector ATTR_ROW::PackAttrs(const std::vector& attrs) +{ + std::vector runs; + if (attrs.empty()) + { + return runs; + } + for (auto attr : attrs) + { + if (runs.empty() || runs.back().GetAttributes() != attr) + { + const TextAttributeRun run(1, attr); + runs.push_back(run); + } + else + { + runs.back().SetLength(runs.back().GetLength() + 1); + } + } + return runs; +} + +ATTR_ROW::const_iterator ATTR_ROW::begin() const noexcept +{ + return AttrRowIterator(this); +} + +ATTR_ROW::const_iterator ATTR_ROW::end() const noexcept +{ + return AttrRowIterator::CreateEndIterator(this); +} + +ATTR_ROW::const_iterator ATTR_ROW::cbegin() const noexcept +{ + return AttrRowIterator(this); +} + +ATTR_ROW::const_iterator ATTR_ROW::cend() const noexcept +{ + return AttrRowIterator::CreateEndIterator(this); +} + +bool operator==(const ATTR_ROW& a, const ATTR_ROW& b) noexcept +{ + return (a._list.size() == b._list.size() && + a._list.data() == b._list.data() && + a._cchRowWidth == b._cchRowWidth); +} diff --git a/src/buffer/out/AttrRow.hpp b/src/buffer/out/AttrRow.hpp new file mode 100644 index 000000000..a45d814a6 --- /dev/null +++ b/src/buffer/out/AttrRow.hpp @@ -0,0 +1,76 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- AttrRow.hpp + +Abstract: +- contains data structure for the attributes of one row of screen buffer + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "TextAttributeRun.hpp" +#include "AttrRowIterator.hpp" + +class ATTR_ROW final +{ +public: + using const_iterator = typename AttrRowIterator; + + ATTR_ROW(const UINT cchRowWidth, const TextAttribute attr); + + void Reset(const TextAttribute attr); + + TextAttribute GetAttrByColumn(const size_t column) const; + TextAttribute GetAttrByColumn(const size_t column, + size_t* const pApplies) const; + + size_t GetNumberOfRuns() const noexcept; + + size_t FindAttrIndex(const size_t index, + size_t* const pApplies) const; + + bool SetAttrToEnd(const UINT iStart, const TextAttribute attr); + void ReplaceLegacyAttrs(const WORD wToBeReplacedAttr, const WORD wReplaceWith) noexcept; + void ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith) noexcept; + + void Resize(const size_t newWidth); + + [[nodiscard]] + HRESULT InsertAttrRuns(const std::basic_string_view newAttrs, + const size_t iStart, + const size_t iEnd, + const size_t cBufferWidth); + + static std::vector PackAttrs(const std::vector& attrs); + + const_iterator begin() const noexcept; + const_iterator end() const noexcept; + + const_iterator cbegin() const noexcept; + const_iterator cend() const noexcept; + + friend bool operator==(const ATTR_ROW& a, const ATTR_ROW& b) noexcept; + friend class AttrRowIterator; + +private: + + std::vector _list; + size_t _cchRowWidth; + +#ifdef UNIT_TESTING + friend class AttrRowTests; +#endif + +}; diff --git a/src/buffer/out/AttrRowIterator.cpp b/src/buffer/out/AttrRowIterator.cpp new file mode 100644 index 000000000..0462afa14 --- /dev/null +++ b/src/buffer/out/AttrRowIterator.cpp @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "AttrRowIterator.hpp" +#include "AttrRow.hpp" + +AttrRowIterator AttrRowIterator::CreateEndIterator(const ATTR_ROW* const attrRow) +{ + AttrRowIterator it{ attrRow }; + it._setToEnd(); + return it; +} + +AttrRowIterator::AttrRowIterator(const ATTR_ROW* const attrRow) : + _pAttrRow{ attrRow }, + _run{ attrRow->_list.cbegin() }, + _currentAttributeIndex{ 0 } +{ +} + +AttrRowIterator::operator bool() const noexcept +{ + return _run < _pAttrRow->_list.cend(); +} + +bool AttrRowIterator::operator==(const AttrRowIterator& it) const +{ + return (_pAttrRow == it._pAttrRow && + _run == it._run && + _currentAttributeIndex == it._currentAttributeIndex); +} + +bool AttrRowIterator::operator!=(const AttrRowIterator& it) const +{ + return !(*this == it); +} + +AttrRowIterator& AttrRowIterator::operator++() +{ + _increment(1); + return *this; +} + +AttrRowIterator AttrRowIterator::operator++(int) +{ + auto copy = *this; + _increment(1); + return copy; +} + +AttrRowIterator& AttrRowIterator::operator+=(const ptrdiff_t& movement) +{ + if (movement >= 0) + { + _increment(gsl::narrow(movement)); + } + else + { + _decrement(gsl::narrow(-movement)); + } + + return *this; +} + +AttrRowIterator& AttrRowIterator::operator-=(const ptrdiff_t& movement) +{ + return this->operator+=(-movement); +} + +AttrRowIterator& AttrRowIterator::operator--() +{ + _decrement(1); + return *this; +} + +AttrRowIterator AttrRowIterator::operator--(int) +{ + auto copy = *this; + _decrement(1); + return copy; +} + +const TextAttribute* AttrRowIterator::operator->() const +{ + return &_run->GetAttributes(); +} + +const TextAttribute& AttrRowIterator::operator*() const +{ + return _run->GetAttributes(); +} + +// Routine Description: +// - increments the index the iterator points to +// Arguments: +// - count - the amount to increment by +void AttrRowIterator::_increment(size_t count) +{ + while (count > 0) + { + const size_t runLength = _run->GetLength(); + if (count + _currentAttributeIndex < runLength) + { + _currentAttributeIndex += count; + return; + } + else + { + count -= runLength - _currentAttributeIndex; + ++_run; + _currentAttributeIndex = 0; + } + } +} + +// Routine Description: +// - decrements the index the iterator points to +// Arguments: +// - count - the amount to decrement by +void AttrRowIterator::_decrement(size_t count) +{ + while (count > 0) + { + if (count <= _currentAttributeIndex) + { + _currentAttributeIndex -= count; + return; + } + else + { + count -= _currentAttributeIndex; + --_run; + _currentAttributeIndex = _run->GetLength() - 1; + } + } +} + +// Routine Description: +// - sets fields on the iterator to describe the end() state of the ATTR_ROW +void AttrRowIterator::_setToEnd() +{ + _run = _pAttrRow->_list.cend(); + _currentAttributeIndex = 0; +} diff --git a/src/buffer/out/AttrRowIterator.hpp b/src/buffer/out/AttrRowIterator.hpp new file mode 100644 index 000000000..92aadef59 --- /dev/null +++ b/src/buffer/out/AttrRowIterator.hpp @@ -0,0 +1,62 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- AttrRowIterator.hpp + +Abstract: +- iterator for ATTR_ROW to walk the TextAttributes of the run +- read only iterator + +Author(s): +- Austin Diviness (AustDi) 04-Jun-2018 +--*/ + + +#pragma once + +#include "TextAttribute.hpp" +#include "TextAttributeRun.hpp" + +class ATTR_ROW; + +class AttrRowIterator final +{ +public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = TextAttribute; + using difference_type = std::ptrdiff_t; + using pointer = TextAttribute*; + using reference = TextAttribute&; + + static AttrRowIterator CreateEndIterator(const ATTR_ROW* const attrRow); + + AttrRowIterator(const ATTR_ROW* const attrRow); + + operator bool() const noexcept; + + bool operator==(const AttrRowIterator& it) const; + bool operator!=(const AttrRowIterator& it) const; + + AttrRowIterator& operator++(); + AttrRowIterator operator++(int); + + AttrRowIterator& operator+=(const ptrdiff_t& movement); + AttrRowIterator& operator-=(const ptrdiff_t& movement); + + AttrRowIterator& operator--(); + AttrRowIterator operator--(int); + + const TextAttribute* operator->() const; + const TextAttribute& operator*() const; + +private: + std::vector::const_iterator _run; + const ATTR_ROW* _pAttrRow; + size_t _currentAttributeIndex; // index of TextAttribute within the current TextAttributeRun + + void _increment(size_t count); + void _decrement(size_t count); + void _setToEnd(); +}; diff --git a/src/buffer/out/CharRow.cpp b/src/buffer/out/CharRow.cpp new file mode 100644 index 000000000..2e5cf9993 --- /dev/null +++ b/src/buffer/out/CharRow.cpp @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "CharRow.hpp" +#include "unicode.hpp" +#include "Row.hpp" + +// Routine Description: +// - constructor +// Arguments: +// - rowWidth - the size (in wchar_t) of the char and attribute rows +// - pParent - the parent ROW +// Return Value: +// - instantiated object +// Note: will through if unable to allocate char/attribute buffers +CharRow::CharRow(size_t rowWidth, ROW* const pParent) : + _wrapForced{ false }, + _doubleBytePadded{ false }, + _data(rowWidth, value_type()), + _pParent{ FAIL_FAST_IF_NULL(pParent) } +{ +} + +// Routine Description: +// - Sets the wrap status for the current row +// Arguments: +// - wrapForced - True if the row ran out of space and we forced to wrap to the next row. False otherwise. +// Return Value: +// - +void CharRow::SetWrapForced(const bool wrapForced) noexcept +{ + _wrapForced = wrapForced; +} + +// Routine Description: +// - Gets the wrap status for the current row +// Arguments: +// - +// Return Value: +// - True if the row ran out of space and we were forced to wrap to the next row. False otherwise. +bool CharRow::WasWrapForced() const noexcept +{ + return _wrapForced; +} + +// Routine Description: +// - Sets the double byte padding for the current row +// Arguments: +// - fWrapWasForced - True if the row ran out of space for a double byte character and we padded out the row. False otherwise. +// Return Value: +// - +void CharRow::SetDoubleBytePadded(const bool doubleBytePadded) noexcept +{ + _doubleBytePadded = doubleBytePadded; +} + +// Routine Description: +// - Gets the double byte padding status for the current row. +// Arguments: +// - +// Return Value: +// - True if the row didn't have space for a double byte character and we were padded out the row. False otherwise. +bool CharRow::WasDoubleBytePadded() const noexcept +{ + return _doubleBytePadded; +} + +// Routine Description: +// - gets the size of the row, in glyph cells +// Arguments: +// - +// Return Value: +// - the size of the row +size_t CharRow::size() const noexcept +{ + return _data.size(); +} + +// Routine Description: +// - Sets all properties of the CharRowBase to default values +// Arguments: +// - sRowWidth - The width of the row. +// Return Value: +// - +void CharRow::Reset() +{ + for (auto& cell : _data) + { + cell.Reset(); + } + + _wrapForced = false; + _doubleBytePadded = false; +} + +// Routine Description: +// - resizes the width of the CharRowBase +// Arguments: +// - newSize - the new width of the character and attributes rows +// Return Value: +// - S_OK on success, otherwise relevant error code +[[nodiscard]] +HRESULT CharRow::Resize(const size_t newSize) noexcept +{ + try + { + const value_type insertVals; + _data.resize(newSize, insertVals); + } + CATCH_RETURN(); + + return S_OK; +} + +typename CharRow::iterator CharRow::begin() noexcept +{ + return _data.begin(); +} + +typename CharRow::const_iterator CharRow::cbegin() const noexcept +{ + return _data.cbegin(); +} + +typename CharRow::iterator CharRow::end() noexcept +{ + return _data.end(); +} + +typename CharRow::const_iterator CharRow::cend() const noexcept +{ + return _data.cend(); +} + +// Routine Description: +// - Inspects the current internal string to find the left edge of it +// Arguments: +// - +// Return Value: +// - The calculated left boundary of the internal string. +size_t CharRow::MeasureLeft() const +{ + std::vector::const_iterator it = _data.cbegin(); + while (it != _data.cend() && it->IsSpace()) + { + ++it; + } + return it - _data.cbegin(); +} + +// Routine Description: +// - Inspects the current internal string to find the right edge of it +// Arguments: +// - +// Return Value: +// - The calculated right boundary of the internal string. +size_t CharRow::MeasureRight() const noexcept +{ + std::vector::const_reverse_iterator it = _data.crbegin(); + while (it != _data.crend() && it->IsSpace()) + { + ++it; + } + return _data.crend() - it; +} + +void CharRow::ClearCell(const size_t column) +{ + _data.at(column).Reset(); +} + +// Routine Description: +// - Tells you whether or not this row contains any valid text. +// Arguments: +// - +// Return Value: +// - True if there is valid text in this row. False otherwise. +bool CharRow::ContainsText() const noexcept +{ + for (const value_type& cell : _data) + { + if (!cell.IsSpace()) + { + return true; + } + } + return false; +} + +// Routine Description: +// - gets the attribute at the specified column +// Arguments: +// - column - the column to get the attribute for +// Return Value: +// - the attribute +// Note: will throw exception if column is out of bounds +const DbcsAttribute& CharRow::DbcsAttrAt(const size_t column) const +{ + return _data.at(column).DbcsAttr(); +} + +// Routine Description: +// - gets the attribute at the specified column +// Arguments: +// - column - the column to get the attribute for +// Return Value: +// - the attribute +// Note: will throw exception if column is out of bounds +DbcsAttribute& CharRow::DbcsAttrAt(const size_t column) +{ + return const_cast(static_cast(this)->DbcsAttrAt(column)); +} + +// Routine Description: +// - resets text data at column +// Arguments: +// - column - column index to clear text data from +// Return Value: +// - +// Note: will throw exception if column is out of bounds +void CharRow::ClearGlyph(const size_t column) +{ + _data.at(column).EraseChars(); +} + +// Routine Description: +// - returns text data at column as a const reference. +// Arguments: +// - column - column to get text data for +// Return Value: +// - text data at column +// - Note: will throw exception if column is out of bounds +const CharRow::reference CharRow::GlyphAt(const size_t column) const +{ + THROW_HR_IF(E_INVALIDARG, column >= _data.size()); + return { const_cast(*this), column }; +} + +// Routine Description: +// - returns text data at column as a reference. +// Arguments: +// - column - column to get text data for +// Return Value: +// - text data at column +// - Note: will throw exception if column is out of bounds +CharRow::reference CharRow::GlyphAt(const size_t column) +{ + THROW_HR_IF(E_INVALIDARG, column >= _data.size()); + return { *this, column }; +} + +// Routine Description: +// - returns string containing text data exactly how it's stored internally, including doubling of +// leading/trailing cells. +// Arguments: +// - none +// Return Value: +// - text stored in char row +// - Note: will throw exception if out of memory +std::wstring CharRow::GetTextRaw() const +{ + std::wstring wstr; + wstr.reserve(_data.size()); + for (size_t i = 0; i < _data.size(); ++i) + { + auto glyph = GlyphAt(i); + for (auto it = glyph.begin(); it != glyph.end(); ++it) + { + wstr.push_back(*it); + } + } + return wstr; +} + +std::wstring CharRow::GetText() const +{ + std::wstring wstr; + wstr.reserve(_data.size()); + + for (size_t i = 0; i < _data.size(); ++i) + { + auto glyph = GlyphAt(i); + if (!DbcsAttrAt(i).IsTrailing()) + { + for (auto it = glyph.begin(); it != glyph.end(); ++it) + { + wstr.push_back(*it); + } + } + } + return wstr; +} + +UnicodeStorage& CharRow::GetUnicodeStorage() +{ + return _pParent->GetUnicodeStorage(); +} + +const UnicodeStorage& CharRow::GetUnicodeStorage() const +{ + return _pParent->GetUnicodeStorage(); +} + +// Routine Description: +// - calculates the key used by the given column of the char row to store glyph data in UnicodeStorage +// Arguments: +// - column - the column to generate the key for +// Return Value: +// - the COORD key for data access from UnicodeStorage for the column +COORD CharRow::GetStorageKey(const size_t column) const +{ + return { gsl::narrow(column), _pParent->GetId() }; +} + +// Routine Description: +// - Updates the pointer to the parent row (which might change if we shuffle the rows around) +// Arguments: +// - pParent - Pointer to the parent row +void CharRow::UpdateParent(ROW* const pParent) noexcept +{ + _pParent = FAIL_FAST_IF_NULL(pParent); +} diff --git a/src/buffer/out/CharRow.hpp b/src/buffer/out/CharRow.hpp new file mode 100644 index 000000000..555dfd6be --- /dev/null +++ b/src/buffer/out/CharRow.hpp @@ -0,0 +1,123 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CharRow.hpp + +Abstract: +- contains data structure for UCS2 encoded character data of a row + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" +#include "CharRowCellReference.hpp" +#include "CharRowCell.hpp" +#include "UnicodeStorage.hpp" + +class ROW; + +// the characters of one row of screen buffer +// we keep the following values so that we don't write +// more pixels to the screen than we have to: +// left is initialized to screenbuffer width. right is +// initialized to zero. +// +// [ foo.bar 12-12-61 ] +// ^ ^ ^ ^ +// | | | | +// Chars Left Right end of Chars buffer +class CharRow final +{ +public: + using glyph_type = typename wchar_t; + using value_type = typename CharRowCell; + using iterator = typename std::vector::iterator; + using const_iterator = typename std::vector::const_iterator; + using reference = typename CharRowCellReference; + + CharRow(size_t rowWidth, ROW* const pParent); + + void SetWrapForced(const bool wrap) noexcept; + bool WasWrapForced() const noexcept; + void SetDoubleBytePadded(const bool doubleBytePadded) noexcept; + bool WasDoubleBytePadded() const noexcept; + size_t size() const noexcept; + void Reset(); + [[nodiscard]] + HRESULT Resize(const size_t newSize) noexcept; + size_t MeasureLeft() const; + size_t MeasureRight() const noexcept; + void ClearCell(const size_t column); + bool ContainsText() const noexcept; + const DbcsAttribute& DbcsAttrAt(const size_t column) const; + DbcsAttribute& DbcsAttrAt(const size_t column); + void ClearGlyph(const size_t column); + std::wstring GetText() const; + + // other functions implemented at the template class level + std::wstring GetTextRaw() const; + + // working with glyphs + const reference GlyphAt(const size_t column) const; + reference GlyphAt(const size_t column); + + // iterators + iterator begin() noexcept; + const_iterator cbegin() const noexcept; + + iterator end() noexcept; + const_iterator cend() const noexcept; + + UnicodeStorage& GetUnicodeStorage(); + const UnicodeStorage& GetUnicodeStorage() const; + COORD GetStorageKey(const size_t column) const; + + void UpdateParent(ROW* const pParent) noexcept; + + friend CharRowCellReference; + friend constexpr bool operator==(const CharRow& a, const CharRow& b) noexcept; + +protected: + // Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line + bool _wrapForced; + + // Occurs when the user runs out of text to support a double byte character and we're forced to the next line + bool _doubleBytePadded; + + // storage for glyph data and dbcs attributes + std::vector _data; + + // ROW that this CharRow belongs to + ROW* _pParent; +}; + +constexpr bool operator==(const CharRow& a, const CharRow& b) noexcept +{ + return (a._wrapForced == b._wrapForced && + a._doubleBytePadded == b._doubleBytePadded && + a._data == b._data); +} + +template +void OverwriteColumns(InputIt1 startChars, InputIt1 endChars, InputIt2 startAttrs, CharRow::iterator outIt) +{ + std::transform(startChars, + endChars, + startAttrs, + outIt, + [](const wchar_t wch, const DbcsAttribute attr) + { + return CharRow::value_type{ wch, attr }; + }); +} diff --git a/src/buffer/out/CharRowCell.cpp b/src/buffer/out/CharRowCell.cpp new file mode 100644 index 000000000..180c3520c --- /dev/null +++ b/src/buffer/out/CharRowCell.cpp @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "precomp.h" + +#include "CharRowCell.hpp" +#include "unicode.hpp" + + +// default glyph value, used for reseting the character data portion of a cell +static constexpr wchar_t DefaultValue = UNICODE_SPACE; + +CharRowCell::CharRowCell() : + _wch{ DefaultValue }, + _attr{} +{ +} + +CharRowCell::CharRowCell(const wchar_t wch, const DbcsAttribute attr) : + _wch{ wch }, + _attr{ attr } +{ +} + +// Routine Description: +// - "erases" the glyph. really sets it back to the default "empty" value +void CharRowCell::EraseChars() +{ + if (_attr.IsGlyphStored()) + { + _attr.SetGlyphStored(false); + } + _wch = DefaultValue; +} + +// Routine Description: +// - resets this object back to the defaults it would have from the default constructor +void CharRowCell::Reset() noexcept +{ + _attr.Reset(); + _wch = DefaultValue; +} + +// Routine Description: +// - checks if cell contains a space glyph +// Return Value: +// - true if cell contains a space glyph, false otherwise +bool CharRowCell::IsSpace() const noexcept +{ + return !_attr.IsGlyphStored() && _wch == UNICODE_SPACE; +} + +// Routine Description: +// - Access the DbcsAttribute for the cell +// Return Value: +// - ref to the cells' DbcsAttribute +DbcsAttribute& CharRowCell::DbcsAttr() noexcept +{ + return _attr; +} + +// Routine Description: +// - Access the DbcsAttribute for the cell +// Return Value: +// - ref to the cells' DbcsAttribute +const DbcsAttribute& CharRowCell::DbcsAttr() const noexcept +{ + return _attr; +} + +// Routine Description: +// - Access the cell's wchar field. this does not access any char data through UnicodeStorage. +// Return Value: +// - the cell's wchar field +wchar_t& CharRowCell::Char() noexcept +{ + return _wch; +} + +// Routine Description: +// - Access the cell's wchar field. this does not access any char data through UnicodeStorage. +// Return Value: +// - the cell's wchar field +const wchar_t& CharRowCell::Char() const noexcept +{ + return _wch; +} diff --git a/src/buffer/out/CharRowCell.hpp b/src/buffer/out/CharRowCell.hpp new file mode 100644 index 000000000..cca4ad453 --- /dev/null +++ b/src/buffer/out/CharRowCell.hpp @@ -0,0 +1,60 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CharRowCell.hpp + +Abstract: +- data structure for one cell of a char row. contains the char data for one + coordinate position in the output buffer (leading/trailing information and + the char itself. + +Author(s): +- Austin Diviness (AustDi) 02-May-2018 +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" + + +#if (defined(_M_IX86) || defined(_M_AMD64)) +// currently CharRowCell's fields use 3 bytes of memory, leaving the 4th byte in unused. this leads +// to a rather large amount of useless memory allocated. so instead, pack CharRowCell by bytes instead of words. +#pragma pack(push, 1) +#endif + + +class CharRowCell final +{ +public: + CharRowCell(); + CharRowCell(const wchar_t wch, const DbcsAttribute attr); + + void EraseChars(); + void Reset() noexcept; + + bool IsSpace() const noexcept; + + DbcsAttribute& DbcsAttr() noexcept; + const DbcsAttribute& DbcsAttr() const noexcept; + + wchar_t& Char() noexcept; + const wchar_t& Char() const noexcept; + + friend constexpr bool operator==(const CharRowCell& a, const CharRowCell& b) noexcept; +private: + wchar_t _wch; + DbcsAttribute _attr; +}; + +#if (defined(_M_IX86) || defined(_M_AMD64)) +#pragma pack(pop) +#endif + +constexpr bool operator==(const CharRowCell& a, const CharRowCell& b) noexcept +{ + return (a._wch == b._wch && + a._attr == b._attr); +} diff --git a/src/buffer/out/CharRowCellReference.cpp b/src/buffer/out/CharRowCellReference.cpp new file mode 100644 index 000000000..b380ae8ee --- /dev/null +++ b/src/buffer/out/CharRowCellReference.cpp @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "UnicodeStorage.hpp" +#include "CharRow.hpp" + + +// Routine Description: +// - assignment operator. will store extended glyph data in a separate storage location +// Arguments: +// - chars - the glyph data to store +void CharRowCellReference::operator=(const std::wstring_view chars) +{ + THROW_HR_IF(E_INVALIDARG, chars.empty()); + if (chars.size() == 1) + { + _cellData().Char() = chars.front(); + _cellData().DbcsAttr().SetGlyphStored(false); + } + else + { + auto& storage = _parent.GetUnicodeStorage(); + const auto key = _parent.GetStorageKey(_index); + storage.StoreGlyph(key, { chars.cbegin(), chars.cend() }); + _cellData().DbcsAttr().SetGlyphStored(true); + } +} + +// Routine Description: +// - implicit conversion to vector operator. +// Return Value: +// - std::vector of the glyph data in the referenced cell +CharRowCellReference::operator std::wstring_view() const +{ + return _glyphData(); +} + +// Routine Description: +// - The CharRowCell this object "references" +// Return Value: +// - ref to the CharRowCell +CharRowCell& CharRowCellReference::_cellData() +{ + return _parent._data.at(_index); +} + +// Routine Description: +// - The CharRowCell this object "references" +// Return Value: +// - ref to the CharRowCell +const CharRowCell& CharRowCellReference::_cellData() const +{ + return _parent._data.at(_index); +} + +// Routine Description: +// - the glyph data of the referenced cell +// Return Value: +// - the glyph data +std::wstring_view CharRowCellReference::_glyphData() const +{ + if (_cellData().DbcsAttr().IsGlyphStored()) + { + const auto& text = _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)); + + return { text.data(), text.size() }; + } + else + { + return { &_cellData().Char(), 1 }; + } +} + +// Routine Description: +// - gets read-only iterator to the beginning of the glyph data +// Return Value: +// - iterator of the glyph data +CharRowCellReference::const_iterator CharRowCellReference::begin() const +{ + if (_cellData().DbcsAttr().IsGlyphStored()) + { + return _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)).data(); + } + else + { + return &_cellData().Char(); + } +} + +// Routine Description: +// - get read-only iterator to the end of the glyph data +// Return Value: +// - end iterator of the glyph data +CharRowCellReference::const_iterator CharRowCellReference::end() const +{ + if (_cellData().DbcsAttr().IsGlyphStored()) + { + + const auto& chars = _parent.GetUnicodeStorage().GetText(_parent.GetStorageKey(_index)); + return chars.data() + chars.size(); + } + else + { + return &_cellData().Char() + 1; + } +} + +bool operator==(const CharRowCellReference& ref, const std::vector& glyph) +{ + const DbcsAttribute& dbcsAttr = ref._cellData().DbcsAttr(); + if (glyph.size() == 1 && dbcsAttr.IsGlyphStored()) + { + return false; + } + else if (glyph.size() > 1 && !dbcsAttr.IsGlyphStored()) + { + return false; + } + else if (glyph.size() == 1 && !dbcsAttr.IsGlyphStored()) + { + return ref._cellData().Char() == glyph.front(); + } + else + { + const auto& chars = ref._parent.GetUnicodeStorage().GetText(ref._parent.GetStorageKey(ref._index)); + return chars == glyph; + } +} + +bool operator==(const std::vector& glyph, const CharRowCellReference& ref) +{ + return ref == glyph; +} diff --git a/src/buffer/out/CharRowCellReference.hpp b/src/buffer/out/CharRowCellReference.hpp new file mode 100644 index 000000000..d1fb7e80c --- /dev/null +++ b/src/buffer/out/CharRowCellReference.hpp @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CharRowCellReference.hpp + +Abstract: +- reference class for the glyph data of a char row cell + +Author(s): +- Austin Diviness (AustDi) 02-May-2018 +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" +#include "CharRowCell.hpp" +#include + +class CharRow; + +class CharRowCellReference final +{ +public: + + using const_iterator = const wchar_t*; + + CharRowCellReference(CharRow& parent, const size_t index) : + _parent{ parent }, + _index{ index } + { + } + + ~CharRowCellReference() = default; + CharRowCellReference(const CharRowCellReference&) = default; + CharRowCellReference(CharRowCellReference&&) = default; + + void operator=(const CharRowCellReference&) = delete; + void operator=(CharRowCellReference&&) = delete; + + void operator=(const std::wstring_view chars); + operator std::wstring_view() const; + + const_iterator begin() const; + const_iterator end() const; + + + friend bool operator==(const CharRowCellReference& ref, const std::vector& glyph); + friend bool operator==(const std::vector& glyph, const CharRowCellReference& ref); + +private: + // what char row the object belongs to + CharRow& _parent; + // the index of the cell in the parent char row + const size_t _index; + + CharRowCell& _cellData(); + const CharRowCell& _cellData() const; + + std::wstring_view _glyphData() const; +}; + +bool operator==(const CharRowCellReference& ref, const std::vector& glyph); +bool operator==(const std::vector& glyph, const CharRowCellReference& ref); diff --git a/src/buffer/out/DbcsAttribute.hpp b/src/buffer/out/DbcsAttribute.hpp new file mode 100644 index 000000000..34045efc1 --- /dev/null +++ b/src/buffer/out/DbcsAttribute.hpp @@ -0,0 +1,143 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DbcsAttribute.hpp + +Abstract: +- helper class for storing double byte character set information about a cell in the output buffer. + +Author(s): +- Austin Diviness (AustDi) 26-Jan-2018 + +Revision History: +--*/ + +#pragma once + +class DbcsAttribute final +{ +public: + enum class Attribute : BYTE + { + Single = 0x00, + Leading = 0x01, + Trailing = 0x02 + }; + + DbcsAttribute() noexcept : + _attribute{ Attribute::Single }, + _glyphStored{ false } + { + } + + DbcsAttribute(const Attribute attribute) noexcept : + _attribute{ attribute }, + _glyphStored{ false } + { + } + + constexpr bool IsSingle() const noexcept + { + return _attribute == Attribute::Single; + } + + constexpr bool IsLeading() const noexcept + { + return _attribute == Attribute::Leading; + } + + constexpr bool IsTrailing() const noexcept + { + return _attribute == Attribute::Trailing; + } + + constexpr bool IsDbcs() const noexcept + { + return IsLeading() || IsTrailing(); + } + + constexpr bool IsGlyphStored() const noexcept + { + return _glyphStored; + } + + void SetGlyphStored(const bool stored) + { + _glyphStored = stored; + } + + void SetSingle() noexcept + { + _attribute = Attribute::Single; + } + + void SetLeading() noexcept + { + _attribute = Attribute::Leading; + } + + void SetTrailing() noexcept + { + _attribute = Attribute::Trailing; + } + + void Reset() noexcept + { + SetSingle(); + SetGlyphStored(false); + } + + WORD GeneratePublicApiAttributeFormat() const noexcept + { + WORD publicAttribute = 0; + if (IsLeading()) + { + WI_SetFlag(publicAttribute, COMMON_LVB_LEADING_BYTE); + } + if (IsTrailing()) + { + WI_SetFlag(publicAttribute, COMMON_LVB_TRAILING_BYTE); + } + return publicAttribute; + } + + static DbcsAttribute FromPublicApiAttributeFormat(WORD publicAttribute) + { + // it's not valid to be both a leading and trailing byte + if (WI_AreAllFlagsSet(publicAttribute, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE)) + { + THROW_HR(E_INVALIDARG); + } + + DbcsAttribute attr; + if (WI_IsFlagSet(publicAttribute, COMMON_LVB_LEADING_BYTE)) + { + attr.SetLeading(); + } + else if (WI_IsFlagSet(publicAttribute, COMMON_LVB_TRAILING_BYTE)) + { + attr.SetTrailing(); + } + return attr; + } + + friend constexpr bool operator==(const DbcsAttribute& a, const DbcsAttribute& b) noexcept; + +private: + Attribute _attribute : 2; + bool _glyphStored : 1; + +#ifdef UNIT_TESTING + friend class TextBufferTests; +#endif +}; + +constexpr bool operator==(const DbcsAttribute& a, const DbcsAttribute& b) noexcept +{ + return a._attribute == b._attribute; +} + +static_assert(sizeof(DbcsAttribute) == sizeof(BYTE), "DbcsAttribute should be one byte big. if this changes then it needs" + " either an implicit conversion to a BYTE or an update to all places that assume it's a byte big"); diff --git a/src/buffer/out/OutputCell.cpp b/src/buffer/out/OutputCell.cpp new file mode 100644 index 000000000..7cc1d36d7 --- /dev/null +++ b/src/buffer/out/OutputCell.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "OutputCell.hpp" + +#include "../../types/inc/GlyphWidth.hpp" +#include "../../types/inc/convert.hpp" +#include "../../inc/conattrs.hpp" + +static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; + +OutputCell::OutputCell() : + _text{}, + _dbcsAttribute{}, + _textAttribute{ InvalidTextAttribute }, + _behavior{ TextAttributeBehavior::Stored } +{ + +} + +OutputCell::OutputCell(const std::wstring_view charData, + const DbcsAttribute dbcsAttribute, + const TextAttributeBehavior behavior) : + _text{ UNICODE_INVALID }, + _dbcsAttribute{ dbcsAttribute }, + _textAttribute{ InvalidTextAttribute }, + _behavior{ behavior } +{ + THROW_HR_IF(E_INVALIDARG, charData.empty()); + _setFromStringView(charData); + _setFromBehavior(behavior); +} + +OutputCell::OutputCell(const std::wstring_view charData, + const DbcsAttribute dbcsAttribute, + const TextAttribute textAttribute) : + _text{ UNICODE_INVALID }, + _dbcsAttribute{ dbcsAttribute }, + _textAttribute{ textAttribute }, + _behavior{ TextAttributeBehavior::Stored } +{ + THROW_HR_IF(E_INVALIDARG, charData.empty()); + _setFromStringView(charData); +} + +OutputCell::OutputCell(const CHAR_INFO& charInfo) : + _text{ UNICODE_INVALID }, + _dbcsAttribute{}, + _textAttribute{ InvalidTextAttribute }, + _behavior{ TextAttributeBehavior::Stored } +{ + _setFromCharInfo(charInfo); +} + +OutputCell::OutputCell(const OutputCellView& cell) +{ + _setFromOutputCellView(cell); +} + +const std::wstring_view OutputCell::Chars() const noexcept +{ + return _text; +} + +void OutputCell::SetChars(const std::wstring_view chars) +{ + _setFromStringView(chars); +} + +DbcsAttribute& OutputCell::DbcsAttr() noexcept +{ + return _dbcsAttribute; +} + +TextAttribute& OutputCell::TextAttr() +{ + THROW_HR_IF(E_INVALIDARG, _behavior == TextAttributeBehavior::Current); + return _textAttribute; +} + +void OutputCell::_setFromBehavior(const TextAttributeBehavior behavior) +{ + THROW_HR_IF(E_INVALIDARG, behavior == TextAttributeBehavior::Stored); +} + +void OutputCell::_setFromCharInfo(const CHAR_INFO& charInfo) +{ + _text = charInfo.Char.UnicodeChar; + + if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_LEADING_BYTE)) + { + _dbcsAttribute.SetLeading(); + } + else if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_TRAILING_BYTE)) + { + _dbcsAttribute.SetTrailing(); + } + _textAttribute.SetFromLegacy(charInfo.Attributes); + + _behavior = TextAttributeBehavior::Stored; +} + +void OutputCell::_setFromStringView(const std::wstring_view view) +{ + _text = view; +} + +void OutputCell::_setFromOutputCellView(const OutputCellView& cell) +{ + _dbcsAttribute = cell.DbcsAttr(); + _textAttribute = cell.TextAttr(); + _behavior = cell.TextAttrBehavior(); + + const auto& view = cell.Chars(); + _text = view; +} diff --git a/src/buffer/out/OutputCell.hpp b/src/buffer/out/OutputCell.hpp new file mode 100644 index 000000000..5783dfcfc --- /dev/null +++ b/src/buffer/out/OutputCell.hpp @@ -0,0 +1,92 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OutputCell.hpp + +Abstract: +- Representation of all data stored in a cell of the output buffer. +- RGB color supported. + +Author: +- Austin Diviness (AustDi) 20-Mar-2018 + +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" +#include "TextAttribute.hpp" +#include "OutputCellView.hpp" + +#include +#include + +class InvalidCharInfoConversionException : public std::exception +{ + const char* what() noexcept + { + return "Cannot convert to CHAR_INFO without explicit TextAttribute"; + } +}; + +class OutputCell final +{ +public: + OutputCell(); + + OutputCell(const std::wstring_view charData, + const DbcsAttribute dbcsAttribute, + const TextAttributeBehavior behavior); + + OutputCell(const std::wstring_view charData, + const DbcsAttribute dbcsAttribute, + const TextAttribute textAttribute); + + OutputCell(const CHAR_INFO& charInfo); + OutputCell(const OutputCellView& view); + + OutputCell(const OutputCell&) = default; + OutputCell(OutputCell&&) = default; + + OutputCell& operator=(const OutputCell&) = default; + OutputCell& operator=(OutputCell&&) = default; + + ~OutputCell() = default; + + const std::wstring_view Chars() const noexcept; + void SetChars(const std::wstring_view chars); + + DbcsAttribute& DbcsAttr() noexcept; + TextAttribute& TextAttr(); + + constexpr const DbcsAttribute& DbcsAttr() const + { + return _dbcsAttribute; + } + + const TextAttribute& TextAttr() const + { + THROW_HR_IF(E_INVALIDARG, _behavior == TextAttributeBehavior::Current); + return _textAttribute; + } + + constexpr TextAttributeBehavior TextAttrBehavior() const + { + return _behavior; + } + +private: + // basic_string contains a small storage internally so we don't need + // to worry about heap allocation for short strings. + std::wstring _text; + DbcsAttribute _dbcsAttribute; + TextAttribute _textAttribute; + TextAttributeBehavior _behavior; + + void _setFromBehavior(const TextAttributeBehavior behavior); + void _setFromCharInfo(const CHAR_INFO& charInfo); + void _setFromStringView(const std::wstring_view view); + void _setFromOutputCellView(const OutputCellView& cell); +}; diff --git a/src/buffer/out/OutputCellIterator.cpp b/src/buffer/out/OutputCellIterator.cpp new file mode 100644 index 000000000..e213c2c40 --- /dev/null +++ b/src/buffer/out/OutputCellIterator.cpp @@ -0,0 +1,560 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "OutputCellIterator.hpp" + +#include "../../types/inc/convert.hpp" +#include "../../types/inc/Utf16Parser.hpp" +#include "../../types/inc/GlyphWidth.hpp" +#include "../../inc/conattrs.hpp" + +static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; + +// Routine Description: +// - This is a fill-mode iterator for one particular wchar. It will repeat forever if fillLimit is 0. +// Arguments: +// - wch - The character to use for filling +// - fillLimit - How many times to allow this value to be viewed/filled. Infinite if 0. +OutputCellIterator::OutputCellIterator(const wchar_t& wch, const size_t fillLimit) : + _mode(Mode::Fill), + _currentView(s_GenerateView(wch)), + _run(), + _attr(InvalidTextAttribute), + _pos(0), + _distance(0), + _fillLimit(fillLimit) +{ + +} + +// Routine Description: +// - This is a fill-mode iterator for one particular color. It will repeat forever if fillLimit is 0. +// Arguments: +// - attr - The color attribute to use for filling +// - fillLimit - How many times to allow this value to be viewed/filled. Infinite if 0. +OutputCellIterator::OutputCellIterator(const TextAttribute& attr, const size_t fillLimit) : + _mode(Mode::Fill), + _currentView(s_GenerateView(attr)), + _run(), + _attr(InvalidTextAttribute), + _pos(0), + _distance(0), + _fillLimit(fillLimit) +{ + +} + +// Routine Description: +// - This is a fill-mode iterator for one particular character and color. It will repeat forever if fillLimit is 0. +// Arguments: +// - wch - The character to use for filling +// - attr - The color attribute to use for filling +// - fillLimit - How many times to allow this value to be viewed/filled. Infinite if 0. +OutputCellIterator::OutputCellIterator(const wchar_t& wch, const TextAttribute& attr, const size_t fillLimit) : + _mode(Mode::Fill), + _currentView(s_GenerateView(wch, attr)), + _run(), + _attr(InvalidTextAttribute), + _pos(0), + _distance(0), + _fillLimit(fillLimit) +{ + +} + +// Routine Description: +// - This is a fill-mode iterator for one particular CHAR_INFO. It will repeat forever if fillLimit is 0. +// Arguments: +// - charInfo - The legacy character and color data to use for fililng (uses Unicode portion of text data) +// - fillLimit - How many times to allow this value to be viewed/filled. Infinite if 0. +OutputCellIterator::OutputCellIterator(const CHAR_INFO& charInfo, const size_t fillLimit) : + _mode(Mode::Fill), + _currentView(s_GenerateView(charInfo)), + _run(), + _attr(InvalidTextAttribute), + _pos(0), + _distance(0), + _fillLimit(fillLimit) +{ + +} + +// Routine Description: +// - This is an iterator over a range of text only. No color data will be modified as the text is inserted. +// Arguments: +// - utf16Text - UTF-16 text range +OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text) : + _mode(Mode::LooseTextOnly), + _currentView(s_GenerateView(utf16Text)), + _run(utf16Text), + _attr(InvalidTextAttribute), + _pos(0), + _distance(0), + _fillLimit(0) +{ + +} + +// Routine Description: +// - This is an iterator over a range text that will apply the same color to every position. +// Arguments: +// - utf16Text - UTF-16 text range +// - attribute - Color to apply over the entire range +OutputCellIterator::OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute attribute) : + _mode(Mode::Loose), + _currentView(s_GenerateView(utf16Text, attribute)), + _run(utf16Text), + _attr(attribute), + _distance(0), + _pos(0), + _fillLimit(0) +{ + +} + +// Routine Description: +// - This is an iterator over legacy colors only. The text is not modified. +// Arguments: +// - legacyAttrs - One legacy color item per cell +// - unused - useless bool to change function signature for legacyAttrs constructor because the C++ compiler in +// razzle cannot distinguish between a std::wstring_view and a std::basic_string_view +// NOTE: This one internally casts to wchar_t because Razzle sees WORD and wchar_t as the same type +// despite that Visual Studio build can tell the difference. +OutputCellIterator::OutputCellIterator(const std::basic_string_view legacyAttrs, const bool /*unused*/) : + _mode(Mode::LegacyAttr), + _currentView(s_GenerateViewLegacyAttr(legacyAttrs.at(0))), + _run(std::wstring_view(reinterpret_cast(legacyAttrs.data()), legacyAttrs.size())), + _attr(InvalidTextAttribute), + _distance(0), + _pos(0), + _fillLimit(0) +{ + +} + +// Routine Description: +// - This is an iterator over legacy cell data. We will use the unicode text and the legacy color attribute. +// Arguments: +// - charInfos - Multiple cell with unicode text and legacy color data. +OutputCellIterator::OutputCellIterator(const std::basic_string_view charInfos) : + _mode(Mode::CharInfo), + _currentView(s_GenerateView(charInfos.at(0))), + _run(charInfos), + _attr(InvalidTextAttribute), + _distance(0), + _pos(0), + _fillLimit(0) +{ + +} + +// Routine Description: +// - This is an iterator over existing OutputCells with full text and color data. +// Arguments: +// - cells - Multiple cells in a run +OutputCellIterator::OutputCellIterator(const std::basic_string_view cells) : + _mode(Mode::Cell), + _currentView(s_GenerateView(cells.at(0))), + _run(cells), + _attr(InvalidTextAttribute), + _distance(0), + _pos(0), + _fillLimit(0) +{ + +} + +// Routine Description: +// - Specifies whether this iterator is valid for dereferencing (still valid underlying data) +// Return Value: +// - True if the views on dereference are valid. False if it shouldn't be dereferenced. +OutputCellIterator::operator bool() const noexcept +{ + try + { + switch (_mode) + { + case Mode::Loose: + case Mode::LooseTextOnly: + { + // In lieu of using start and end, this custom iterator type simply becomes bool false + // when we run out of items to iterate over. + return _pos < std::get(_run).length(); + } + case Mode::Fill: + { + if (_fillLimit > 0) + { + return _pos < _fillLimit; + } + return true; + } + case Mode::Cell: + { + return _pos < std::get>(_run).length(); + } + case Mode::CharInfo: + { + return _pos < std::get>(_run).length(); + } + case Mode::LegacyAttr: + { + return _pos < std::get(_run).length(); + } + default: + FAIL_FAST_HR(E_NOTIMPL); + } + } + CATCH_FAIL_FAST(); +} + +// Routine Description: +// - Advances the iterator one position over the underlying data source. +// Return Value: +// - Reference to self after advancement. +OutputCellIterator& OutputCellIterator::operator++() +{ + // Keep track of total distance moved (cells filled) + _distance++; + + switch (_mode) + { + case Mode::Loose: + { + if (!_TryMoveTrailing()) + { + // When walking through a text sequence, we need to move forward by the number of wchar_ts consumed in the previous view + // in case we had a surrogate pair (or wider complex sequence) in the previous view. + _pos += _currentView.Chars().size(); + if (operator bool()) + { + _currentView = s_GenerateView(std::get(_run).substr(_pos), _attr); + } + } + break; + } + case Mode::LooseTextOnly: + { + if (!_TryMoveTrailing()) + { + // When walking through a text sequence, we need to move forward by the number of wchar_ts consumed in the previous view + // in case we had a surrogate pair (or wider complex sequence) in the previous view. + _pos += _currentView.Chars().size(); + if (operator bool()) + { + _currentView = s_GenerateView(std::get(_run).substr(_pos)); + } + } + break; + } + case Mode::Fill: + { + if (!_TryMoveTrailing()) + { + if (_currentView.DbcsAttr().IsTrailing()) + { + auto dbcsAttr = _currentView.DbcsAttr(); + dbcsAttr.SetLeading(); + + _currentView = OutputCellView(_currentView.Chars(), + dbcsAttr, + _currentView.TextAttr(), + _currentView.TextAttrBehavior()); + } + + if (_fillLimit > 0) + { + // We walk forward by one because we fill with the same cell over and over no matter what + _pos++; + } + } + break; + } + case Mode::Cell: + { + // Walk forward by one because cells are assumed to be in the form they needed to be + _pos++; + if (operator bool()) + { + _currentView = s_GenerateView(std::get>(_run).at(_pos)); + } + break; + } + case Mode::CharInfo: + { + // Walk forward by one because charinfos are just the legacy version of cells and prealigned to columns + _pos++; + if (operator bool()) + { + _currentView = s_GenerateView(std::get>(_run).at(_pos)); + } + break; + } + case Mode::LegacyAttr: + { + // Walk forward by one because color attributes apply cell by cell (no complex text information) + _pos++; + if (operator bool()) + { + _currentView = s_GenerateViewLegacyAttr(std::get(_run).at(_pos)); + } + break; + } + default: + FAIL_FAST_HR(E_NOTIMPL); + } + + return (*this); +} + +// Routine Description: +// - Advances the iterator one position over the underlying data source. +// Return Value: +// - Reference to self after advancement. +OutputCellIterator OutputCellIterator::operator++(int) +{ + auto temp(*this); + operator++(); + return temp; +} + +// Routine Description: +// - Reference the view to fully-formed output cell data representing the underlying data source. +// Return Value: +// - Reference to the view +const OutputCellView& OutputCellIterator::operator*() const +{ + return _currentView; +} + +// Routine Description: +// - Get pointer to the view to fully-formed output cell data representing the underlying data source. +// Return Value: +// - Pointer to the view +const OutputCellView* OutputCellIterator::operator->() const +{ + return &_currentView; +} + +// Routine Description: +// - Checks the current view. If it is a leading half, it updates the current +// view to the trailing half of the same glyph. +// - This helps us to draw glyphs that are two columns wide by "doubling" +// the view that is returned so it will consume two cells. +// Return Value: +// - True if we just turned a lead half into a trailing half (and caller doesn't +// need to further update the view). +// - False if this wasn't applicable and the caller should update the view. +bool OutputCellIterator::_TryMoveTrailing() +{ + if (_currentView.DbcsAttr().IsLeading()) + { + auto dbcsAttr = _currentView.DbcsAttr(); + dbcsAttr.SetTrailing(); + + _currentView = OutputCellView(_currentView.Chars(), + dbcsAttr, + _currentView.TextAttr(), + _currentView.TextAttrBehavior()); + return true; + } + else + { + return false; + } +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and specify that the attributes shouldn't be changed. +// Arguments: +// - view - View representing characters corresponding to a single glyph +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view) +{ + return s_GenerateView(view, InvalidTextAttribute, TextAttributeBehavior::Current); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - view - View representing characters corresponding to a single glyph +// - attr - Color attributes to apply to the text +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, + const TextAttribute attr) +{ + return s_GenerateView(view, attr, TextAttributeBehavior::Stored); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - view - View representing characters corresponding to a single glyph +// - attr - Color attributes to apply to the text +// - behavior - Behavior of the given text attribute (used when writing) +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const std::wstring_view view, + const TextAttribute attr, + const TextAttributeBehavior behavior) +{ + const auto glyph = Utf16Parser::ParseNext(view); + DbcsAttribute dbcsAttr; + if (!glyph.empty() && IsGlyphFullWidth(glyph)) + { + dbcsAttr.SetLeading(); + } + + return OutputCellView(glyph, dbcsAttr, attr, behavior); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - wch - View representing a single UTF-16 character (that can be represented without surrogates) +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const wchar_t& wch) +{ + const auto glyph = std::wstring_view(&wch, 1); + + DbcsAttribute dbcsAttr; + if (IsGlyphFullWidth(wch)) + { + dbcsAttr.SetLeading(); + } + + return OutputCellView(glyph, dbcsAttr, InvalidTextAttribute, TextAttributeBehavior::Current); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - attr - View representing a single color +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const TextAttribute& attr) +{ + return OutputCellView({}, {}, attr, TextAttributeBehavior::StoredOnly); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - wch - View representing a single UTF-16 character (that can be represented without surrogates) +// - attr - View representing a single color +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const wchar_t& wch, const TextAttribute& attr) +{ + const auto glyph = std::wstring_view(&wch, 1); + + DbcsAttribute dbcsAttr; + if (IsGlyphFullWidth(wch)) + { + dbcsAttr.SetLeading(); + } + + return OutputCellView(glyph, dbcsAttr, attr, TextAttributeBehavior::Stored); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - legacyAttr - View representing a single legacy color +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateViewLegacyAttr(const WORD& legacyAttr) +{ + WORD cleanAttr = legacyAttr; + WI_ClearAllFlags(cleanAttr, COMMON_LVB_SBCSDBCS); // don't use legacy lead/trailing byte flags for colors + + TextAttribute attr(cleanAttr); + return s_GenerateView(attr); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - charInfo - character and attribute pair representing a single cell +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const CHAR_INFO& charInfo) +{ + const auto glyph = std::wstring_view(&charInfo.Char.UnicodeChar, 1); + + DbcsAttribute dbcsAttr; + if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_LEADING_BYTE)) + { + dbcsAttr.SetLeading(); + } + else if (WI_IsFlagSet(charInfo.Attributes, COMMON_LVB_TRAILING_BYTE)) + { + dbcsAttr.SetTrailing(); + } + + TextAttribute textAttr; + textAttr.SetFromLegacy(charInfo.Attributes); + + const auto behavior = TextAttributeBehavior::Stored; + return OutputCellView(glyph, dbcsAttr, textAttr, behavior); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// Arguments: +// - cell - A reference to the cell for which we will make the read-only view +// Return Value: +// - Object representing the view into this cell +OutputCellView OutputCellIterator::s_GenerateView(const OutputCell& cell) +{ + return OutputCellView(cell.Chars(), cell.DbcsAttr(), cell.TextAttr(), cell.TextAttrBehavior()); +} + +// Routine Description: +// - Gets the distance between two iterators relative to the input data given in. +// Return Value: +// - The number of items of the input run consumed between these two iterators. +ptrdiff_t OutputCellIterator::GetInputDistance(OutputCellIterator other) const noexcept +{ + return _pos - other._pos; +} + +// Routine Description: +// - Gets the distance between two iterators relative to the number of cells inserted. +// Return Value: +// - The number of cells in the backing buffer filled between these two iterators. +ptrdiff_t OutputCellIterator::GetCellDistance(OutputCellIterator other) const noexcept +{ + return _distance - other._distance; +} diff --git a/src/buffer/out/OutputCellIterator.hpp b/src/buffer/out/OutputCellIterator.hpp new file mode 100644 index 000000000..dc7bdc958 --- /dev/null +++ b/src/buffer/out/OutputCellIterator.hpp @@ -0,0 +1,124 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OutputCellIterator.hpp + +Abstract: +- Read-only view into an entire batch of data to be written into the output buffer. +- This is done for performance reasons (avoid heap allocs and copies). + +Author: +- Michael Niksa (MiNiksa) 06-Oct-2018 + +Revision History: +- Based on work from OutputCell.hpp/cpp by Austin Diviness (AustDi) + +--*/ + +#pragma once + +#include "TextAttribute.hpp" + +#include "OutputCell.hpp" +#include "OutputCellView.hpp" + +class OutputCellIterator final +{ +public: + using iterator_category = std::input_iterator_tag; + using value_type = OutputCellView; + using difference_type = ptrdiff_t; + using pointer = OutputCellView*; + using reference = OutputCellView&; + + OutputCellIterator(const wchar_t& wch, const size_t fillLimit = 0); + OutputCellIterator(const TextAttribute& attr, const size_t fillLimit = 0); + OutputCellIterator(const wchar_t& wch, const TextAttribute& attr, const size_t fillLimit = 0); + OutputCellIterator(const CHAR_INFO& charInfo, const size_t fillLimit = 0); + OutputCellIterator(const std::wstring_view utf16Text); + OutputCellIterator(const std::wstring_view utf16Text, const TextAttribute attribute); + OutputCellIterator(const std::basic_string_view legacyAttributes, const bool unused); + OutputCellIterator(const std::basic_string_view charInfos); + OutputCellIterator(const std::basic_string_view cells); + ~OutputCellIterator() = default; + + OutputCellIterator& operator=(const OutputCellIterator& it) = default; + + operator bool() const noexcept; + + ptrdiff_t GetCellDistance(OutputCellIterator other) const noexcept; + ptrdiff_t GetInputDistance(OutputCellIterator other) const noexcept; + friend ptrdiff_t operator-(OutputCellIterator one, OutputCellIterator two) = delete; + + OutputCellIterator& operator++(); + OutputCellIterator operator++(int); + + const OutputCellView& operator*() const; + const OutputCellView* operator->() const; + +private: + + enum class Mode + { + // Loose mode is where we're given text and attributes in a raw sort of form + // like while data is being inserted from an API call. + Loose, + + // Loose mode with only text is where we're given just text and we want + // to use the attribute already in the buffer when writing + LooseTextOnly, + + // Fill mode is where we were given one thing and we just need to keep giving + // that back over and over for eternity. + Fill, + + // Given a run of legacy attributes, convert each of them and insert only attribute data. + LegacyAttr, + + // CharInfo mode is where we've been given a pair of text and attribute for each + // cell in the legacy format from an API call. + CharInfo, + + // Cell mode is where we have an already fully structured cell data usually + // from accessing/copying data already put into the OutputBuffer. + Cell, + }; + Mode _mode; + + std::basic_string_view _legacyAttrs; + + std::variant< + std::wstring_view, + std::basic_string_view, + std::basic_string_view, + std::monostate> _run; + + TextAttribute _attr; + + bool _TryMoveTrailing(); + + static OutputCellView s_GenerateView(const std::wstring_view view); + + static OutputCellView s_GenerateView(const std::wstring_view view, + const TextAttribute attr); + + static OutputCellView s_GenerateView(const std::wstring_view view, + const TextAttribute attr, + const TextAttributeBehavior behavior); + + static OutputCellView s_GenerateView(const wchar_t& wch); + static OutputCellView s_GenerateViewLegacyAttr(const WORD& legacyAttr); + static OutputCellView s_GenerateView(const TextAttribute& attr); + static OutputCellView s_GenerateView(const wchar_t& wch, const TextAttribute& attr); + static OutputCellView s_GenerateView(const CHAR_INFO& charInfo); + + static OutputCellView s_GenerateView(const OutputCell& cell); + + OutputCellView _currentView; + + size_t _pos; + size_t _distance; + size_t _fillLimit; +}; diff --git a/src/buffer/out/OutputCellRect.cpp b/src/buffer/out/OutputCellRect.cpp new file mode 100644 index 000000000..444e39479 --- /dev/null +++ b/src/buffer/out/OutputCellRect.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "OutputCellRect.hpp" + +// Routine Description: +// - Constucts an empty in-memory region for holding output buffer cell data. +OutputCellRect::OutputCellRect() : + _rows(0), + _cols(0) +{ + +} + +// Routine Description: +// - Constructs an in-memory region for holding a copy of output buffer cell data. +// - NOTE: This creatively skips the constructors for every cell. You must fill +// every single cell in this rectangle before iterating/reading for it to be valid. +// - NOTE: This is designed for perf-sensitive paths ONLY. Take care. +// Arguments: +// - rows - Rows in the rectangle (height) +// - cols - Columns in the rectangle (width) +OutputCellRect::OutputCellRect(const size_t rows, const size_t cols) : + _rows(rows), + _cols(cols) +{ + size_t totalCells; + THROW_IF_FAILED(SizeTMult(rows, cols, &totalCells)); + + _storage.resize(totalCells); +} + +// Routine Description: +// - Gets a read/write span over a single row inside the rectangle. +// Arguments: +// - row - The Y position or row index in the buffer. +// Return Value: +// - Read/write span of OutputCells +gsl::span OutputCellRect::GetRow(const size_t row) +{ + return gsl::span(_FindRowOffset(row), _cols); +} + +// Routine Description: +// - Gets a read-only iterator view over a single row of the rectangle. +// Arguments: +// - row - The Y position or row index in the buffer. +// Return Value: +// - Read-only iterator of OutputCells +OutputCellIterator OutputCellRect::GetRowIter(const size_t row) const +{ + const std::basic_string_view view(_FindRowOffset(row), _cols); + + return OutputCellIterator(view); +} + +// Routine Description: +// - Internal helper to find the pointer to the specific row offset in the giant +// contiguous block of memory allocated for this rectangle. +// Arguments: +// - row - The Y position or row index in the buffer. +// Return Value: +// - Pointer to the location in the rectangle that represents the start of the requested row. +OutputCell* OutputCellRect::_FindRowOffset(const size_t row) +{ + return (_storage.data() + (row * _cols)); +} + +// Routine Description: +// - Internal helper to find the pointer to the specific row offset in the giant +// contiguous block of memory allocated for this rectangle. +// Arguments: +// - row - The Y position or row index in the buffer. +// Return Value: +// - Pointer to the location in the rectangle that represents the start of the requested row. +const OutputCell* OutputCellRect::_FindRowOffset(const size_t row) const +{ + return (_storage.data() + (row * _cols)); +} + +// Routine Description: +// - Gets the height of the rectangle +// Return Value: +// - Height +size_t OutputCellRect::Height() const noexcept +{ + return _rows; +} + +// Routine Description: +// - Gets the width of the rectangle +// Return Value: +// - Width +size_t OutputCellRect::Width() const noexcept +{ + return _cols; +} diff --git a/src/buffer/out/OutputCellRect.hpp b/src/buffer/out/OutputCellRect.hpp new file mode 100644 index 000000000..9d6b05b6c --- /dev/null +++ b/src/buffer/out/OutputCellRect.hpp @@ -0,0 +1,49 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OutputCellRect.hpp + +Abstract: +- Designed to hold a rectangular area of OutputCells where the column/row count is known ahead of time. +- This is done for performance reasons (one big heap allocation block with appropriate views instead of tiny allocations.) +- NOTE: For cases where the internal buffer will not change during your call, use Iterators and Views to completely + avoid any copy or allocate at all. Only use this when a copy of your content or the buffer is needed. + +Author: +- Michael Niksa (MiNiksa) 12-Oct-2018 + +Revision History: +- Based on work from OutputCell.hpp/cpp by Austin Diviness (AustDi) + +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" +#include "TextAttribute.hpp" +#include "OutputCell.hpp" +#include "OutputCellIterator.hpp" + +class OutputCellRect final +{ +public: + OutputCellRect(); + OutputCellRect(const size_t rows, const size_t cols); + + gsl::span GetRow(const size_t row); + OutputCellIterator GetRowIter(const size_t row) const; + + size_t Height() const noexcept; + size_t Width() const noexcept; + +private: + std::vector _storage; + + OutputCell* _FindRowOffset(const size_t row); + const OutputCell* _FindRowOffset(const size_t row) const; + + size_t _cols; + size_t _rows; +}; diff --git a/src/buffer/out/OutputCellView.cpp b/src/buffer/out/OutputCellView.cpp new file mode 100644 index 000000000..b5de8a823 --- /dev/null +++ b/src/buffer/out/OutputCellView.cpp @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "OutputCellView.hpp" + +// Routine Description: +// - Constructs a read-only view of data formatted as a single output buffer cell +// Arguments: +// - view - String data for the text displayed on screen +// - dbcsAttr - Describes column width information (double byte character data) +// - textAttr - Describes color and formatting data +// - behavior - Describes where to retrieve color/format data. From this view? From defaults? etc. +OutputCellView::OutputCellView(const std::wstring_view view, + const DbcsAttribute dbcsAttr, + const TextAttribute textAttr, + const TextAttributeBehavior behavior) : + _view(view), + _dbcsAttr(dbcsAttr), + _textAttr(textAttr), + _behavior(behavior) +{ + +} + +// Routine Description: +// - Returns reference to view over text data +// Return Value: +// - Reference to UTF-16 character data +const std::wstring_view& OutputCellView::Chars() const noexcept +{ + return _view; +} + +// Routine Description: +// - Reports how many columns we expect the Chars() text data to consume +// Return Value: +// - Count of column cells on the screen +size_t OutputCellView::Columns() const noexcept +{ + if (DbcsAttr().IsSingle()) + { + return 1; + } + else if (DbcsAttr().IsLeading()) + { + return 2; + } + else if (DbcsAttr().IsTrailing()) + { + return 1; + } + + return 1; +} + +// Routine Description: +// - Retrieves character cell width data +// Return Value: +// - DbcsAttribute data +DbcsAttribute OutputCellView::DbcsAttr() const noexcept +{ + return _dbcsAttr; +} + +// Routine Description: +// - Retrieves text color/formatting information +// Return Value: +// - TextAttribute with encoded formatting data +TextAttribute OutputCellView::TextAttr() const noexcept +{ + return _textAttr; +} + +// Routine Description: +// - Retrieves behavior for inserting this cell into the buffer. See enum for details. +// Return Value: +// - TextAttributeBehavior enum value +TextAttributeBehavior OutputCellView::TextAttrBehavior() const noexcept +{ + return _behavior; +} + +// Routine Description: +// - Compares two views +// Arguments: +// - it - Other view to compare to this one +// Return Value: +// - True if all contents/references are equal. False otherwise. +bool OutputCellView::operator==(const OutputCellView& it) const noexcept +{ + return _view == it._view && + _dbcsAttr == it._dbcsAttr && + _textAttr == it._textAttr && + _behavior == it._behavior; +} + +// Routine Description: +// - Compares two views for inequality +// Arguments: +// - it - Other view to compare tot his one. +// Return Value: +// - True if any contents or references are inequal. False if they're all equal. +bool OutputCellView::operator!=(const OutputCellView& it) const noexcept +{ + return !(*this == it); +} diff --git a/src/buffer/out/OutputCellView.hpp b/src/buffer/out/OutputCellView.hpp new file mode 100644 index 000000000..470222a0c --- /dev/null +++ b/src/buffer/out/OutputCellView.hpp @@ -0,0 +1,48 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OutputCellView.hpp + +Abstract: +- Read-only view into a single cell of data that someone is attempting to write into the output buffer. +- This is done for performance reasons (avoid heap allocs and copies). + +Author: +- Michael Niksa (MiNiksa) 06-Oct-2018 + +Revision History: +- Based on work from OutputCell.hpp/cpp by Austin Diviness (AustDi) + +--*/ + +#pragma once + +#include "DbcsAttribute.hpp" +#include "TextAttribute.hpp" + +class OutputCellView +{ +public: + + OutputCellView(const std::wstring_view view, + const DbcsAttribute dbcsAttr, + const TextAttribute textAttr, + const TextAttributeBehavior behavior); + + const std::wstring_view& Chars() const noexcept; + size_t Columns() const noexcept; + DbcsAttribute DbcsAttr() const noexcept; + TextAttribute TextAttr() const noexcept; + TextAttributeBehavior TextAttrBehavior() const noexcept; + + bool operator==(const OutputCellView& view) const noexcept; + bool operator!=(const OutputCellView& view) const noexcept; + +private: + std::wstring_view _view; + DbcsAttribute _dbcsAttr; + TextAttribute _textAttr; + TextAttributeBehavior _behavior; +}; diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp new file mode 100644 index 000000000..e3a32f359 --- /dev/null +++ b/src/buffer/out/Row.cpp @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "Row.hpp" +#include "CharRow.hpp" +#include "textBuffer.hpp" +#include "../types/inc/convert.hpp" + +// Routine Description: +// - constructor +// Arguments: +// - rowId - the row index in the text buffer +// - rowWidth - the width of the row, cell elements +// - fillAttribute - the default text attribute +// - pParent - the text buffer that this row belongs to +// Return Value: +// - constructed object +ROW::ROW(const SHORT rowId, const short rowWidth, const TextAttribute fillAttribute, TextBuffer* const pParent) : + _id{ rowId }, + _rowWidth{ gsl::narrow(rowWidth) }, + _charRow{ gsl::narrow(rowWidth), this }, + _attrRow{ gsl::narrow(rowWidth), fillAttribute }, + _pParent{ pParent } +{ +} + +size_t ROW::size() const noexcept +{ + return _rowWidth; +} + +const CharRow& ROW::GetCharRow() const +{ + return _charRow; +} + +CharRow& ROW::GetCharRow() +{ + return const_cast(static_cast(this)->GetCharRow()); +} + +const ATTR_ROW& ROW::GetAttrRow() const noexcept +{ + return _attrRow; +} + +ATTR_ROW& ROW::GetAttrRow() noexcept +{ + return const_cast(static_cast(this)->GetAttrRow()); +} + +SHORT ROW::GetId() const noexcept +{ + return _id; +} + +void ROW::SetId(const SHORT id) noexcept +{ + _id = id; +} + +// Routine Description: +// - Sets all properties of the ROW to default values +// Arguments: +// - Attr - The default attribute (color) to fill +// Return Value: +// - +bool ROW::Reset(const TextAttribute Attr) +{ + _charRow.Reset(); + try + { + _attrRow.Reset(Attr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return false; + } + return true; +} + +// Routine Description: +// - resizes ROW to new width +// Arguments: +// - width - the new width, in cells +// Return Value: +// - S_OK if successful, otherwise relevant error +[[nodiscard]] +HRESULT ROW::Resize(const size_t width) +{ + RETURN_IF_FAILED(_charRow.Resize(width)); + try + { + _attrRow.Resize(width); + } + CATCH_RETURN(); + + _rowWidth = width; + + return S_OK; +} + +// Routine Description: +// - clears char data in column in row +// Arguments: +// - column - 0-indexed column index +// Return Value: +// - +void ROW::ClearColumn(const size_t column) +{ + THROW_HR_IF(E_INVALIDARG, column >= _charRow.size()); + _charRow.ClearCell(column); +} + +// Routine Description: +// - gets the text of the row as it would be shown on the screen +// Return Value: +// - wstring containing text for the row +std::wstring ROW::GetText() const +{ + return _charRow.GetText(); +} + +RowCellIterator ROW::AsCellIter(const size_t startIndex) const +{ + return AsCellIter(startIndex, size() - startIndex); +} + +RowCellIterator ROW::AsCellIter(const size_t startIndex, const size_t count) const +{ + return RowCellIterator(*this, startIndex, count); +} + +UnicodeStorage& ROW::GetUnicodeStorage() +{ + return _pParent->GetUnicodeStorage(); +} + +const UnicodeStorage& ROW::GetUnicodeStorage() const +{ + return _pParent->GetUnicodeStorage(); +} + +// Routine Description: +// - writes cell data to the row +// Arguments: +// - it - custom console iterator to use for seeking input data. bool() false when it becomes invalid while seeking. +// - index - column in row to start writing at +// - setWrap - set the wrap flags if we hit the end of the row while writing and there's still more data in the iterator. +// - limitRight - right inclusive column ID for the last write in this row. (optional, will just write to the end of row if nullopt) +// Return Value: +// - iterator to first cell that was not written to this row. +OutputCellIterator ROW::WriteCells(OutputCellIterator it, const size_t index, const bool setWrap, std::optional limitRight) +{ + THROW_HR_IF(E_INVALIDARG, index >= _charRow.size()); + THROW_HR_IF(E_INVALIDARG, limitRight.value_or(0) >= _charRow.size()); + size_t currentIndex = index; + + // If we're given a right-side column limit, use it. Otherwise, the write limit is the final column index available in the char row. + const auto finalColumnInRow = limitRight.value_or(_charRow.size() - 1); + + while (it && currentIndex <= finalColumnInRow) + { + // Fill the color if the behavior isn't set to keeping the current color. + if (it->TextAttrBehavior() != TextAttributeBehavior::Current) + { + const TextAttributeRun attrRun{ 1, it->TextAttr() }; + LOG_IF_FAILED(_attrRow.InsertAttrRuns({ &attrRun, 1 }, + currentIndex, + currentIndex, + _charRow.size())); + } + + // Fill the text if the behavior isn't set to saying there's only a color stored in this iterator. + if (it->TextAttrBehavior() != TextAttributeBehavior::StoredOnly) + { + const bool fillingLastColumn = currentIndex == finalColumnInRow; + + // TODO: MSFT: 19452170 - We need to ensure when writing any trailing byte that the one to the left + // is a matching leading byte. Likewise, if we're writing a leading byte, we need to make sure we still have space in this loop + // for the trailing byte coming up before writing it. + + // If we're trying to fill the first cell with a trailing byte, pad it out instead by clearing it. + // Don't increment iterator. We'll advance the index and try again with this value on the next round through the loop. + if (currentIndex == 0 && it->DbcsAttr().IsTrailing()) + { + _charRow.ClearCell(currentIndex); + } + // If we're trying to fill the last cell with a leading byte, pad it out instead by clearing it. + // Don't increment iterator. We'll exit because we couldn't write a lead at the end of a line. + else if (fillingLastColumn && it->DbcsAttr().IsLeading()) + { + _charRow.ClearCell(currentIndex); + _charRow.SetDoubleBytePadded(true); + } + // Otherwise, copy the data given and increment the iterator. + else + { + _charRow.DbcsAttrAt(currentIndex) = it->DbcsAttr(); + _charRow.GlyphAt(currentIndex) = it->Chars(); + ++it; + } + + // If we're asked to set the wrap status and we just filled the last column with some text, set wrap status on the row. + if (setWrap && fillingLastColumn) + { + _charRow.SetWrapForced(true); + } + } + else + { + ++it; + } + + // Move to the next cell for the next time through the loop. + ++currentIndex; + } + + return it; +} diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp new file mode 100644 index 000000000..697fad812 --- /dev/null +++ b/src/buffer/out/Row.hpp @@ -0,0 +1,84 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Row.hpp + +Abstract: +- data structure for information associated with one row of screen buffer + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "AttrRow.hpp" +#include "OutputCell.hpp" +#include "OutputCellIterator.hpp" +#include "CharRow.hpp" +#include "RowCellIterator.hpp" +#include "UnicodeStorage.hpp" + +class TextBuffer; + +class ROW final +{ +public: + ROW(const SHORT rowId, const short rowWidth, const TextAttribute fillAttribute, TextBuffer* const pParent); + + size_t size() const noexcept; + + const CharRow& GetCharRow() const; + CharRow& GetCharRow(); + + const ATTR_ROW& GetAttrRow() const noexcept; + ATTR_ROW& GetAttrRow() noexcept; + + SHORT GetId() const noexcept; + void SetId(const SHORT id) noexcept; + + bool Reset(const TextAttribute Attr); + [[nodiscard]] + HRESULT Resize(const size_t width); + + void ClearColumn(const size_t column); + std::wstring GetText() const; + + RowCellIterator AsCellIter(const size_t startIndex) const; + RowCellIterator AsCellIter(const size_t startIndex, const size_t count) const; + + UnicodeStorage& GetUnicodeStorage(); + const UnicodeStorage& GetUnicodeStorage() const; + + OutputCellIterator WriteCells(OutputCellIterator it, const size_t index, const bool setWrap, std::optional limitRight = std::nullopt); + + friend bool operator==(const ROW& a, const ROW& b) noexcept; + +#ifdef UNIT_TESTING + friend class RowTests; +#endif + +private: + CharRow _charRow; + ATTR_ROW _attrRow; + SHORT _id; + size_t _rowWidth; + TextBuffer* _pParent; // non ownership pointer +}; + +inline bool operator==(const ROW& a, const ROW& b) noexcept +{ + return (a._charRow == b._charRow && + a._attrRow == b._attrRow && + a._rowWidth == b._rowWidth && + a._pParent == b._pParent && + a._id == b._id); +} diff --git a/src/buffer/out/RowCellIterator.cpp b/src/buffer/out/RowCellIterator.cpp new file mode 100644 index 000000000..2a681e1cf --- /dev/null +++ b/src/buffer/out/RowCellIterator.cpp @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "RowCellIterator.hpp" +#include "Row.hpp" + +#include "../../types/inc/convert.hpp" +#include "../../types/inc/Utf16Parser.hpp" + +RowCellIterator::RowCellIterator(const ROW& row, const size_t start, const size_t length) : + _row(row), + _start(start), + _length(length), + _pos(start), + _view(s_GenerateView(row, start)) +{ + +} + +RowCellIterator::operator bool() const noexcept +{ + // In lieu of using start and end, this custom iterator type simply becomes bool false + // when we run out of items to iterate over. + return _pos < (_start + _length); +} + +bool RowCellIterator::operator==(const RowCellIterator& it) const noexcept +{ + return _row == it._row && + _start == it._start && + _length == it._length && + _pos == it._pos; +} +bool RowCellIterator::operator!=(const RowCellIterator& it) const noexcept +{ + return !(*this == it); +} + +RowCellIterator& RowCellIterator::operator+=(const ptrdiff_t& movement) +{ + _pos += movement; + + return (*this); +} + +RowCellIterator& RowCellIterator::operator++() +{ + return this->operator+=(1); +} + +RowCellIterator RowCellIterator::operator++(int) +{ + auto temp(*this); + operator++(); + return temp; +} + +RowCellIterator RowCellIterator::operator+(const ptrdiff_t& movement) +{ + auto temp(*this); + temp += movement; + return temp; +} + +const OutputCellView& RowCellIterator::operator*() const +{ + return _view; +} + +const OutputCellView* RowCellIterator::operator->() const +{ + return &_view; +} + +// Routine Description: +// - Member function to update the view to the current position in the buffer with +// the data held on this object. +// Arguments: +// - +// Return Value: +// - +void RowCellIterator::_RefreshView() +{ + _view = s_GenerateView(_row, _pos); +} + +// Routine Description: +// - Static function to create a view. +// - It's pulled out statically so it can be used during construction with just the given +// variables (so OutputCellView doesn't need an empty default constructor) +// - This will infer the width of the glyph and apply the appropriate attributes to the view. +// Arguments: +// - view - View representing characters corresponding to a single glyph +// - attr - Color attributes to apply to the text +// Return Value: +// - Object representing the view into this cell +OutputCellView RowCellIterator::s_GenerateView(const ROW& row, + const size_t pos) +{ + const auto& charRow = row.GetCharRow(); + + const auto glyph = charRow.GlyphAt(pos); + const auto dbcsAttr = charRow.DbcsAttrAt(pos); + + const auto textAttr = row.GetAttrRow().GetAttrByColumn(pos); + const auto behavior = TextAttributeBehavior::Stored; + + return OutputCellView(glyph, dbcsAttr, textAttr, behavior); +} diff --git a/src/buffer/out/RowCellIterator.hpp b/src/buffer/out/RowCellIterator.hpp new file mode 100644 index 000000000..c386bb6af --- /dev/null +++ b/src/buffer/out/RowCellIterator.hpp @@ -0,0 +1,59 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- RowCellIterator.hpp + +Abstract: +- Read-only view into cells already in the input buffer. +- This is done for performance reasons (avoid heap allocs and copies). + +Author: +- Michael Niksa (MiNiksa) 12-Oct-2018 + +--*/ + +#pragma once + +#include "OutputCellView.hpp" +class ROW; + +class RowCellIterator final +{ +public: + using iterator_category = std::forward_iterator_tag; + using value_type = OutputCellView; + using difference_type = ptrdiff_t; + using pointer = OutputCellView*; + using reference = OutputCellView&; + + RowCellIterator(const ROW& row, const size_t start, const size_t length); + ~RowCellIterator() = default; + + RowCellIterator& operator=(const RowCellIterator& it) = default; + + operator bool() const noexcept; + + bool operator==(const RowCellIterator& it) const noexcept; + bool operator!=(const RowCellIterator& it) const noexcept; + + RowCellIterator& operator+=(const ptrdiff_t& movement); + RowCellIterator& operator++(); + RowCellIterator operator++(int); + RowCellIterator operator+(const ptrdiff_t& movement); + + const OutputCellView& operator*() const; + const OutputCellView* operator->() const; + +private: + const ROW& _row; + const size_t _start; + const size_t _length; + + size_t _pos; + OutputCellView _view; + + void _RefreshView(); + static OutputCellView s_GenerateView(const ROW& row, const size_t pos); +}; diff --git a/src/buffer/out/TextAttribute.cpp b/src/buffer/out/TextAttribute.cpp new file mode 100644 index 000000000..4e0003eec --- /dev/null +++ b/src/buffer/out/TextAttribute.cpp @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "TextAttribute.hpp" +#include "../../inc/conattrs.hpp" + +bool TextAttribute::IsLegacy() const noexcept +{ + return _foreground.IsLegacy() && _background.IsLegacy(); +} + +// Arguments: +// - None +// Return Value: +// - color that should be displayed as the foreground color +COLORREF TextAttribute::CalculateRgbForeground(std::basic_string_view colorTable, + COLORREF defaultFgColor, + COLORREF defaultBgColor) const +{ + return _IsReverseVideo() ? _GetRgbBackground(colorTable, defaultBgColor) : _GetRgbForeground(colorTable, defaultFgColor); +} + +// Routine Description: +// - Calculates rgb background color based off of current color table and active modification attributes +// Arguments: +// - None +// Return Value: +// - color that should be displayed as the background color +COLORREF TextAttribute::CalculateRgbBackground(std::basic_string_view colorTable, + COLORREF defaultFgColor, + COLORREF defaultBgColor) const +{ + return _IsReverseVideo() ? _GetRgbForeground(colorTable, defaultFgColor) : _GetRgbBackground(colorTable, defaultBgColor); +} + +// Routine Description: +// - gets rgb foreground color, possibly based off of current color table. Does not take active modification +// attributes into account +// Arguments: +// - None +// Return Value: +// - color that is stored as the foreground color +COLORREF TextAttribute::_GetRgbForeground(std::basic_string_view colorTable, + COLORREF defaultColor) const +{ + return _foreground.GetColor(colorTable, defaultColor, _isBold); +} + +// Routine Description: +// - gets rgb background color, possibly based off of current color table. Does not take active modification +// attributes into account +// Arguments: +// - None +// Return Value: +// - color that is stored as the background color +COLORREF TextAttribute::_GetRgbBackground(std::basic_string_view colorTable, + COLORREF defaultColor) const +{ + return _background.GetColor(colorTable, defaultColor, false); +} + +void TextAttribute::SetMetaAttributes(const WORD wMeta) noexcept +{ + WI_UpdateFlagsInMask(_wAttrLegacy, META_ATTRS, wMeta); + WI_ClearAllFlags(_wAttrLegacy, COMMON_LVB_SBCSDBCS); +} + +WORD TextAttribute::GetMetaAttributes() const noexcept +{ + WORD wMeta = _wAttrLegacy; + WI_ClearAllFlags(wMeta, FG_ATTRS); + WI_ClearAllFlags(wMeta, BG_ATTRS); + WI_ClearAllFlags(wMeta, COMMON_LVB_SBCSDBCS); + return wMeta; +} + +void TextAttribute::SetForeground(const COLORREF rgbForeground) +{ + _foreground = TextColor(rgbForeground); +} + +void TextAttribute::SetBackground(const COLORREF rgbBackground) +{ + _background = TextColor(rgbBackground); +} + +void TextAttribute::SetFromLegacy(const WORD wLegacy) noexcept +{ + _wAttrLegacy = static_cast(wLegacy & META_ATTRS); + WI_ClearAllFlags(_wAttrLegacy, COMMON_LVB_SBCSDBCS); + BYTE fgIndex = static_cast(wLegacy & FG_ATTRS); + BYTE bgIndex = static_cast(wLegacy & BG_ATTRS) >> 4; + _foreground = TextColor(fgIndex); + _background = TextColor(bgIndex); +} + +void TextAttribute::SetLegacyAttributes(const WORD attrs, + const bool setForeground, + const bool setBackground, + const bool setMeta) +{ + if (setForeground) + { + BYTE fgIndex = (BYTE)(attrs & FG_ATTRS); + _foreground = TextColor(fgIndex); + } + if (setBackground) + { + BYTE bgIndex = (BYTE)(attrs & BG_ATTRS) >> 4; + _background = TextColor(bgIndex); + } + if (setMeta) + { + SetMetaAttributes(attrs); + } +} + +// Method Description: +// - Sets the foreground and/or background to a particular index in the 256color +// table. If either parameter is nullptr, it's ignored. +// This method can be used to set the colors to indexes in the range [0, 255], +// as opposed to SetLegacyAttributes, which clamps them to [0,15] +// Arguments: +// - foreground: nullptr if we should ignore this attr, else a pointer to a byte +// value to use as an index into the 256-color table. +// - background: nullptr if we should ignore this attr, else a pointer to a byte +// value to use as an index into the 256-color table. +// Return Value: +// - +void TextAttribute::SetIndexedAttributes(const std::optional foreground, + const std::optional background) noexcept +{ + if (foreground) + { + BYTE fgIndex = (*foreground) & 0xFF; + _foreground = TextColor(fgIndex); + } + if (background) + { + BYTE bgIndex = (*background) & 0xFF; + _background = TextColor(bgIndex); + } +} + +void TextAttribute::SetColor(const COLORREF rgbColor, const bool fIsForeground) +{ + if (fIsForeground) + { + SetForeground(rgbColor); + } + else + { + SetBackground(rgbColor); + } +} + +bool TextAttribute::IsBold() const noexcept +{ + return _isBold; +} + +bool TextAttribute::_IsReverseVideo() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO); +} + +bool TextAttribute::IsLeadingByte() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_LEADING_BYTE); +} + +bool TextAttribute::IsTrailingByte() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_LEADING_BYTE); +} + +bool TextAttribute::IsTopHorizontalDisplayed() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_HORIZONTAL); +} + +bool TextAttribute::IsBottomHorizontalDisplayed() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_UNDERSCORE); +} + +bool TextAttribute::IsLeftVerticalDisplayed() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_LVERTICAL); +} + +bool TextAttribute::IsRightVerticalDisplayed() const noexcept +{ + return WI_IsFlagSet(_wAttrLegacy, COMMON_LVB_GRID_RVERTICAL); +} + +void TextAttribute::SetLeftVerticalDisplayed(const bool isDisplayed) noexcept +{ + WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_LVERTICAL, isDisplayed); +} + +void TextAttribute::SetRightVerticalDisplayed(const bool isDisplayed) noexcept +{ + WI_UpdateFlag(_wAttrLegacy, COMMON_LVB_GRID_RVERTICAL, isDisplayed); +} + +void TextAttribute::Embolden() noexcept +{ + _SetBoldness(true); +} + +void TextAttribute::Debolden() noexcept +{ + _SetBoldness(false); +} + +// Routine Description: +// - swaps foreground and background color +void TextAttribute::Invert() noexcept +{ + WI_ToggleFlag(_wAttrLegacy, COMMON_LVB_REVERSE_VIDEO); +} + +void TextAttribute::_SetBoldness(const bool isBold) noexcept +{ + _isBold = isBold; +} + +void TextAttribute::SetDefaultForeground() noexcept +{ + _foreground = TextColor(); +} + +void TextAttribute::SetDefaultBackground() noexcept +{ + _background = TextColor(); +} + +// Method Description: +// - Returns true if this attribute indicates it's foreground is the "default" +// foreground. It's _rgbForeground will contain the actual value of the +// default foreground. If the default colors are ever changed, this method +// should be used to identify attributes with the default fg value, and +// update them accordingly. +// Arguments: +// - +// Return Value: +// - true iff this attribute indicates it's the "default" foreground color. +bool TextAttribute::ForegroundIsDefault() const noexcept +{ + return _foreground.IsDefault(); +} + +// Method Description: +// - Returns true if this attribute indicates it's background is the "default" +// background. It's _rgbBackground will contain the actual value of the +// default background. If the default colors are ever changed, this method +// should be used to identify attributes with the default bg value, and +// update them accordingly. +// Arguments: +// - +// Return Value: +// - true iff this attribute indicates it's the "default" background color. +bool TextAttribute::BackgroundIsDefault() const noexcept +{ + return _background.IsDefault(); +} diff --git a/src/buffer/out/TextAttribute.hpp b/src/buffer/out/TextAttribute.hpp new file mode 100644 index 000000000..5d5485b6d --- /dev/null +++ b/src/buffer/out/TextAttribute.hpp @@ -0,0 +1,235 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TextAttribute.hpp + +Abstract: +- contains data structure for run-length-encoding of text attribute data + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +- Pulled each of the fg/bg colors into their own abstraction (migrie, Nov 2018) +--*/ + +#pragma once +#include "TextColor.h" +#include "../../inc/conattrs.hpp" + +#ifdef UNIT_TESTING +#include "WexTestClass.h" +#endif + +class TextAttribute final +{ +public: + constexpr TextAttribute() noexcept : + _wAttrLegacy{ 0 }, + _foreground{}, + _background{}, + _isBold{ false } + { + } + + constexpr TextAttribute(const WORD wLegacyAttr) noexcept : + _wAttrLegacy{ static_cast(wLegacyAttr & META_ATTRS) }, + _foreground{ static_cast(wLegacyAttr & FG_ATTRS) }, + _background{ static_cast((wLegacyAttr & BG_ATTRS) >> 4) }, + _isBold{ false } + { + // If we're given lead/trailing byte information with the legacy color, strip it. + WI_ClearAllFlags(_wAttrLegacy, COMMON_LVB_SBCSDBCS); + } + + constexpr TextAttribute(const COLORREF rgbForeground, + const COLORREF rgbBackground) noexcept : + _wAttrLegacy{ 0 }, + _foreground{ rgbForeground }, + _background{ rgbBackground }, + _isBold{ false } + { + } + + constexpr WORD GetLegacyAttributes() const noexcept + { + BYTE fg = (_foreground.GetIndex() & FG_ATTRS); + BYTE bg = (_background.GetIndex() << 4) & BG_ATTRS; + WORD meta = (_wAttrLegacy & META_ATTRS); + return (fg | bg | meta) | (_isBold ? FOREGROUND_INTENSITY : 0); + } + + // Method Description: + // - Returns a WORD with legacy-style attributes for this textattribute. + // If either the foreground or background of this textattribute is not + // a legacy attribute, then instead use the provided default index as + // the value for that component. + // Arguments: + // - defaultFgIndex: the BYTE to use as the index for the foreground, should + // the foreground not be a legacy style attribute. + // - defaultBgIndex: the BYTE to use as the index for the backgound, should + // the background not be a legacy style attribute. + // Return Value: + // - a WORD with legacy-style attributes for this textattribute. + constexpr WORD GetLegacyAttributes(const BYTE defaultFgIndex, + const BYTE defaultBgIndex) const noexcept + { + BYTE fgIndex = _foreground.IsLegacy() ? _foreground.GetIndex() : defaultFgIndex; + BYTE bgIndex = _background.IsLegacy() ? _background.GetIndex() : defaultBgIndex; + BYTE fg = (fgIndex & FG_ATTRS); + BYTE bg = (bgIndex << 4) & BG_ATTRS; + WORD meta = (_wAttrLegacy & META_ATTRS); + return (fg | bg | meta) | (_isBold ? FOREGROUND_INTENSITY : 0); + } + + COLORREF CalculateRgbForeground(std::basic_string_view colorTable, + COLORREF defaultFgColor, + COLORREF defaultBgColor) const; + COLORREF CalculateRgbBackground(std::basic_string_view colorTable, + COLORREF defaultFgColor, + COLORREF defaultBgColor) const; + + bool IsLeadingByte() const noexcept; + bool IsTrailingByte() const noexcept; + bool IsTopHorizontalDisplayed() const noexcept; + bool IsBottomHorizontalDisplayed() const noexcept; + bool IsLeftVerticalDisplayed() const noexcept; + bool IsRightVerticalDisplayed() const noexcept; + + void SetLeftVerticalDisplayed(const bool isDisplayed) noexcept; + void SetRightVerticalDisplayed(const bool isDisplayed) noexcept; + + void SetFromLegacy(const WORD wLegacy) noexcept; + + void SetLegacyAttributes(const WORD attrs, + const bool setForeground, + const bool setBackground, + const bool setMeta); + + void SetIndexedAttributes(const std::optional foreground, + const std::optional background) noexcept; + + void SetMetaAttributes(const WORD wMeta) noexcept; + WORD GetMetaAttributes() const noexcept; + + void Embolden() noexcept; + void Debolden() noexcept; + + void Invert() noexcept; + + friend constexpr bool operator==(const TextAttribute& a, const TextAttribute& b) noexcept; + friend constexpr bool operator!=(const TextAttribute& a, const TextAttribute& b) noexcept; + friend constexpr bool operator==(const TextAttribute& attr, const WORD& legacyAttr) noexcept; + friend constexpr bool operator!=(const TextAttribute& attr, const WORD& legacyAttr) noexcept; + friend constexpr bool operator==(const WORD& legacyAttr, const TextAttribute& attr) noexcept; + friend constexpr bool operator!=(const WORD& legacyAttr, const TextAttribute& attr) noexcept; + + bool IsLegacy() const noexcept; + bool IsBold() const noexcept; + + void SetForeground(const COLORREF rgbForeground); + void SetBackground(const COLORREF rgbBackground); + void SetColor(const COLORREF rgbColor, const bool fIsForeground); + + void SetDefaultForeground() noexcept; + void SetDefaultBackground() noexcept; + + bool ForegroundIsDefault() const noexcept; + bool BackgroundIsDefault() const noexcept; + + constexpr bool IsRgb() const noexcept + { + return _foreground.IsRgb() || _background.IsRgb(); + } + +private: + COLORREF _GetRgbForeground(std::basic_string_view colorTable, + COLORREF defaultColor) const; + COLORREF _GetRgbBackground(std::basic_string_view colorTable, + COLORREF defaultColor) const; + bool _IsReverseVideo() const noexcept; + void _SetBoldness(const bool isBold) noexcept; + + WORD _wAttrLegacy; + TextColor _foreground; + TextColor _background; + bool _isBold; + +#ifdef UNIT_TESTING + friend class TextBufferTests; + friend class TextAttributeTests; + template friend class WEX::TestExecution::VerifyOutputTraits; +#endif +}; + +enum class TextAttributeBehavior +{ + Stored, // use contained text attribute + Current, // use text attribute of cell being written to + StoredOnly, // only use the contained text attribute and skip the insertion of anything else +}; + +constexpr bool operator==(const TextAttribute& a, const TextAttribute& b) noexcept +{ + return a._wAttrLegacy == b._wAttrLegacy && + a._foreground == b._foreground && + a._background == b._background && + a._isBold == b._isBold; +} + +constexpr bool operator!=(const TextAttribute& a, const TextAttribute& b) noexcept +{ + return !(a == b); +} + +constexpr bool operator==(const TextAttribute& attr, const WORD& legacyAttr) noexcept +{ + return attr.GetLegacyAttributes() == legacyAttr; +} + +constexpr bool operator!=(const TextAttribute& attr, const WORD& legacyAttr) noexcept +{ + return !(attr == legacyAttr); +} + +constexpr bool operator==(const WORD& legacyAttr, const TextAttribute& attr) noexcept +{ + return attr == legacyAttr; +} + +constexpr bool operator!=(const WORD& legacyAttr, const TextAttribute& attr) noexcept +{ + return !(attr == legacyAttr); +} + +#ifdef UNIT_TESTING + +#define LOG_ATTR(attr) (Log::Comment(NoThrowString().Format(\ + L#attr L"=%s", VerifyOutputTraits::ToString(attr).GetBuffer()))) + +namespace WEX { + namespace TestExecution { + template<> + class VerifyOutputTraits < TextAttribute > + { + public: + static WEX::Common::NoThrowString ToString(const TextAttribute& attr) + { + return WEX::Common::NoThrowString().Format( + L"{FG:%s,BG:%s,bold:%d,wLegacy:(0x%04x)}", + VerifyOutputTraits::ToString(attr._foreground).GetBuffer(), + VerifyOutputTraits::ToString(attr._background).GetBuffer(), + attr.IsBold(), + attr._wAttrLegacy + ); + } + }; + } +} +#endif diff --git a/src/buffer/out/TextAttributeRun.cpp b/src/buffer/out/TextAttributeRun.cpp new file mode 100644 index 000000000..a2045eaae --- /dev/null +++ b/src/buffer/out/TextAttributeRun.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "TextAttributeRun.hpp" + +TextAttributeRun::TextAttributeRun() noexcept : + _cchLength(0) +{ + SetAttributes(TextAttribute(0)); +} + +TextAttributeRun::TextAttributeRun(const size_t cchLength, const TextAttribute attr) noexcept : + _cchLength(cchLength) +{ + SetAttributes(attr); +} + +size_t TextAttributeRun::GetLength() const noexcept +{ + return _cchLength; +} + +void TextAttributeRun::SetLength(const size_t cchLength) noexcept +{ + _cchLength = cchLength; +} + +void TextAttributeRun::IncrementLength() noexcept +{ + _cchLength++; +} + +void TextAttributeRun::DecrementLength() noexcept +{ + _cchLength--; +} + +const TextAttribute& TextAttributeRun::GetAttributes() const noexcept +{ + return _attributes; +} + +void TextAttributeRun::SetAttributes(const TextAttribute textAttribute) noexcept +{ + _attributes = textAttribute; +} + +// Routine Description: +// - Sets the attributes of this run to the given legacy attributes +// Arguments: +// - wNew - the new value for this run's attributes +// Return Value: +// +void TextAttributeRun::SetAttributesFromLegacy(const WORD wNew) noexcept +{ + _attributes.SetFromLegacy(wNew); +} diff --git a/src/buffer/out/TextAttributeRun.hpp b/src/buffer/out/TextAttributeRun.hpp new file mode 100644 index 000000000..ff0c91799 --- /dev/null +++ b/src/buffer/out/TextAttributeRun.hpp @@ -0,0 +1,47 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TextAttributeRun.hpp + +Abstract: +- contains data structure for run-length-encoding of text attribute data + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "TextAttribute.hpp" + +class TextAttributeRun final +{ +public: + TextAttributeRun() noexcept; + TextAttributeRun(const size_t cchLength, const TextAttribute attr) noexcept; + + size_t GetLength() const noexcept; + void SetLength(const size_t cchLength) noexcept; + void IncrementLength() noexcept; + void DecrementLength() noexcept; + + const TextAttribute& GetAttributes() const noexcept; + void SetAttributes(const TextAttribute textAttribute) noexcept; + void SetAttributesFromLegacy(const WORD wNew) noexcept; + +private: + size_t _cchLength; + TextAttribute _attributes; + +#ifdef UNIT_TESTING + friend class AttrRowTests; +#endif +}; diff --git a/src/buffer/out/TextColor.cpp b/src/buffer/out/TextColor.cpp new file mode 100644 index 000000000..90c6323dd --- /dev/null +++ b/src/buffer/out/TextColor.cpp @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "TextColor.h" + +// Method Description: +// - Sets the color value of this attribute, and sets this color to be an RGB +// attribute. +// Arguments: +// - rgbColor: the COLORREF containing the color information for this TextColor +// Return Value: +// - +void TextColor::SetColor(const COLORREF rgbColor) +{ + _meta = ColorType::IsRgb; + _red = GetRValue(rgbColor); + _green = GetGValue(rgbColor); + _blue = GetBValue(rgbColor); +} + +// Method Description: +// - Sets this TextColor to be a legacy-style index into the color table. +// Arguments: +// - index: the index of the colortable we should use for this TextColor. +// Return Value: +// - +void TextColor::SetIndex(const BYTE index) +{ + _meta = ColorType::IsIndex; + _index = index; +} + +// Method Description: +// - Sets this TextColor to be a default text color, who's appearance is +// controlled by the terminal's implementation of what a default color is. +// Arguments: +// - +// Return Value: +// - +void TextColor::SetDefault() +{ + _meta = ColorType::IsDefault; +} + +// Method Description: +// - Retrieve the real color value for this TextColor. +// * If we're an RGB color, we'll use that value. +// * If we're an indexed color table value, we'll use that index to look up +// our value in the provided color table. +// - If brighten is true, and the index is in the "dark" portion of the +// color table (indicies [0,7]), then we'll look up the bright version of +// this color (from indicies [8,15]). This should be true for +// TextAttributes that are "Bold" and we're treating bold as bright +// (which is the default behavior of most terminals.) +// * If we're a default color, we'll return the default color provided. +// Arguments: +// - colorTable: The table of colors we should use to look up the value of a +// legacy attribute from +// - defaultColor: The color value to use if we're a default attribute. +// - brighten: if true, we'll brighten a dark color table index. +// Return Value: +// - a COLORREF containing the real value of this TextColor. +COLORREF TextColor::GetColor(std::basic_string_view colorTable, + const COLORREF defaultColor, + bool brighten) const +{ + if (IsDefault()) + { + if (brighten) + { + FAIL_FAST_IF(colorTable.size() < 16); + // See MSFT:20266024 for context on this fix. + // Additionally todo MSFT:20271956 to fix this better for 19H2+ + // If we're a default color, check to see if the defaultColor exists + // in the dark section of the color table. If it does, then chances + // are we're not a separate default color, instead we're an index + // color being used as the default color + // (Settings::_DefaultForeground==INVALID_COLOR, and the index + // from _wFillAttribute is being used instead.) + // If we find a match, return instead the bright version of this color + for (size_t i = 0; i < 8; i++) + { + if (colorTable[i] == defaultColor) + { + return colorTable[i + 8]; + } + } + + } + + return defaultColor; + } + else if (IsRgb()) + { + return _GetRGB(); + } + else + { + FAIL_FAST_IF(colorTable.size() < _index); + // If the color is already bright (it's in index [8,15] or it's a + // 256color value [16,255], then boldness does nothing. + if (brighten && _index < 8) + { + FAIL_FAST_IF(colorTable.size() < 16); + FAIL_FAST_IF((size_t)(_index + 8) > (size_t)(colorTable.size())); + return colorTable[_index + 8]; + } + else + { + return colorTable[_index]; + } + } +} + +// Method Description: +// - Return a COLORREF containing our stored value. Will return garbage if this +//attribute is not a RGB attribute. +// Arguments: +// - +// Return Value: +// - a COLORREF containing our stored value +COLORREF TextColor::_GetRGB() const +{ + return RGB(_red, _green, _blue); +} diff --git a/src/buffer/out/TextColor.h b/src/buffer/out/TextColor.h new file mode 100644 index 000000000..52885b02d --- /dev/null +++ b/src/buffer/out/TextColor.h @@ -0,0 +1,169 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TextColor.h + +Abstract: +- contains data for a single color of the text. Text Attributes are composed of + two of these - one for the foreground and one for the background. + The color can be in one of three states: + * Default Colors - The terminal should use the terminal's notion of whatever + the default color should be for this component. + It's up to the terminal that's consuming this buffer to control the + behavior of default attributes. + Terminals typically have a pair of Default colors that are separate from + their color table. This component should use that value. + Consoles also can have a legacy table index as their default colors. + * Indexed Color - The terminal should use our value as an index into the + color table to retrieve the real value of the color. + This is the type of color that "legacy" 16-color attributes have. + * RGB color - We'll store a real color value in this attribute + +Author(s): +- Mike Griese (migrie) Nov 2018 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 +- Pulled into its own file from textBuffer.hpp/cpp (AustDi, 2017) +- Moved the colors into their own seperate abstraction. (migrie Nov 2018) +--*/ + +#pragma once + +#ifdef UNIT_TESTING +#include "WexTestClass.h" +#endif + +#pragma pack(push, 1) + +enum class ColorType : BYTE +{ + IsIndex = 0x0, + IsDefault = 0x1, + IsRgb = 0x2 +}; + +struct TextColor +{ +public: + + constexpr TextColor() noexcept : + _meta{ ColorType::IsDefault }, + _red{ 0 }, + _green{ 0 }, + _blue{ 0 } + { + } + + constexpr TextColor(const BYTE wLegacyAttr) noexcept : + _meta{ ColorType::IsIndex }, + _index{ wLegacyAttr }, + _green{ 0 }, + _blue{ 0 } + { + } + + constexpr TextColor(const COLORREF rgb) noexcept : + _meta{ ColorType::IsRgb }, + _red{ GetRValue(rgb) }, + _green{ GetGValue(rgb) }, + _blue{ GetBValue(rgb) } + { + } + + friend constexpr bool operator==(const TextColor& a, const TextColor& b) noexcept; + friend constexpr bool operator!=(const TextColor& a, const TextColor& b) noexcept; + + constexpr bool IsLegacy() const noexcept + { + return !(IsDefault() || IsRgb()); + } + + constexpr bool IsDefault() const noexcept + { + return _meta == ColorType::IsDefault; + } + + constexpr bool IsRgb() const noexcept + { + return _meta == ColorType::IsRgb; + } + + void SetColor(const COLORREF rgbColor); + void SetIndex(const BYTE index); + void SetDefault(); + + COLORREF GetColor(std::basic_string_view colorTable, + const COLORREF defaultColor, + const bool brighten) const; + + constexpr BYTE GetIndex() const noexcept + { + return _index; + } + + +private: + ColorType _meta : 2; + union + { + BYTE _red, _index; + }; + BYTE _green; + BYTE _blue; + + COLORREF _GetRGB() const; + +#ifdef UNIT_TESTING + friend class TextBufferTests; + template friend class WEX::TestExecution::VerifyOutputTraits; +#endif +}; + +#pragma pack(pop) + +bool constexpr operator==(const TextColor& a, const TextColor& b) noexcept +{ + return a._meta == b._meta && + a._red == b._red && + a._green == b._green && + a._blue == b._blue; +} + +bool constexpr operator!=(const TextColor& a, const TextColor& b) noexcept +{ + return !(a == b); +} + +#ifdef UNIT_TESTING + +namespace WEX { + namespace TestExecution { + template<> + class VerifyOutputTraits < TextColor > + { + public: + static WEX::Common::NoThrowString ToString(const TextColor& color) + { + if (color.IsDefault()) + { + return L"{default}"; + } + else if (color.IsRgb()) + { + return WEX::Common::NoThrowString().Format(L"{RGB:0x%06x}", color._GetRGB()); + } + else + { + return WEX::Common::NoThrowString().Format(L"{index:0x%04x}", color._red); + } + } + }; + } +} +#endif + +static_assert(sizeof(TextColor) <= 4*sizeof(BYTE), "We should only need 4B for an entire TextColor. Any more than that is just waste"); diff --git a/src/buffer/out/UnicodeStorage.cpp b/src/buffer/out/UnicodeStorage.cpp new file mode 100644 index 000000000..813cf3fd4 --- /dev/null +++ b/src/buffer/out/UnicodeStorage.cpp @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "UnicodeStorage.hpp" + +UnicodeStorage::UnicodeStorage() : + _map{} +{ +} + +// Routine Description: +// - fetches the text associated with key +// Arguments: +// - key - the key into the storage +// Return Value: +// - the glyph data associated with key +// Note: will throw exception if key is not stored yet +const UnicodeStorage::mapped_type& UnicodeStorage::GetText(const key_type key) const +{ + return _map.at(key); +} + +// Routine Description: +// - stores glyph data associated with key. +// Arguments: +// - key - the key into the storage +// - glyph - the glyph data to store +void UnicodeStorage::StoreGlyph(const key_type key, const mapped_type& glyph) +{ + _map.insert_or_assign(key, glyph); +} + +// Routine Description: +// - erases key and it's associated data from the storage +// Arguments: +// - key - the key to remove +void UnicodeStorage::Erase(const key_type key) noexcept +{ + _map.erase(key); +} + +// Routine Description: +// - Remaps all of the stored items to new coordinate positions +// based on a bulk rearrangement of row IDs and potential row width resize. +// Arguments: +// - rowMap - A map of the old row IDs to the new row IDs. +// - width - The width of the new row. Remove any items that are beyond the row width. +// - Use nullopt if we're not resizing the width of the row, just renumbering the rows. +void UnicodeStorage::Remap(const std::map& rowMap, const std::optional width) +{ + // Make a temporary map to hold all the new row positioning + std::unordered_map newMap; + + // Walk through every stored item. + for (const auto& pair : _map) + { + // Extract the old coordinate position + const auto oldCoord = pair.first; + + // Only try to short-circuit based on width if we were told it changed + // by being given a new width value. + if (width.has_value()) + { + // Get the column ID + const auto oldColId = oldCoord.X; + + // If the column index is at/beyond the row width, don't bother copying it to the new map. + if (oldColId >= width.value()) + { + continue; + } + } + + // Get the row ID from the position as that's what we need to remap + const auto oldRowId = oldCoord.Y; + + // Use the mapping given to convert the old row ID to the new row ID + const auto mapIter = rowMap.find(oldRowId); + + // If there's no mapping to a new row, don't bother copying it to the new map. The row is gone. + if (mapIter == rowMap.end()) + { + continue; + } + + const auto newRowId = mapIter->second; + + // Generate a new coordinate with the same X as the old one, but a new Y value. + const auto newCoord = COORD{ oldCoord.X, newRowId }; + + // Put the adjusted coordinate into the map with the original value. + newMap.emplace(newCoord, pair.second); + } + + // Swap into the stored map, free the temporary when we exit. + _map.swap(newMap); +} diff --git a/src/buffer/out/UnicodeStorage.hpp b/src/buffer/out/UnicodeStorage.hpp new file mode 100644 index 000000000..0df3e7eb3 --- /dev/null +++ b/src/buffer/out/UnicodeStorage.hpp @@ -0,0 +1,68 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- UnicodeStorage.hpp + +Abstract: +- dynamic storage location for glyphs that can't normally fit in the output buffer + +Author(s): +- Austin Diviness (AustDi) 02-May-2018 +--*/ + +#pragma once + +#include +#include +#include + +// std::unordered_map needs help to know how to hash a COORD +namespace std +{ + template <> + struct hash + { + + // Routine Description: + // - hashes a coord. coord will be hashed by storing the x and y values consecutively in the lower + // bits of a size_t. + // Arguments: + // - coord - the coord to hash + // Return Value: + // - the hashed coord + constexpr size_t operator()(const COORD& coord) const noexcept + { + size_t retVal = coord.Y; + const size_t xCoord = coord.X; + retVal |= xCoord << (sizeof(coord.Y) * CHAR_BIT); + return retVal; + } + }; +} + +class UnicodeStorage final +{ +public: + using key_type = typename COORD; + using mapped_type = typename std::vector; + + UnicodeStorage(); + + const mapped_type& GetText(const key_type key) const; + + void StoreGlyph(const key_type key, const mapped_type& glyph); + + void Erase(const key_type key) noexcept; + + void Remap(const std::map& rowMap, const std::optional width); + +private: + std::unordered_map _map; + +#ifdef UNIT_TESTING + friend class UnicodeStorageTests; + friend class TextBufferTests; +#endif +}; diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp new file mode 100644 index 000000000..e95e7eef2 --- /dev/null +++ b/src/buffer/out/cursor.cpp @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "cursor.h" +#include "TextBuffer.hpp" + +#pragma hdrstop + +// Routine Description: +// - Constructor to set default properties for Cursor +// Arguments: +// - ulSize - The height of the cursor within this buffer +Cursor::Cursor(const ULONG ulSize, TextBuffer& parentBuffer) : + _parentBuffer{ parentBuffer }, + _cPosition{ 0 }, + _fHasMoved(false), + _fIsVisible(true), + _fIsOn(true), + _fIsDouble(false), + _fBlinkingAllowed(true), + _fDelay(false), + _fIsConversionArea(false), + _fIsPopupShown(false), + _fDelayedEolWrap(false), + _coordDelayedAt{ 0 }, + _fDeferCursorRedraw(false), + _fHaveDeferredCursorRedraw(false), + _ulSize(ulSize), + _cursorType(CursorType::Legacy), + _fUseColor(false), + _color(s_InvertCursorColor) +{ +} + +Cursor::~Cursor() +{ +} + +COORD Cursor::GetPosition() const noexcept +{ + return _cPosition; +} + +bool Cursor::HasMoved() const noexcept +{ + return _fHasMoved; +} + +bool Cursor::IsVisible() const noexcept +{ + return _fIsVisible; +} + +bool Cursor::IsOn() const noexcept +{ + return _fIsOn; +} + +bool Cursor::IsBlinkingAllowed() const noexcept +{ + return _fBlinkingAllowed; +} + +bool Cursor::IsDouble() const noexcept +{ + return _fIsDouble; +} + +bool Cursor::IsConversionArea() const noexcept +{ + return _fIsConversionArea; +} + +bool Cursor::IsPopupShown() const noexcept +{ + return _fIsPopupShown; +} + +bool Cursor::GetDelay() const noexcept +{ + return _fDelay; +} + +ULONG Cursor::GetSize() const noexcept +{ + return _ulSize; +} + +void Cursor::SetHasMoved(const bool fHasMoved) +{ + _fHasMoved = fHasMoved; +} + +void Cursor::SetIsVisible(const bool fIsVisible) +{ + _fIsVisible = fIsVisible; + _RedrawCursor(); +} + +void Cursor::SetIsOn(const bool fIsOn) +{ + _fIsOn = fIsOn; + _RedrawCursorAlways(); +} + +void Cursor::SetBlinkingAllowed(const bool fBlinkingAllowed) +{ + _fBlinkingAllowed = fBlinkingAllowed; + _RedrawCursorAlways(); +} + +void Cursor::SetIsDouble(const bool fIsDouble) +{ + _fIsDouble = fIsDouble; + _RedrawCursor(); +} + +void Cursor::SetIsConversionArea(const bool fIsConversionArea) +{ + // Functionally the same as "Hide cursor" + // Never called with TRUE, it's only used in the creation of a + // ConversionAreaInfo, and never changed after that. + _fIsConversionArea = fIsConversionArea; + _RedrawCursorAlways(); +} + +void Cursor::SetIsPopupShown(const bool fIsPopupShown) +{ + // Functionally the same as "Hide cursor" + _fIsPopupShown = fIsPopupShown; + _RedrawCursorAlways(); +} + +void Cursor::SetDelay(const bool fDelay) +{ + _fDelay = fDelay; +} + +void Cursor::SetSize(const ULONG ulSize) +{ + _ulSize = ulSize; + _RedrawCursor(); +} + +void Cursor::SetStyle(const ULONG ulSize, const COLORREF color, const CursorType type) noexcept +{ + _ulSize = ulSize; + _color = color; + _cursorType = type; + + _RedrawCursor(); +} + +// Routine Description: +// - Sends a redraw message to the renderer only if the cursor is currently on. +// - NOTE: For use with most methods in this class. +// Arguments: +// - +// Return Value: +// - +void Cursor::_RedrawCursor() noexcept +{ + // Only trigger the redraw if we're on. + // Don't draw the cursor if this was triggered from a conversion area. + // (Conversion areas have cursors to mark the insertion point internally, but the user's actual cursor is the one on the primary screen buffer.) + if (IsOn() && !IsConversionArea()) + { + if (_fDeferCursorRedraw) + { + _fHaveDeferredCursorRedraw = true; + } + else + { + _RedrawCursorAlways(); + } + } +} + +// Routine Description: +// - Sends a redraw message to the renderer no matter what. +// - NOTE: For use with the method that turns the cursor on and off to force a refresh +// and clear the ON cursor from the screen. Not for use with other methods. +// They should use the other method so refreshes are suppressed while the cursor is off. +// Arguments: +// - +// Return Value: +// - +void Cursor::_RedrawCursorAlways() noexcept +{ + try + { + _parentBuffer.GetRenderTarget().TriggerRedrawCursor(&_cPosition); + } + CATCH_LOG(); +} + +void Cursor::SetPosition(const COORD cPosition) +{ + _RedrawCursor(); + _cPosition.X = cPosition.X; + _cPosition.Y = cPosition.Y; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::SetXPosition(const int NewX) +{ + _RedrawCursor(); + _cPosition.X = (SHORT)NewX; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::SetYPosition(const int NewY) +{ + _RedrawCursor(); + _cPosition.Y = (SHORT)NewY; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::IncrementXPosition(const int DeltaX) +{ + _RedrawCursor(); + _cPosition.X += (SHORT)DeltaX; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::IncrementYPosition(const int DeltaY) +{ + _RedrawCursor(); + _cPosition.Y += (SHORT)DeltaY; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::DecrementXPosition(const int DeltaX) +{ + _RedrawCursor(); + _cPosition.X -= (SHORT)DeltaX; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +void Cursor::DecrementYPosition(const int DeltaY) +{ + _RedrawCursor(); + _cPosition.Y -= (SHORT)DeltaY; + _RedrawCursor(); + ResetDelayEOLWrap(); +} + +/////////////////////////////////////////////////////////////////////////////// +// Routine Description: +// - Copies properties from another cursor into this one. +// - This is primarily to copy properties that would otherwise not be specified during CreateInstance +// - NOTE: As of now, this function is specifically used to handle the ResizeWithReflow operation. +// It will need modification for other future users. +// Arguments: +// - OtherCursor - The cursor to copy properties from +// Return Value: +// - +void Cursor::CopyProperties(const Cursor& OtherCursor) +{ + // We shouldn't copy the position as it will be already rearranged by the resize operation. + //_cPosition = pOtherCursor->_cPosition; + + _fHasMoved = OtherCursor._fHasMoved; + _fIsVisible = OtherCursor._fIsVisible; + _fIsOn = OtherCursor._fIsOn; + _fIsDouble = OtherCursor._fIsDouble; + _fBlinkingAllowed = OtherCursor._fBlinkingAllowed; + _fDelay = OtherCursor._fDelay; + _fIsConversionArea = OtherCursor._fIsConversionArea; + + // A resize operation should invalidate the delayed end of line status, so do not copy. + //_fDelayedEolWrap = OtherCursor._fDelayedEolWrap; + //_coordDelayedAt = OtherCursor._coordDelayedAt; + + _fDeferCursorRedraw = OtherCursor._fDeferCursorRedraw; + _fHaveDeferredCursorRedraw = OtherCursor._fHaveDeferredCursorRedraw; + + // Size will be handled seperately in the resize operation. + //_ulSize = OtherCursor._ulSize; + _cursorType = OtherCursor._cursorType; + _color = OtherCursor._color; +} + +void Cursor::DelayEOLWrap(const COORD coordDelayedAt) +{ + _coordDelayedAt = coordDelayedAt; + _fDelayedEolWrap = true; +} + +void Cursor::ResetDelayEOLWrap() +{ + _coordDelayedAt = {0}; + _fDelayedEolWrap = false; +} + +COORD Cursor::GetDelayedAtPosition() const +{ + return _coordDelayedAt; +} + +bool Cursor::IsDelayedEOLWrap() const +{ + return _fDelayedEolWrap; +} + +void Cursor::StartDeferDrawing() +{ + _fDeferCursorRedraw = true; +} + +void Cursor::EndDeferDrawing() +{ + if (_fHaveDeferredCursorRedraw) + { + _RedrawCursorAlways(); + } + + _fDeferCursorRedraw = FALSE; +} + +const CursorType Cursor::GetType() const +{ + return _cursorType; +} + +const bool Cursor::IsUsingColor() const +{ + return GetColor() != INVALID_COLOR; +} + +const COLORREF Cursor::GetColor() const +{ + return _color; +} + +void Cursor::SetColor(const unsigned int color) +{ + _color = (COLORREF)color; +} + +void Cursor::SetType(const CursorType type) +{ + _cursorType = type; +} diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h new file mode 100644 index 000000000..027d9f08d --- /dev/null +++ b/src/buffer/out/cursor.h @@ -0,0 +1,122 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- cursor.h + +Abstract: +- This file implements the NT console server cursor routines. + +Author: +- Therese Stowell (ThereseS) 5-Dec-1990 + +Revision History: +- Grouped into class and items made private. (MiNiksa, 2014) +--*/ + +#pragma once + +#include "../inc/conattrs.hpp" + +// the following values are used to create the textmode cursor. +#define CURSOR_SMALL_SIZE 25 // large enough to be one pixel on a six pixel font +class TextBuffer; + +class Cursor final +{ +public: + + static const unsigned int s_InvertCursorColor = INVALID_COLOR; + + Cursor(const ULONG ulSize, TextBuffer& parentBuffer); + + ~Cursor(); + + // No Copy. It will copy the timer handle. Bad news. + Cursor(const Cursor&) = delete; + Cursor& operator=(const Cursor&) & = delete; + + Cursor(Cursor&&) = default; + Cursor& operator=(Cursor&&) & = default; + + bool HasMoved() const noexcept; + bool IsVisible() const noexcept; + bool IsOn() const noexcept; + bool IsBlinkingAllowed() const noexcept; + bool IsDouble() const noexcept; + bool IsConversionArea() const noexcept; + bool IsPopupShown() const noexcept; + bool GetDelay() const noexcept; + ULONG GetSize() const noexcept; + COORD GetPosition() const noexcept; + + const CursorType GetType() const; + const bool IsUsingColor() const; + const COLORREF GetColor() const; + + void StartDeferDrawing(); + void EndDeferDrawing(); + + void SetHasMoved(const bool fHasMoved); + void SetIsVisible(const bool fIsVisible); + void SetIsOn(const bool fIsOn); + void SetBlinkingAllowed(const bool fIsOn); + void SetIsDouble(const bool fIsDouble); + void SetIsConversionArea(const bool fIsConversionArea); + void SetIsPopupShown(const bool fIsPopupShown); + void SetDelay(const bool fDelay); + void SetSize(const ULONG ulSize); + void SetStyle(const ULONG ulSize, const COLORREF color, const CursorType type) noexcept; + + void SetPosition(const COORD cPosition); + void SetXPosition(const int NewX); + void SetYPosition(const int NewY); + void IncrementXPosition(const int DeltaX); + void IncrementYPosition(const int DeltaY); + void DecrementXPosition(const int DeltaX); + void DecrementYPosition(const int DeltaY); + + void CopyProperties(const Cursor& OtherCursor); + + void DelayEOLWrap(const COORD coordDelayedAt); + void ResetDelayEOLWrap(); + COORD GetDelayedAtPosition() const; + bool IsDelayedEOLWrap() const; + + void SetColor(const unsigned int color); + void SetType(const CursorType type); + +private: + TextBuffer& _parentBuffer; + + //TODO: seperate the rendering and text placement + + // NOTE: If you are adding a property here, go add it to CopyProperties. + + COORD _cPosition; // current position on screen (in screen buffer coords). + + bool _fHasMoved; + bool _fIsVisible; // whether cursor is visible (set only through the API) + bool _fIsOn; // whether blinking cursor is on or not + bool _fIsDouble; // whether the cursor size should be doubled + bool _fBlinkingAllowed; //Whether or not the cursor is allowed to blink at all. only set through VT (^[[?12h/l) + bool _fDelay; // don't blink scursor on next timer message + bool _fIsConversionArea; // is attached to a conversion area so it doesn't actually need to display the cursor. + bool _fIsPopupShown; // if a popup is being shown, turn off, stop blinking. + + bool _fDelayedEolWrap; // don't wrap at EOL till the next char comes in. + COORD _coordDelayedAt; // coordinate the EOL wrap was delayed at. + + bool _fDeferCursorRedraw; // whether we should defer redrawing the cursor or not + bool _fHaveDeferredCursorRedraw; // have we been asked to redraw the cursor while it was being deferred? + + ULONG _ulSize; + + void _RedrawCursor() noexcept; + void _RedrawCursorAlways() noexcept; + + CursorType _cursorType; + bool _fUseColor; + COLORREF _color; +}; diff --git a/src/buffer/out/dirs b/src/buffer/out/dirs new file mode 100644 index 000000000..d52eeb64f --- /dev/null +++ b/src/buffer/out/dirs @@ -0,0 +1,4 @@ +DIRS=lib \ + ut_textbuffer \ + + diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj new file mode 100644 index 000000000..c38c4118c --- /dev/null +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {0CF235BD-2DA0-407E-90EE-C467E8BBC714} + Win32Proj + bufferout + BufferOut + ConBufferOut + + + + $(SolutionDir)\dep;$(SolutionDir)\dep\Console;$(SolutionDir)\dep\Win32K;$(SolutionDir)\dep\AppModel;$(SolutionDir)\dep\MinCore;%(AdditionalIncludeDirectories) + + + + + + \ No newline at end of file diff --git a/src/buffer/out/lib/sources b/src/buffer/out/lib/sources new file mode 100644 index 000000000..52a8b0c27 --- /dev/null +++ b/src/buffer/out/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConBufferOut +TARGETTYPE = LIBRARY diff --git a/src/buffer/out/precomp.cpp b/src/buffer/out/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/buffer/out/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/buffer/out/precomp.h b/src/buffer/out/precomp.h new file mode 100644 index 000000000..4c6505cc1 --- /dev/null +++ b/src/buffer/out/precomp.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#pragma warning(push) +#pragma warning(disable: ALL_CPPCORECHECK_WARNINGS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#endif + +// Windows Header Files: +#include +#include + +// private dependencies +#include "..\inc\operators.hpp" +#include "..\inc\unicode.hpp" +#pragma warning(pop) diff --git a/src/buffer/out/sources.inc b/src/buffer/out/sources.inc new file mode 100644 index 000000000..660c473d1 --- /dev/null +++ b/src/buffer/out/sources.inc @@ -0,0 +1,54 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Output Buffer +# ------------------------------------- + +# This module encapsulates the objects used to manage +# the output buffer of the console + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES= \ + ..\AttrRow.cpp \ + ..\AttrRowIterator.cpp \ + ..\cursor.cpp \ + ..\OutputCell.cpp \ + ..\OutputCellIterator.cpp \ + ..\OutputCellRect.cpp \ + ..\OutputCellView.cpp \ + ..\Row.cpp \ + ..\RowCellIterator.cpp \ + ..\TextColor.cpp \ + ..\TextAttribute.cpp \ + ..\TextAttributeRun.cpp \ + ..\textBuffer.cpp \ + ..\textBufferCellIterator.cpp \ + ..\textBufferTextIterator.cpp \ + ..\CharRow.cpp \ + ..\CharRowCell.cpp \ + ..\CharRowCellReference.cpp \ + ..\UnicodeStorage.cpp \ + +INCLUDES= \ + $(INCLUDES); \ + ..; \ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp new file mode 100644 index 000000000..3cb52eacf --- /dev/null +++ b/src/buffer/out/textBuffer.cpp @@ -0,0 +1,1033 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "textBuffer.hpp" +#include "CharRow.hpp" + +#include "../types/inc/convert.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Creates a new instance of TextBuffer +// Arguments: +// - fontInfo - The font to use for this text buffer as specified in the global font cache +// - screenBufferSize - The X by Y dimensions of the new screen buffer +// - fill - Uses the .Attributes property to decide which default color to apply to all text in this buffer +// - cursorSize - The height of the cursor within this buffer +// Return Value: +// - constructed object +// Note: may throw exception +TextBuffer::TextBuffer(const COORD screenBufferSize, + const TextAttribute defaultAttributes, + const UINT cursorSize, + Microsoft::Console::Render::IRenderTarget& renderTarget) : + _firstRow{ 0 }, + _currentAttributes{ defaultAttributes }, + _cursor{ cursorSize, *this }, + _storage{}, + _unicodeStorage{}, + _renderTarget{ renderTarget } +{ + // initialize ROWs + for (size_t i = 0; i < static_cast(screenBufferSize.Y); ++i) + { + _storage.emplace_back(static_cast(i), screenBufferSize.X, _currentAttributes, this); + } +} + +// 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 +// Arguments: +// - OtherBuffer - The text buffer to copy properties from +// Return Value: +// - +void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) +{ + GetCursor().CopyProperties(OtherBuffer.GetCursor()); +} + +// Routine Description: +// - Gets the number of rows in the buffer +// Arguments: +// - +// Return Value: +// - Total number of rows in the buffer +UINT TextBuffer::TotalRowCount() const +{ + return static_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 size_t index) const +{ + const size_t totalRows = TotalRowCount(); + + // Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows. + const size_t offsetIndex = (_firstRow + index) % totalRows; + return _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 size_t index) +{ + return const_cast(static_cast(this)->GetRowByOffset(index)); +} + +// Routine Description: +// - Retrieves read-only text iterator at the given buffer location +// Arguments: +// - at - X,Y position in buffer for iterator start position +// Return Value: +// - Read-only iterator of text data only. +TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at) const +{ + return TextBufferTextIterator(GetCellDataAt(at)); +} + +// Routine Description: +// - Retrieves read-only cell iterator at the given buffer location +// Arguments: +// - at - X,Y position in buffer for iterator start position +// Return Value: +// - Read-only iterator of cell data. +TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at) const +{ + return TextBufferCellIterator(*this, at); +} + +// Routine Description: +// - Retrieves read-only text iterator at the given buffer location +// but restricted to only the specific line (Y coordinate). +// Arguments: +// - at - X,Y position in buffer for iterator start position +// Return Value: +// - Read-only iterator of text data only. +TextBufferTextIterator TextBuffer::GetTextLineDataAt(const COORD at) const +{ + return TextBufferTextIterator(GetCellLineDataAt(at)); +} + +// Routine Description: +// - Retrieves read-only cell iterator at the given buffer location +// but restricted to only the specific line (Y coordinate). +// Arguments: +// - at - X,Y position in buffer for iterator start position +// Return Value: +// - Read-only iterator of cell data. +TextBufferCellIterator TextBuffer::GetCellLineDataAt(const COORD at) const +{ + SMALL_RECT limit; + limit.Top = at.Y; + limit.Bottom = at.Y; + limit.Left = 0; + limit.Right = GetSize().RightInclusive(); + + return TextBufferCellIterator(*this, at, Viewport::FromInclusive(limit)); +} + +// Routine Description: +// - Retrieves read-only text iterator at the given buffer location +// but restricted to operate only inside the given viewport. +// Arguments: +// - at - X,Y position in buffer for iterator start position +// - limit - boundaries for the iterator to operate within +// Return Value: +// - Read-only iterator of text data only. +TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at, const Viewport limit) const +{ + return TextBufferTextIterator(GetCellDataAt(at, limit)); +} + +// Routine Description: +// - Retrieves read-only cell iterator at the given buffer location +// but restricted to operate only inside the given viewport. +// Arguments: +// - at - X,Y position in buffer for iterator start position +// - limit - boundaries for the iterator to operate within +// Return Value: +// - Read-only iterator of cell data. +TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at, const Viewport limit) const +{ + return TextBufferCellIterator(*this, at, limit); +} + +//Routine Description: +// - Corrects and enforces consistent double byte character state (KAttrs line) within a row of the text buffer. +// - This will take the given double byte information and check that it will be consistent when inserted into the buffer +// at the current cursor position. +// - It will correct the buffer (by erasing the character prior to the cursor) if necessary to make a consistent state. +//Arguments: +// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer +//Return Value: +// - True if it is valid to insert a character with the given double byte attributes. False otherwise. +bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute) +{ + // To figure out if the sequence is valid, we have to look at the character that comes before the current one + const COORD coordPrevPosition = _GetPreviousFromCursor(); + ROW& prevRow = GetRowByOffset(coordPrevPosition.Y); + DbcsAttribute prevDbcsAttr; + try + { + prevDbcsAttr = prevRow.GetCharRow().DbcsAttrAt(coordPrevPosition.X); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return false; + } + + bool fValidSequence = true; // Valid until proven otherwise + bool fCorrectableByErase = false; // Can't be corrected until proven otherwise + + // Here's the matrix of valid items: + // N = None (single byte) + // L = Lead (leading byte of double byte sequence + // T = Trail (trailing byte of double byte sequence + // Prev Curr Result + // N N OK. + // N L OK. + // N T Fail, uncorrectable. Trailing byte must have had leading before it. + // L N Fail, OK with erase. Lead needs trailing pair. Can erase lead to correct. + // L L Fail, OK with erase. Lead needs trailing pair. Can erase prev lead to correct. + // L T OK. + // T N OK. + // T L OK. + // T T Fail, uncorrectable. New trailing byte must have had leading before it. + + // Check for only failing portions of the matrix: + if (prevDbcsAttr.IsSingle() && dbcsAttribute.IsTrailing()) + { + // N, T failing case (uncorrectable) + fValidSequence = false; + } + else if (prevDbcsAttr.IsLeading()) + { + if (dbcsAttribute.IsSingle() || dbcsAttribute.IsLeading()) + { + // L, N and L, L failing cases (correctable) + fValidSequence = false; + fCorrectableByErase = true; + } + } + else if (prevDbcsAttr.IsTrailing() && dbcsAttribute.IsTrailing()) + { + // T, T failing case (uncorrectable) + fValidSequence = false; + } + + // If it's correctable by erase, erase the previous character + if (fCorrectableByErase) + { + // Erase previous character into an N type. + try + { + prevRow.GetCharRow().ClearCell(coordPrevPosition.X); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return false; + } + + // Sequence is now N N or N L, which are both okay. Set sequence back to valid. + fValidSequence = true; + } + + return fValidSequence; +} + +//Routine Description: +// - Call before inserting a character into the buffer. +// - This will ensure a consistent double byte state (KAttrs line) within the text buffer +// - It will attempt to correct the buffer if we're inserting an unexpected double byte character type +// and it will pad out the buffer if we're going to split a double byte sequence across two rows. +//Arguments: +// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer +//Return Value: +// - true if we successfully prepared the buffer and moved the cursor +// - false otherwise (out of memory) +bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) +{ + // Assert the buffer state is ready for this character + // This function corrects most errors. If this is false, we had an uncorrectable one. + FAIL_FAST_IF(!(_AssertValidDoubleByteSequence(dbcsAttribute))); // Shouldn't be uncorrectable sequences unless something is very wrong. + + bool 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.IsLeading()) + { + short const sBufferWidth = GetSize().Width(); + + // If we're about to lead on the last column in the row, we need to add a padding space + if (GetCursor().GetPosition().X == sBufferWidth - 1) + { + // set that we're wrapping for double byte reasons + CharRow& charRow = GetRowByOffset(GetCursor().GetPosition().Y).GetCharRow(); + charRow.SetDoubleBytePadded(true); + + // then move the cursor forward and onto the next row + fSuccess = IncrementCursor(); + } + } + return fSuccess; +} + +// Routine Description: +// - Writes cells to the output buffer. Writes at the cursor. +// Arguments: +// - givenIt - Iterator representing output cell data to write +// Return Value: +// - The final position of the iterator +OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt) +{ + const auto& cursor = GetCursor(); + const auto target = cursor.GetPosition(); + + const auto finalIt = Write(givenIt, target); + + return finalIt; +} + +// Routine Description: +// - Writes cells to the output buffer. +// Arguments: +// - givenIt - Iterator representing output cell data to write +// - target - the row/column to start writing the text to +// Return Value: +// - The final position of the iterator +OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt, + const COORD target) +{ + // Make mutable copy so we can walk. + auto it = givenIt; + + // Make mutable target so we can walk down lines. + auto lineTarget = target; + + // Get size of the text buffer so we can stay in bounds. + const auto size = GetSize(); + + // While there's still data in the iterator and we're still targeting in bounds... + while (it && size.IsInBounds(lineTarget)) + { + // Attempt to write as much data as possible onto this line. + it = WriteLine(it, lineTarget, true); + + // Move to the next line down. + lineTarget.X = 0; + ++lineTarget.Y; + } + + return it; +} + +// Routine Description: +// - Writes one line of text to the output buffer. +// Arguments: +// - givenIt - The iterator that will dereference into cell data to insert +// - target - Coordinate targeted within output buffer +// - setWrap - Whether we should try to set the wrap flag if we write up to the end of the line and have more data +// - limitRight - Optionally restrict the right boundary for writing (e.g. stop writing earlier than the end of line) +// Return Value: +// - The iterator, but advanced to where we stopped writing. Use to find input consumed length or cells written length. +OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, + const COORD target, + const bool setWrap, + std::optional limitRight) +{ + // If we're not in bounds, exit early. + if (!GetSize().IsInBounds(target)) + { + return givenIt; + } + + // Get the row and write the cells + ROW& row = GetRowByOffset(target.Y); + const auto newIt = row.WriteCells(givenIt, target.X, setWrap, limitRight); + + // Take the cell distance written and notify that it needs to be repainted. + const auto written = newIt.GetCellDistance(givenIt); + const Viewport paint = Viewport::FromDimensions(target, { gsl::narrow(written), 1 }); + _NotifyPaint(paint); + + return newIt; +} + +//Routine Description: +// - Inserts one codepoint into the buffer at the current cursor position and advances the cursor as appropriate. +//Arguments: +// - chars - The codepoint to insert +// - dbcsAttribute - Double byte information associated with the codepoint +// - bAttr - Color data associated with the character +//Return Value: +// - true if we successfully inserted the character +// - false otherwise (out of memory) +bool 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 + bool fSuccess = _PrepareForDoubleByteSequence(dbcsAttribute); + + if (fSuccess) + { + // Get the current cursor position + short const iRow = GetCursor().GetPosition().Y; // row stored as logical position, not array position + short const iCol = GetCursor().GetPosition().X; // column logical and array positions are equal. + + // Get the row associated with the given logical position + ROW& Row = GetRowByOffset(iRow); + + // Store character and double byte data + CharRow& charRow = Row.GetCharRow(); + short const cBufferWidth = GetSize().Width(); + + try + { + charRow.GlyphAt(iCol) = chars; + charRow.DbcsAttrAt(iCol) = dbcsAttribute; + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return false; + } + + // Store color data + fSuccess = Row.GetAttrRow().SetAttrToEnd(iCol, attr); + if (fSuccess) + { + // Advance the cursor + fSuccess = IncrementCursor(); + } + } + return fSuccess; +} + +//Routine Description: +// - Inserts one ucs2 codepoint into the buffer at the current cursor position and advances the cursor as appropriate. +//Arguments: +// - wch - The codepoint to insert +// - dbcsAttribute - Double byte information associated with the codepoint +// - bAttr - Color data associated with the character +//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) +{ + return InsertCharacter({ &wch, 1 }, dbcsAttribute, attr); +} + +//Routine Description: +// - Finds the current row in the buffer (as indicated by the cursor position) +// and specifies that we have forced a line wrap on that row +//Arguments: +// - - Always sets to wrap +//Return Value: +// - +void TextBuffer::_SetWrapOnCurrentRow() +{ + _AdjustWrapOnCurrentRow(true); +} + +//Routine Description: +// - Finds the current row in the buffer (as indicated by the cursor position) +// and specifies whether or not it should have a line wrap flag. +//Arguments: +// - fSet - True if this row has a wrap. False otherwise. +//Return Value: +// - +void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) +{ + // The vertical position of the cursor represents the current row we're manipulating. + const UINT uiCurrentRowOffset = GetCursor().GetPosition().Y; + + // Set the wrap status as appropriate + GetRowByOffset(uiCurrentRowOffset).GetCharRow().SetWrapForced(fSet); +} + +//Routine Description: +// - Increments the cursor one position in the buffer as if text is being typed into the buffer. +// - NOTE: Will introduce a wrap marker if we run off the end of the current row +//Arguments: +// - +//Return Value: +// - true if we successfully moved the cursor. +// - false otherwise (out of memory) +bool 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. + // So subtract 1 from buffer size in each direction to find the index of the final column in the buffer + const short iFinalColumnIndex = GetSize().RightInclusive(); + + // Move the cursor one position to the right + GetCursor().IncrementXPosition(1); + + bool fSuccess = true; + // If we've passed the final valid column... + if (GetCursor().GetPosition().X > iFinalColumnIndex) + { + // Then mark that we've been forced to wrap + _SetWrapOnCurrentRow(); + + // Then move the cursor to a new line + fSuccess = NewlineCursor(); + } + return fSuccess; +} + +//Routine Description: +// - Increments the cursor one line down in the buffer and to the beginning of the line +//Arguments: +// - +//Return Value: +// - true if we successfully moved the cursor. +bool TextBuffer::NewlineCursor() +{ + bool fSuccess = false; + short const iFinalRowIndex = GetSize().BottomInclusive(); + + // Reset the cursor position to 0 and move down one line + GetCursor().SetXPosition(0); + GetCursor().IncrementYPosition(1); + + // If we've passed the final valid row... + if (GetCursor().GetPosition().Y > iFinalRowIndex) + { + // Stay on the final logical/offset row of the buffer. + GetCursor().SetYPosition(iFinalRowIndex); + + // Instead increment the circular buffer to move us into the "oldest" row of the backing buffer + fSuccess = IncrementCircularBuffer(); + } + else + { + fSuccess = true; + } + return fSuccess; +} + +//Routine Description: +// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable. +//Arguments: +// - +//Return Value: +// - true if we successfully incremented the buffer. +bool TextBuffer::IncrementCircularBuffer() +{ + // 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). + _renderTarget.TriggerCircling(); + + // First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed. + bool fSuccess = _storage.at(_firstRow).Reset(_currentAttributes); + if (fSuccess) + { + // Now proceed to increment. + // Incrementing it will cause the next line down to become the new "top" of the window (the new "0" in logical coordinates) + _firstRow++; + + // If we pass up the height of the buffer, loop back to 0. + if (_firstRow >= GetSize().Height()) + { + _firstRow = 0; + } + } + return fSuccess; +} + +//Routine Description: +// - Retrieves the position of the last non-space character on the final line of the text buffer. +//Arguments: +// - +//Return Value: +// - Coordinate position in screen coordinates (offset coordinates, not array index coordinates). +COORD TextBuffer::GetLastNonSpaceCharacter() const +{ + COORD coordEndOfText; + // Always search the whole buffer, by starting at the bottom. + coordEndOfText.Y = GetSize().BottomInclusive(); + + const ROW* pCurrRow = &GetRowByOffset(coordEndOfText.Y); + // The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character) + coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; + + // If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text. + bool fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); // this row is empty, and we're not at the top + while (fDoBackUp) + { + coordEndOfText.Y--; + pCurrRow = &GetRowByOffset(coordEndOfText.Y); + // We need to back up to the previous row if this line is empty, AND there are more rows + + coordEndOfText.X = static_cast(pCurrRow->GetCharRow().MeasureRight()) - 1; + fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > 0); + } + + // don't allow negative results + coordEndOfText.Y = std::max(coordEndOfText.Y, 0i16); + coordEndOfText.X = std::max(coordEndOfText.X, 0i16); + + return coordEndOfText; +} + +// Routine Description: +// - Retrieves the position of the previous character relative to the current cursor position +// Arguments: +// - +// Return Value: +// - Coordinate position in screen coordinates of the character just before the cursor. +// - NOTE: Will return 0,0 if already in the top left corner +COORD TextBuffer::_GetPreviousFromCursor() const +{ + COORD coordPosition = GetCursor().GetPosition(); + + // If we're not at the left edge, simply move the cursor to the left by one + if (coordPosition.X > 0) + { + coordPosition.X--; + } + else + { + // Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous) + if (coordPosition.Y > 0) + { + // move the cursor to the right edge + coordPosition.X = GetSize().RightInclusive(); + + // and up one line + coordPosition.Y--; + } + } + + return coordPosition; +} + +const SHORT TextBuffer::GetFirstRowIndex() const +{ + return _firstRow; +} +const Viewport TextBuffer::GetSize() const +{ + return Viewport::FromDimensions({ 0, 0 }, { gsl::narrow(_storage.at(0).size()), gsl::narrow(_storage.size()) }); +} + +void TextBuffer::_SetFirstRowIndex(const SHORT FirstRowIndex) +{ + _firstRow = FirstRowIndex; +} + +void TextBuffer::ScrollRows(const SHORT firstRow, const SHORT size, const SHORT 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()); + + // The first row is now at the top. + _firstRow = 0; + } + + // Rotate just the subsection specified + if (delta < 0) + { + // The layout is like this: + // delta is -2, size is 3, firstRow is 5 + // We want 3 rows from 5 (5, 6, and 7) to move up 2 spots. + // --- (storage) ---- + // | 0 begin + // | 1 + // | 2 + // | 3 A. begin + firstRow + delta (because delta is negative) + // | 4 + // | 5 B. begin + firstRow + // | 6 + // | 7 + // | 8 C. begin + 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); + } + else + { + // The layout is like this: + // delta is 2, size is 3, firstRow is 5 + // We want 3 rows from 5 (5, 6, and 7) to move down 2 spots. + // --- (storage) ---- + // | 0 begin + // | 1 + // | 2 + // | 3 + // | 4 + // | 5 A. begin + firstRow + // | 6 + // | 7 + // | 8 B. begin + firstRow + size + // | 9 + // | 10 C. begin + 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); + } + + // Renumber the IDs now that we've rearranged where the rows sit within the buffer. + // Refreshing should also delegate to the UnicodeStorage to re-key all the stored unicode sequences (where applicable). + _RefreshRowIDs(std::nullopt); +} + +Cursor& TextBuffer::GetCursor() +{ + return _cursor; +} + +const Cursor& TextBuffer::GetCursor() const +{ + return _cursor; +} + +[[nodiscard]] +TextAttribute TextBuffer::GetCurrentAttributes() const noexcept +{ + return _currentAttributes; +} + +void TextBuffer::SetCurrentAttributes(const TextAttribute currentAttributes) noexcept +{ + _currentAttributes = currentAttributes; +} + +// Routine Description: +// - Resets the text contents of this buffer with the default character +// and the default current color attributes +void TextBuffer::Reset() +{ + const auto attr = GetCurrentAttributes(); + + for (auto& row : _storage) + { + row.GetCharRow().Reset(); + row.GetAttrRow().Reset(attr); + } +} + +// Routine Description: +// - This is the legacy screen resize with minimal changes +// Arguments: +// - newSize - new size of screen. +// Return Value: +// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. +[[nodiscard]] +NTSTATUS TextBuffer::ResizeTraditional(const COORD newSize) noexcept +{ + RETURN_HR_IF(E_INVALIDARG, newSize.X < 0 || newSize.Y < 0); + + const auto currentSize = GetSize().Dimensions(); + const auto attributes = GetCurrentAttributes(); + + SHORT TopRow = 0; // new top row of the screen buffer + if (newSize.Y <= GetCursor().GetPosition().Y) + { + TopRow = GetCursor().GetPosition().Y - newSize.Y + 1; + } + const SHORT TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.Y; + + // rotate rows until the top row is at index 0 + try + { + const ROW& newTopRow = _storage[TopRowIndex]; + while (&newTopRow != &_storage.front()) + { + _storage.push_back(std::move(_storage.front())); + _storage.pop_front(); + } + + _SetFirstRowIndex(0); + + // realloc in the Y direction + // remove rows if we're shrinking + while (_storage.size() > static_cast(newSize.Y)) + { + _storage.pop_back(); + } + // add rows if we're growing + while (_storage.size() < static_cast(newSize.Y)) + { + _storage.emplace_back(static_cast(_storage.size()), newSize.X, attributes, this); + } + + // Now that we've tampered with the row placement, refresh all the row IDs. + // Also take advantage of the row ID refresh loop to resize the rows in the X dimension + // and cleanup the UnicodeStorage characters that might fall outside the resized buffer. + _RefreshRowIDs(newSize.X); + + } + CATCH_RETURN(); + + return S_OK; +} + +const UnicodeStorage& TextBuffer::GetUnicodeStorage() const +{ + return _unicodeStorage; +} + +UnicodeStorage& TextBuffer::GetUnicodeStorage() +{ + return _unicodeStorage; +} + +// Routine Description: +// - Method to help refresh all the Row IDs after manipulating the row +// by shuffling pointers around. +// - This will also update parent pointers that are stored in depth within the buffer +// (e.g. it will update CharRow parents pointing at Rows that might have been moved around) +// - Optionally takes a new row width if we're resizing to perform a resize operation and cleanup +// any high unicode (UnicodeStorage) runs while we're already looping through the rows. +// Arguments: +// - newRowWidth - Optional new value for the row width. +void TextBuffer::_RefreshRowIDs(std::optional newRowWidth) +{ + std::map rowMap; + SHORT i = 0; + for (auto& it : _storage) + { + // Build a map so we can update Unicode Storage + rowMap.emplace(it.GetId(), i); + + // Update the IDs + it.SetId(i++); + + // Also update the char row parent pointers as they can get shuffled up in the rotates. + it.GetCharRow().UpdateParent(&it); + + // Resize the rows in the X dimension if we have a new width + if (newRowWidth.has_value()) + { + // Realloc in the X direction + THROW_IF_FAILED(it.Resize(newRowWidth.value())); + } + } + + // Give the new mapping to Unicode Storage + _unicodeStorage.Remap(rowMap, newRowWidth); +} + +void TextBuffer::_NotifyPaint(const Viewport& viewport) const +{ + _renderTarget.TriggerRedraw(viewport); +} + +// Routine Description: +// - Retrieves the first row from the underlying buffer. +// Arguments: +// - +// Return Value: +// - reference to the first row. +ROW& TextBuffer::_GetFirstRow() +{ + return GetRowByOffset(0); +} + +// Routine Description: +// - Retrieves the row that comes before the given row. +// - Does not wrap around the screen buffer. +// Arguments: +// - The current row. +// Return Value: +// - reference to the previous row +// Note: +// - will throw exception if called with the first row of the text buffer +ROW& TextBuffer::_GetPrevRowNoWrap(const ROW& Row) +{ + int prevRowIndex = Row.GetId() - 1; + if (prevRowIndex < 0) + { + prevRowIndex = TotalRowCount() - 1; + } + + THROW_HR_IF(E_FAIL, Row.GetId() == _firstRow); + return _storage[prevRowIndex]; +} + +// Method Description: +// - Retrieves this buffer's current render target. +// Arguments: +// - +// Return Value: +// - This buffer's current render target. +Microsoft::Console::Render::IRenderTarget& TextBuffer::GetRenderTarget() +{ + return _renderTarget; +} + +// Routine Description: +// - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing). +// Arguments: +// - lineSelection - true if entire line is being selected. False otherwise (box selection) +// - trimTrailingWhitespace - setting flag removes trailing whitespace at the end of each row in selection +// - selectionRects - the selection regions from which the data will be extracted from the buffer +// - GetForegroundColor - function used to map TextAttribute to RGB COLORREF for foreground color +// - GetBackgroundColor - function used to map TextAttribute to RGB COLORREF for foreground color +// Return Value: +// - The text, background color, and foreground color data of the selected region of the text buffer. +const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSelection, + const bool trimTrailingWhitespace, + const std::vector& selectionRects, + std::function GetForegroundColor, + std::function GetBackgroundColor) const +{ + TextAndColor data; + + // preallocate our vectors to reduce reallocs + size_t const rows = selectionRects.size(); + data.text.reserve(rows); + data.FgAttr.reserve(rows); + data.BkAttr.reserve(rows); + + // for each row in the selection + for (UINT i = 0; i < rows; i++) + { + const UINT iRow = selectionRects.at(i).Top; + + const Viewport highlight = Viewport::FromInclusive(selectionRects.at(i)); + + // retrieve the data from the screen buffer + auto it = GetCellDataAt(highlight.Origin(), highlight); + + // allocate a string buffer + std::wstring selectionText; + std::vector selectionFgAttr; + std::vector selectionBkAttr; + + // preallocate to avoid reallocs + selectionText.reserve(highlight.Width() + 2); // + 2 for \r\n if we munged it + selectionFgAttr.reserve(highlight.Width() + 2); + selectionBkAttr.reserve(highlight.Width() + 2); + + // copy char data into the string buffer, skipping trailing bytes + while (it) + { + const auto& cell = *it; + auto cellData = cell.TextAttr(); + COLORREF const CellFgAttr = GetForegroundColor(cellData); + COLORREF const CellBkAttr = GetBackgroundColor(cellData); + + if (!cell.DbcsAttr().IsTrailing()) + { + selectionText.append(cell.Chars()); + for (const wchar_t wch : cell.Chars()) + { + selectionFgAttr.push_back(CellFgAttr); + selectionBkAttr.push_back(CellBkAttr); + } + } + it++; + } + + // trim trailing spaces if SHIFT key not held + if (trimTrailingWhitespace) + { + const ROW& Row = GetRowByOffset(iRow); + + // FOR LINE SELECTION ONLY: if the row was wrapped, don't remove the spaces at the end. + if (!lineSelection || !Row.GetCharRow().WasWrapForced()) + { + while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE) + { + selectionText.pop_back(); + selectionFgAttr.pop_back(); + selectionBkAttr.pop_back(); + } + } + + // apply CR/LF to the end of the final string, unless we're the last line. + // a.k.a if we're earlier than the bottom, then apply CR/LF. + if (i < selectionRects.size() - 1) + { + // FOR LINE SELECTION ONLY: if the row was wrapped, do not apply CR/LF. + // a.k.a. if the row was NOT wrapped, then we can assume a CR/LF is proper + // always apply \r\n for box selection + if (!lineSelection || !GetRowByOffset(iRow).GetCharRow().WasWrapForced()) + { + COLORREF const Blackness = RGB(0x00, 0x00, 0x00); // cant see CR/LF so just use black FG & BK + + selectionText.push_back(UNICODE_CARRIAGERETURN); + selectionText.push_back(UNICODE_LINEFEED); + selectionFgAttr.push_back(Blackness); + selectionFgAttr.push_back(Blackness); + selectionBkAttr.push_back(Blackness); + selectionBkAttr.push_back(Blackness); + } + } + } + + data.text.emplace_back(selectionText); + data.FgAttr.emplace_back(selectionFgAttr); + data.BkAttr.emplace_back(selectionBkAttr); + } + + return data; +} diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp new file mode 100644 index 000000000..7d46022e4 --- /dev/null +++ b/src/buffer/out/textBuffer.hpp @@ -0,0 +1,185 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- textBuffer.hpp + +Abstract: +- This module contains structures and functions for manipulating a text + based buffer within the console host window. + +Author(s): +- Michael Niksa (miniksa) 10-Apr-2014 +- Paul Campbell (paulcam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c + by Therese Stowell (ThereseS) 1990-1991 + +Notes: +ScreenBuffer data structure overview: + +each screen buffer has an array of ROW structures. each ROW structure +contains the data for one row of text. the data stored for one row of +text is a character array and an attribute array. the character array +is allocated the full length of the row from the heap, regardless of the +non-space length. we also maintain the non-space length. the character +array is initialized to spaces. the attribute +array is run length encoded (i.e 5 BLUE, 3 RED). if there is only one +attribute for the whole row (the normal case), it is stored in the ATTR_ROW +structure. otherwise the attr string is allocated from the heap. + +ROW - CHAR_ROW - CHAR string +\ \ length of char string +\ +ATTR_ROW - ATTR_PAIR string +\ length of attr pair string +ROW +ROW +ROW + +ScreenInfo->Rows points to the ROW array. ScreenInfo->Rows[0] is not +necessarily the top row. ScreenInfo->BufferInfo.TextInfo->FirstRow contains the index of +the top row. That means scrolling (if scrolling entire screen) +merely involves changing the FirstRow index, +filling in the last row, and updating the screen. + +--*/ + +#pragma once + +#include "cursor.h" +#include "Row.hpp" +#include "TextAttribute.hpp" +#include "UnicodeStorage.hpp" +#include "../types/inc/Viewport.hpp" + +#include "../buffer/out/textBufferCellIterator.hpp" +#include "../buffer/out/textBufferTextIterator.hpp" + +#include "../renderer/inc/IRenderTarget.hpp" + +class TextBuffer final +{ +public: + TextBuffer(const COORD screenBufferSize, + const TextAttribute defaultAttributes, + const UINT cursorSize, + Microsoft::Console::Render::IRenderTarget& renderTarget); + TextBuffer(const TextBuffer& a) = delete; + + ~TextBuffer() = default; + + // Used for duplicating properties to another text buffer + void CopyProperties(const TextBuffer& OtherBuffer); + + // row manipulation + const ROW& GetRowByOffset(const size_t index) const; + ROW& GetRowByOffset(const size_t index); + + TextBufferCellIterator GetCellDataAt(const COORD at) const; + TextBufferCellIterator GetCellLineDataAt(const COORD at) const; + TextBufferCellIterator GetCellDataAt(const COORD at, const Microsoft::Console::Types::Viewport limit) const; + TextBufferTextIterator GetTextDataAt(const COORD at) const; + TextBufferTextIterator GetTextLineDataAt(const COORD at) const; + TextBufferTextIterator GetTextDataAt(const COORD at, const Microsoft::Console::Types::Viewport limit) const; + + // Text insertion functions + OutputCellIterator Write(const OutputCellIterator givenIt); + + OutputCellIterator Write(const OutputCellIterator givenIt, + const COORD target); + + OutputCellIterator WriteLine(const OutputCellIterator givenIt, + const COORD target, + const bool setWrap = false, + 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(); + + // Scroll needs access to this to quickly rotate around the buffer. + bool IncrementCircularBuffer(); + + COORD GetLastNonSpaceCharacter() const; + + Cursor& GetCursor(); + const Cursor& GetCursor() const; + + const SHORT GetFirstRowIndex() const; + + const Microsoft::Console::Types::Viewport GetSize() const; + + void ScrollRows(const SHORT firstRow, const SHORT size, const SHORT delta); + + UINT TotalRowCount() const; + + [[nodiscard]] + TextAttribute GetCurrentAttributes() const noexcept; + + void SetCurrentAttributes(const TextAttribute currentAttributes) noexcept; + + void Reset(); + + [[nodiscard]] + HRESULT ResizeTraditional(const COORD newSize) noexcept; + + const UnicodeStorage& GetUnicodeStorage() const; + UnicodeStorage& GetUnicodeStorage(); + + Microsoft::Console::Render::IRenderTarget& GetRenderTarget(); + + class TextAndColor + { + public: + std::vector text; + std::vector> FgAttr; + std::vector> BkAttr; + }; + + const TextAndColor GetTextForClipboard(const bool lineSelection, + const bool trimTrailingWhitespace, + const std::vector& selectionRects, + std::function GetForegroundColor, + std::function GetBackgroundColor) const; + +private: + + std::deque _storage; + Cursor _cursor; + + SHORT _firstRow; // indexes top row (not necessarily 0) + + TextAttribute _currentAttributes; + + // storage location for glyphs that can't fit into the buffer normally + UnicodeStorage _unicodeStorage; + + void _RefreshRowIDs(std::optional newRowWidth); + + Microsoft::Console::Render::IRenderTarget& _renderTarget; + + void _SetFirstRowIndex(const SHORT FirstRowIndex); + + COORD _GetPreviousFromCursor() const; + + void _SetWrapOnCurrentRow(); + void _AdjustWrapOnCurrentRow(const bool fSet); + + void _NotifyPaint(const Microsoft::Console::Types::Viewport& viewport) const; + + // Assist with maintaining proper buffer state for Double Byte character sequences + bool _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute); + bool _AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute); + + ROW& _GetFirstRow(); + ROW& _GetPrevRowNoWrap(const ROW& row); + +#ifdef UNIT_TESTING + friend class TextBufferTests; + friend class UiaTextRangeTests; +#endif +}; diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp new file mode 100644 index 000000000..765214a31 --- /dev/null +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "textBufferCellIterator.hpp" + +#include "CharRow.hpp" +#include "textBuffer.hpp" +#include "../types/inc/convert.hpp" +#include "../types/inc/viewport.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Creates a new read-only iterator to seek through cell data stored within a screen buffer +// Arguments: +// - buffer - Text buffer to seek throught +// - pos - Starting position to retrieve text data from (within screen buffer bounds) +TextBufferCellIterator::TextBufferCellIterator(const TextBuffer& buffer, COORD pos) : + TextBufferCellIterator(buffer, pos, buffer.GetSize()) +{ +} + +// Routine Description: +// - Creates a new read-only iterator to seek through cell data stored within a screen buffer +// Arguments: +// - buffer - Pointer to screen buffer to seek through +// - pos - Starting position to retrieve text data from (within screen buffer bounds) +// - limits - Viewport limits to restrict the iterator within the buffer bounds (smaller than the buffer itself) +TextBufferCellIterator::TextBufferCellIterator(const TextBuffer& buffer, COORD pos, const Viewport limits) : + _buffer(buffer), + _pos(pos), + _pRow(s_GetRow(buffer, pos)), + _bounds(limits), + _exceeded(false), + _view({}, {}, {}, TextAttributeBehavior::Stored), + _attrIter(s_GetRow(buffer, pos)->GetAttrRow().cbegin()) +{ + // Throw if the bounds rectangle is not limited to the inside of the given buffer. + THROW_HR_IF(E_INVALIDARG, !buffer.GetSize().IsInBounds(limits)); + + // Throw if the coordinate is not limited to the inside of the given buffer. + THROW_HR_IF(E_INVALIDARG, !limits.IsInBounds(pos)); + + _attrIter += pos.X; + + _GenerateView(); +} + +// Routine Description: +// - Tells if the iterator is still valid (hasn't exceeded boundaries of underlying text buffer) +// Return Value: +// - True if this iterator can still be dereferenced for data. False if we've passed the end and are out of data. +TextBufferCellIterator::operator bool() const noexcept +{ + return !_exceeded && _bounds.IsInBounds(_pos); +} + +// Routine Description: +// - Compares two iterators to see if they're pointing to the same position in the same buffer +// Arguments: +// - it - The other iterator to compare to this one. +// Return Value: +// - True if it's the same text buffer and same cell position. False otherwise. +bool TextBufferCellIterator::operator==(const TextBufferCellIterator& it) const noexcept +{ + return _pos == it._pos && + &_buffer == &it._buffer && + _exceeded == it._exceeded && + _bounds == it._bounds && + _pRow == it._pRow && + _attrIter == it._attrIter; +} + +// Routine Description: +// - Compares two iterators to see if they're pointing to the different positions in the same buffer or different buffers entirely. +// Arguments: +// - it - The other iterator to compare to this one. +// Return Value: +// - True if it's the same text buffer and different cell position or if they're different buffers. False otherwise. +bool TextBufferCellIterator::operator!=(const TextBufferCellIterator& it) const noexcept +{ + return !(*this == it); +} + +// Routine Description: +// - Advances the iterator forward relative to the underlying text buffer by the specified movement +// Arguments: +// - movement - Magnitude and direction of movement. +// Return Value: +// - Reference to self after movement. +TextBufferCellIterator& TextBufferCellIterator::operator+=(const ptrdiff_t& movement) +{ + ptrdiff_t move = movement; + auto newPos = _pos; + while (move > 0 && !_exceeded) + { + _exceeded = !_bounds.IncrementInBounds(newPos); + move--; + } + while (move < 0 && !_exceeded) + { + _exceeded = !_bounds.DecrementInBounds(newPos); + move++; + } + _SetPos(newPos); + return (*this); +} + +// Routine Description: +// - Advances the iterator backward relative to the underlying text buffer by the specified movement +// Arguments: +// - movement - Magnitude and direction of movement. +// Return Value: +// - Reference to self after movement. +TextBufferCellIterator& TextBufferCellIterator::operator-=(const ptrdiff_t& movement) +{ + return this->operator+=(-movement); +} + +// Routine Description: +// - Advances the iterator forward relative to the underlying text buffer by exactly 1 +// Return Value: +// - Reference to self after movement. +TextBufferCellIterator& TextBufferCellIterator::operator++() +{ + return this->operator+=(1); +} + +// Routine Description: +// - Advances the iterator backward relative to the underlying text buffer by exactly 1 +// Return Value: +// - Reference to self after movement. +TextBufferCellIterator& TextBufferCellIterator::operator--() +{ + return this->operator-=(1); +} + +// Routine Description: +// - Advances the iterator forward relative to the underlying text buffer by exactly 1 +// Return Value: +// - Value with previous position prior to movement. +TextBufferCellIterator TextBufferCellIterator::operator++(int) +{ + auto temp(*this); + operator++(); + return temp; +} + +// Routine Description: +// - Advances the iterator backward relative to the underlying text buffer by exactly 1 +// Return Value: +// - Value with previous position prior to movement. +TextBufferCellIterator TextBufferCellIterator::operator--(int) +{ + auto temp(*this); + operator--(); + return temp; +} + +// Routine Description: +// - Advances the iterator forward relative to the underlying text buffer by the specified movement +// Arguments: +// - movement - Magnitude and direction of movement. +// Return Value: +// - Value with previous position prior to movement. +TextBufferCellIterator TextBufferCellIterator::operator+(const ptrdiff_t& movement) +{ + auto temp(*this); + temp += movement; + return temp; +} + +// Routine Description: +// - Advances the iterator negative relative to the underlying text buffer by the specified movement +// Arguments: +// - movement - Magnitude and direction of movement. +// Return Value: +// - Value with previous position prior to movement. +TextBufferCellIterator TextBufferCellIterator::operator-(const ptrdiff_t& movement) +{ + auto temp(*this); + temp -= movement; + return temp; +} + +// Routine Description: +// - Provides the difference in position between two iterators. +// Arguments: +// - it - The other iterator to compare to this one. +ptrdiff_t TextBufferCellIterator::operator-(const TextBufferCellIterator& it) +{ + THROW_HR_IF(E_NOT_VALID_STATE, &_buffer != &it._buffer); // It's not valid to compare this for iterators pointing at different buffers. + return _bounds.CompareInBounds(_pos, it._pos); +} + +// Routine Description: +// - 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 COORD newPos) +{ + if (newPos.Y != _pos.Y) + { + _pRow = s_GetRow(_buffer, newPos); + _attrIter = _pRow->GetAttrRow().cbegin(); + _pos.X = 0; + } + + if (newPos.X != _pos.X) + { + const ptrdiff_t diff = newPos.X - _pos.X; + _attrIter += diff; + } + + _pos = newPos; + + _GenerateView(); +} + +// Routine Description: +// - Shortcut for pulling the row out of the text buffer embedded in the screen information. +// We'll hold and cache this to improve performance over looking it up every time. +// Arguments: +// - buffer - Screen information pointer to pull text buffer data from +// - 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 COORD pos) +{ + return &buffer.GetRowByOffset(pos.Y); +} + +// Routine Description: +// - Updates the internal view. Call after updating row, attribute, or positions. +void TextBufferCellIterator::_GenerateView() +{ + _view = OutputCellView(_pRow->GetCharRow().GlyphAt(_pos.X), + _pRow->GetCharRow().DbcsAttrAt(_pos.X), + *_attrIter, + TextAttributeBehavior::Stored); +} + +// Routine Description: +// - Provides full fidelity view of the cell data in the underlying buffer. +// Arguments: +// - - Uses current position +// Return Value: +// - OutputCellView representation that provides a read-only view into the underlying text buffer data. +const OutputCellView& TextBufferCellIterator::operator*() const noexcept +{ + return _view; +} + +// Routine Description: +// - Provides full fidelity view of the cell data in the underlying buffer. +// Arguments: +// - - Uses current position +// Return Value: +// - OutputCellView representation that provides a read-only view into the underlying text buffer data. +const OutputCellView* TextBufferCellIterator::operator->() const noexcept +{ + return &_view; +} diff --git a/src/buffer/out/textBufferCellIterator.hpp b/src/buffer/out/textBufferCellIterator.hpp new file mode 100644 index 000000000..75cc8d08c --- /dev/null +++ b/src/buffer/out/textBufferCellIterator.hpp @@ -0,0 +1,73 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- textBufferCellIterator.hpp + +Abstract: +- This module abstracts walking through text on the screen +- It is currently intended for read-only operations + +Author(s): +- Michael Niksa (MiNiksa) 29-Jun-2018 +--*/ + +#pragma once + +#include "AttrRowIterator.hpp" +#include "CharRow.hpp" +#include "OutputCellView.hpp" +#include "../../types/inc/viewport.hpp" + +class TextBuffer; + +class TextBufferCellIterator +{ +public: + TextBufferCellIterator(const TextBuffer& buffer, COORD pos); + TextBufferCellIterator(const TextBuffer& buffer, COORD pos, const Microsoft::Console::Types::Viewport limits); + + ~TextBufferCellIterator() = default; + + operator bool() const noexcept; + + bool operator==(const TextBufferCellIterator& it) const noexcept; + bool operator!=(const TextBufferCellIterator& it) const noexcept; + + TextBufferCellIterator& operator+=(const ptrdiff_t& movement); + TextBufferCellIterator& operator-=(const ptrdiff_t& movement); + TextBufferCellIterator& operator++(); + TextBufferCellIterator& operator--(); + TextBufferCellIterator operator++(int); + TextBufferCellIterator operator--(int); + TextBufferCellIterator operator+(const ptrdiff_t& movement); + TextBufferCellIterator operator-(const ptrdiff_t& movement); + + ptrdiff_t operator-(const TextBufferCellIterator& it); + + const OutputCellView& operator*() const noexcept; + const OutputCellView* operator->() const noexcept; + +protected: + + void _SetPos(const COORD newPos); + void _GenerateView(); + static const ROW* s_GetRow(const TextBuffer& buffer, const COORD pos); + + OutputCellView _view; + + const ROW* _pRow; + AttrRowIterator _attrIter; + const TextBuffer& _buffer; + const Microsoft::Console::Types::Viewport _bounds; + bool _exceeded; + COORD _pos; + +#if UNIT_TESTING + friend class TextBufferIteratorTests; + friend class TextBufferTests; + friend class ApiRoutinesTests; +#endif +}; + diff --git a/src/buffer/out/textBufferTextIterator.cpp b/src/buffer/out/textBufferTextIterator.cpp new file mode 100644 index 000000000..e1fe1a65b --- /dev/null +++ b/src/buffer/out/textBufferTextIterator.cpp @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "textBufferTextIterator.hpp" + +#include "CharRow.hpp" +#include "Row.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Narrows the view of a cell iterator into a text only iterator. +// Arguments: +// - A cell iterator +TextBufferTextIterator::TextBufferTextIterator(const TextBufferCellIterator& cellIt) : + TextBufferCellIterator(cellIt) +{ +} + +// Routine Description: +// - Returns the text information from the text buffer position addressed by this iterator. +// Return Value: +// - Read only UTF-16 text data +const std::wstring_view TextBufferTextIterator::operator*() const +{ + return _view.Chars(); +} + +// Routine Description: +// - Returns the text information from the text buffer position addressed by this iterator. +// Return Value: +// - Read only UTF-16 text data +const std::wstring_view* TextBufferTextIterator::operator->() const +{ + return &_view.Chars(); +} + diff --git a/src/buffer/out/textBufferTextIterator.hpp b/src/buffer/out/textBufferTextIterator.hpp new file mode 100644 index 000000000..8d8b5cbf9 --- /dev/null +++ b/src/buffer/out/textBufferTextIterator.hpp @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- textBufferTextIterator.hpp + +Abstract: +- This module abstracts walking through text on the screen +- It is currently intended for read-only operations + +Author(s): +- Michael Niksa (MiNiksa) 01-May-2018 +--*/ + +#pragma once + +#include "textBufferCellIterator.hpp" + +class SCREEN_INFORMATION; + +class TextBufferTextIterator final : public TextBufferCellIterator +{ +public: + TextBufferTextIterator(const TextBufferCellIterator& cellIter); + + const std::wstring_view operator*() const; + const std::wstring_view* operator->() const; + +protected: + +#if UNIT_TESTING + friend class TextBufferIteratorTests; +#endif +}; diff --git a/src/buffer/out/ut_textbuffer/DefaultResource.rc b/src/buffer/out/ut_textbuffer/DefaultResource.rc new file mode 100644 index 000000000..b9c0c585f --- /dev/null +++ b/src/buffer/out/ut_textbuffer/DefaultResource.rc @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//Autogenerated file name + version resource file for Device Guard whitelisting effort + +#include +#include + +#define VER_FILETYPE VFT_UNKNOWN +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR ___TARGETNAME +#define VER_INTERNALNAME_STR ___TARGETNAME +#define VER_ORIGINALFILENAME_STR ___TARGETNAME + +#include "common.ver" diff --git a/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp new file mode 100644 index 000000000..090680304 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/TextAttributeTests.cpp @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "../TextAttribute.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class TextAttributeTests +{ + TEST_CLASS(TextAttributeTests); + TEST_CLASS_SETUP(ClassSetup); + + TEST_METHOD(TestRoundtripLegacy); + TEST_METHOD(TestRoundtripMetaBits); + TEST_METHOD(TestRoundtripExhaustive); + TEST_METHOD(TestTextAttributeColorGetters); + TEST_METHOD(TestReverseDefaultColors); + + static const int COLOR_TABLE_SIZE = 16; + COLORREF _colorTable[COLOR_TABLE_SIZE]; + COLORREF _defaultFg = RGB(1, 2, 3); + COLORREF _defaultBg = RGB(4, 5, 6); + std::basic_string_view _GetTableView(); +}; + +bool TextAttributeTests::ClassSetup() +{ + _colorTable[0] = RGB(12, 12, 12); // Black + _colorTable[1] = RGB(0, 55, 218); // Dark Blue + _colorTable[2] = RGB(19, 161, 14); // Dark Green + _colorTable[3] = RGB(58, 150, 221); // Dark Cyan + _colorTable[4] = RGB(197, 15, 31); // Dark Red + _colorTable[5] = RGB(136, 23, 152); // Dark Magenta + _colorTable[6] = RGB(193, 156, 0); // Dark Yellow + _colorTable[7] = RGB(204, 204, 204); // Dark White + _colorTable[8] = RGB(118, 118, 118); // Bright Black + _colorTable[9] = RGB(59, 120, 255); // Bright Blue + _colorTable[10] = RGB(22, 198, 12); // Bright Green + _colorTable[11] = RGB(97, 214, 214); // Bright Cyan + _colorTable[12] = RGB(231, 72, 86); // Bright Red + _colorTable[13] = RGB(180, 0, 158); // Bright Magenta + _colorTable[14] = RGB(249, 241, 165); // Bright Yellow + _colorTable[15] = RGB(242, 242, 242); // White + return true; +} + +std::basic_string_view TextAttributeTests::_GetTableView() +{ + return std::basic_string_view(&_colorTable[0], COLOR_TABLE_SIZE); +} + +void TextAttributeTests::TestRoundtripLegacy() +{ + WORD expectedLegacy = FOREGROUND_BLUE | BACKGROUND_RED; + WORD bgOnly = expectedLegacy & BG_ATTRS; + WORD bgShifted = bgOnly >> 4; + BYTE bgByte = (BYTE)(bgShifted); + + VERIFY_ARE_EQUAL(FOREGROUND_RED, bgByte); + + auto attr = TextAttribute(expectedLegacy); + + VERIFY_IS_TRUE(attr.IsLegacy()); + VERIFY_ARE_EQUAL(expectedLegacy, attr.GetLegacyAttributes()); +} + +void TextAttributeTests::TestRoundtripMetaBits() +{ + WORD metaFlags[] = + { + COMMON_LVB_GRID_HORIZONTAL, + COMMON_LVB_GRID_LVERTICAL, + COMMON_LVB_GRID_RVERTICAL, + COMMON_LVB_REVERSE_VIDEO, + COMMON_LVB_UNDERSCORE + }; + + for (int i = 0; i < ARRAYSIZE(metaFlags); ++i) + { + WORD flag = metaFlags[i]; + WORD expectedLegacy = FOREGROUND_BLUE | BACKGROUND_RED | flag; + WORD metaOnly = expectedLegacy & META_ATTRS; + VERIFY_ARE_EQUAL(flag, metaOnly); + + auto attr = TextAttribute(expectedLegacy); + VERIFY_IS_TRUE(attr.IsLegacy()); + VERIFY_ARE_EQUAL(expectedLegacy, attr.GetLegacyAttributes()); + VERIFY_ARE_EQUAL(flag, attr._wAttrLegacy); + } +} + +void TextAttributeTests::TestRoundtripExhaustive() +{ + WORD allAttrs = (META_ATTRS | FG_ATTRS | BG_ATTRS); + // This test covers some 0xdfff test cases, printing out Verify: IsTrue for + // each takes a lot longer than checking. + // Only VERIFY if the comparison actually fails to speed up the test. + Log::Comment(L"This test will check each possible legacy attribute to make " + "sure it roundtrips through the creation of a text attribute."); + Log::Comment(L"It will only log if it fails."); + for (WORD wLegacy = 0; wLegacy < allAttrs; wLegacy++) + { + // 0x2000 is not an actual meta attribute + // COMMON_LVB_TRAILING_BYTE and COMMON_LVB_TRAILING_BYTE are no longer + // stored in the TextAttributes, they're stored in the CharRow + if (WI_IsFlagSet(wLegacy, 0x2000) || + WI_IsFlagSet(wLegacy, COMMON_LVB_LEADING_BYTE) || + WI_IsFlagSet(wLegacy, COMMON_LVB_TRAILING_BYTE)) + { + continue; + } + + auto attr = TextAttribute(wLegacy); + + bool isLegacy = attr.IsLegacy(); + bool areEqual = (wLegacy == attr.GetLegacyAttributes()); + if (!(isLegacy && areEqual)) + { + Log::Comment(NoThrowString().Format( + L"Failed on wLegacy=0x%x", wLegacy + )); + VERIFY_IS_TRUE(attr.IsLegacy()); + VERIFY_ARE_EQUAL(wLegacy, attr.GetLegacyAttributes()); + } + } +} + +void TextAttributeTests::TestTextAttributeColorGetters() +{ + const COLORREF red = RGB(255, 0, 0); + const COLORREF green = RGB(0, 255, 0); + TextAttribute attr(red, green); + auto view = _GetTableView(); + + // verify that calculated foreground/background are the same as the direct + // values when reverse video is not set + VERIFY_IS_FALSE(attr._IsReverseVideo()); + + VERIFY_ARE_EQUAL(red, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(red, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(green, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(green, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); + + // with reverse video set, calucated foreground/background values should be + // switched while getters stay the same + attr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO); + + VERIFY_ARE_EQUAL(red, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(green, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(green, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); +} + +void TextAttributeTests::TestReverseDefaultColors() +{ + const COLORREF red = RGB(255, 0, 0); + const COLORREF green = RGB(0, 255, 0); + TextAttribute attr{}; + auto view = _GetTableView(); + + // verify that calculated foreground/background are the same as the direct + // values when reverse video is not set + VERIFY_IS_FALSE(attr._IsReverseVideo()); + + VERIFY_ARE_EQUAL(_defaultFg, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(_defaultBg, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); + + // with reverse video set, calucated foreground/background values should be + // switched while getters stay the same + attr.SetMetaAttributes(COMMON_LVB_REVERSE_VIDEO); + VERIFY_IS_TRUE(attr._IsReverseVideo()); + + VERIFY_ARE_EQUAL(_defaultFg, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(_defaultBg, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); + + attr.SetForeground(red); + VERIFY_IS_TRUE(attr._IsReverseVideo()); + + VERIFY_ARE_EQUAL(red, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultBg, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(_defaultBg, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(red, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); + + attr.Invert(); + VERIFY_IS_FALSE(attr._IsReverseVideo()); + attr.SetDefaultForeground(); + attr.SetBackground(green); + + VERIFY_ARE_EQUAL(_defaultFg, attr._GetRgbForeground(view, _defaultFg)); + VERIFY_ARE_EQUAL(_defaultFg, attr.CalculateRgbForeground(view, _defaultFg, _defaultBg)); + + VERIFY_ARE_EQUAL(green, attr._GetRgbBackground(view, _defaultBg)); + VERIFY_ARE_EQUAL(green, attr.CalculateRgbBackground(view, _defaultFg, _defaultBg)); +} diff --git a/src/buffer/out/ut_textbuffer/TextBuffer.Unittests.vcxproj b/src/buffer/out/ut_textbuffer/TextBuffer.Unittests.vcxproj new file mode 100644 index 000000000..e763c0f91 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/TextBuffer.Unittests.vcxproj @@ -0,0 +1,36 @@ + + + + + + + + + Create + + + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + + + + + {531C23E7-4B76-4C08-8BBD-04164CB628C9} + Win32Proj + TextBufferUnitTests + TextBuffer.UnitTests + TextBuffer.UnitTests + + + + ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories) + + + + + + + \ No newline at end of file diff --git a/src/buffer/out/ut_textbuffer/TextColorTests.cpp b/src/buffer/out/ut_textbuffer/TextColorTests.cpp new file mode 100644 index 000000000..dc5c23adb --- /dev/null +++ b/src/buffer/out/ut_textbuffer/TextColorTests.cpp @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "../TextColor.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class TextColorTests +{ + TEST_CLASS(TextColorTests); + + TEST_CLASS_SETUP(ClassSetup); + + TEST_METHOD(TestDefaultColor); + TEST_METHOD(TestDarkIndexColor); + TEST_METHOD(TestBrightIndexColor); + TEST_METHOD(TestRgbColor); + TEST_METHOD(TestChangeColor); + + static const int COLOR_TABLE_SIZE = 16; + COLORREF _colorTable[COLOR_TABLE_SIZE]; + COLORREF _defaultFg = RGB(1, 2, 3); + COLORREF _defaultBg = RGB(4, 5, 6); + std::basic_string_view _GetTableView(); +}; + +bool TextColorTests::ClassSetup() +{ + _colorTable[0] = RGB(12, 12, 12); // Black + _colorTable[1] = RGB(0, 55, 218); // Dark Blue + _colorTable[2] = RGB(19, 161, 14); // Dark Green + _colorTable[3] = RGB(58, 150, 221); // Dark Cyan + _colorTable[4] = RGB(197, 15, 31); // Dark Red + _colorTable[5] = RGB(136, 23, 152); // Dark Magenta + _colorTable[6] = RGB(193, 156, 0); // Dark Yellow + _colorTable[7] = RGB(204, 204, 204); // Dark White + _colorTable[8] = RGB(118, 118, 118); // Bright Black + _colorTable[9] = RGB(59, 120, 255); // Bright Blue + _colorTable[10] = RGB(22, 198, 12); // Bright Green + _colorTable[11] = RGB(97, 214, 214); // Bright Cyan + _colorTable[12] = RGB(231, 72, 86); // Bright Red + _colorTable[13] = RGB(180, 0, 158); // Bright Magenta + _colorTable[14] = RGB(249, 241, 165); // Bright Yellow + _colorTable[15] = RGB(242, 242, 242); // White + return true; +} + +std::basic_string_view TextColorTests::_GetTableView() +{ + return std::basic_string_view(&_colorTable[0], COLOR_TABLE_SIZE); +} + +void TextColorTests::TestDefaultColor() +{ + TextColor defaultColor; + + VERIFY_IS_TRUE(defaultColor.IsDefault()); + VERIFY_IS_FALSE(defaultColor.IsLegacy()); + VERIFY_IS_FALSE(defaultColor.IsRgb()); + + auto view = _GetTableView(); + + auto color = defaultColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_defaultFg, color); + + color = defaultColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_defaultFg, color); + + color = defaultColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_defaultBg, color); + + color = defaultColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_defaultBg, color); +} + +void TextColorTests::TestDarkIndexColor() +{ + TextColor indexColor((BYTE)(7)); + + VERIFY_IS_FALSE(indexColor.IsDefault()); + VERIFY_IS_TRUE(indexColor.IsLegacy()); + VERIFY_IS_FALSE(indexColor.IsRgb()); + + auto view = _GetTableView(); + + auto color = indexColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_colorTable[7], color); + + color = indexColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = indexColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_colorTable[7], color); + + color = indexColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); +} + +void TextColorTests::TestBrightIndexColor() +{ + TextColor indexColor((BYTE)(15)); + + VERIFY_IS_FALSE(indexColor.IsDefault()); + VERIFY_IS_TRUE(indexColor.IsLegacy()); + VERIFY_IS_FALSE(indexColor.IsRgb()); + + auto view = _GetTableView(); + + auto color = indexColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = indexColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = indexColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = indexColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); +} + +void TextColorTests::TestRgbColor() +{ + COLORREF myColor = RGB(7, 8, 9); + TextColor rgbColor(myColor); + + VERIFY_IS_FALSE(rgbColor.IsDefault()); + VERIFY_IS_FALSE(rgbColor.IsLegacy()); + VERIFY_IS_TRUE(rgbColor.IsRgb()); + + auto view = _GetTableView(); + + auto color = rgbColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(myColor, color); +} + +void TextColorTests::TestChangeColor() +{ + COLORREF myColor = RGB(7, 8, 9); + TextColor rgbColor(myColor); + + VERIFY_IS_FALSE(rgbColor.IsDefault()); + VERIFY_IS_FALSE(rgbColor.IsLegacy()); + VERIFY_IS_TRUE(rgbColor.IsRgb()); + + auto view = _GetTableView(); + + auto color = rgbColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(myColor, color); + + color = rgbColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(myColor, color); + + rgbColor.SetDefault(); + + color = rgbColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_defaultFg, color); + + color = rgbColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_defaultFg, color); + + color = rgbColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_defaultBg, color); + + color = rgbColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_defaultBg, color); + + rgbColor.SetIndex(7); + color = rgbColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_colorTable[7], color); + + color = rgbColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = rgbColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_colorTable[7], color); + + color = rgbColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + + rgbColor.SetIndex(15); + color = rgbColor.GetColor(view, _defaultFg, false); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = rgbColor.GetColor(view, _defaultFg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = rgbColor.GetColor(view, _defaultBg, false); + VERIFY_ARE_EQUAL(_colorTable[15], color); + + color = rgbColor.GetColor(view, _defaultBg, true); + VERIFY_ARE_EQUAL(_colorTable[15], color); +} diff --git a/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp b/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp new file mode 100644 index 000000000..9ba5aceac --- /dev/null +++ b/src/buffer/out/ut_textbuffer/UnicodeStorageTests.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "../UnicodeStorage.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class UnicodeStorageTests +{ + TEST_CLASS(UnicodeStorageTests); + + + TEST_METHOD(CanOverwriteEmoji) + { + UnicodeStorage storage; + const COORD coord{ 1, 3 }; + const std::vector newMoon{ 0xD83C, 0xDF11 }; + const std::vector fullMoon{ 0xD83C, 0xDF15 }; + + // store initial glyph + storage.StoreGlyph(coord, newMoon); + + // verify it was stored + auto findIt = storage._map.find(coord); + VERIFY_ARE_NOT_EQUAL(findIt, storage._map.end()); + const std::vector& newMoonGlyph = findIt->second; + VERIFY_ARE_EQUAL(newMoonGlyph.size(), newMoon.size()); + for (size_t i = 0; i < newMoon.size(); ++i) + { + VERIFY_ARE_EQUAL(newMoonGlyph.at(i), newMoon.at(i)); + } + + // overwrite it + storage.StoreGlyph(coord, fullMoon); + + // verify the glyph was overwritten + findIt = storage._map.find(coord); + VERIFY_ARE_NOT_EQUAL(findIt, storage._map.end()); + const std::vector& fullMoonGlyph = findIt->second; + VERIFY_ARE_EQUAL(fullMoonGlyph.size(), fullMoon.size()); + for (size_t i = 0; i < fullMoon.size(); ++i) + { + VERIFY_ARE_EQUAL(fullMoonGlyph.at(i), fullMoon.at(i)); + } + } +}; diff --git a/src/buffer/out/ut_textbuffer/precomp.cpp b/src/buffer/out/ut_textbuffer/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/buffer/out/ut_textbuffer/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/buffer/out/ut_textbuffer/precomp.h b/src/buffer/out/ut_textbuffer/precomp.h new file mode 100644 index 000000000..42155ece0 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/precomp.h @@ -0,0 +1,37 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#pragma warning(push) +#pragma warning(disable: ALL_CPPCORECHECK_WARNINGS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#endif + +// Windows Header Files: +#include +#include + +// private dependencies +#include "..\host\conddkrefs.h" +#include "..\inc\operators.hpp" +#include "..\inc\unicode.hpp" +#pragma warning(pop) diff --git a/src/buffer/out/ut_textbuffer/product.pbxproj b/src/buffer/out/ut_textbuffer/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/buffer/out/ut_textbuffer/sources b/src/buffer/out/ut_textbuffer/sources new file mode 100644 index 000000000..ba50be8e3 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/sources @@ -0,0 +1,31 @@ +!include ..\..\..\project.unittest.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.TextBuffer.UnitTests +TARGETTYPE = DYNLINK +DLLDEF = + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + TextColorTests.cpp \ + TextAttributeTests.cpp \ + DefaultResource.rc \ + +TARGETLIBS = \ + $(CONSOLE_OBJ_PATH)\buffer\out\lib\$(O)\ConBufferOut.lib \ + $(TARGETLIBS) \ + +# ------------------------------------- +# Localization +# ------------------------------------- + +# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe. +C_DEFINES = $(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)""" +MUI_VERIFY_NO_LOC_RESOURCE = 1 diff --git a/src/buffer/out/ut_textbuffer/sources.dep b/src/buffer/out/ut_textbuffer/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/buffer/out/ut_textbuffer/testmd.definition b/src/buffer/out/ut_textbuffer/testmd.definition new file mode 100644 index 000000000..2f5ebc488 --- /dev/null +++ b/src/buffer/out/ut_textbuffer/testmd.definition @@ -0,0 +1,18 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "TextBuffer-UnitTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ ] + }, + "Logs": [ ], + "Plugins": [ ] +} diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj new file mode 100644 index 000000000..832a6516f --- /dev/null +++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj @@ -0,0 +1,159 @@ + + + + + + + + + 0 + 1 + + + + 10.0.17763.0 + 10.0.17763.0 + + false + false + + + + CA5CAD1A-224A-4171-B13A-F16E576FDD12 + ..\WindowsTerminal\WindowsTerminal.vcxproj + + + + false + Never + + + + true + False + CascadiaPackage_TemporaryKey.pfx + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + $(WapProjBeforeGenerateAppxManifestDependsOn); + _ConsoleInjectMUXWinmdIntoReferences; + + + + + <_WinmdFilesFromReferences Include="@(Reference)" Condition="'%(Reference.Filename)' == 'Microsoft.UI.Xaml' and '%(Reference.Extension)' == '.winmd'" /> + + + + + + + + + + +   +     + +      <_TemporaryFilteredWapProjOutput + Include="@(_FilteredNonWapProjProjectOutput)" /> +      <_FilteredNonWapProjProjectOutput Remove="@(_TemporaryFilteredWapProjOutput)" /> +      <_FilteredNonWapProjProjectOutput Include="@(_TemporaryFilteredWapProjOutput)"> + + conhost.exe + +         +       +     +   + + diff --git a/src/cascadia/CascadiaPackage/Images/LargeTile.scale-100.png b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-100.png new file mode 100644 index 000000000..9dc3142df Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/LargeTile.scale-125.png b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-125.png new file mode 100644 index 000000000..5e8f1ef70 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/LargeTile.scale-150.png b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-150.png new file mode 100644 index 000000000..a1a56a21e Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/LargeTile.scale-200.png b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-200.png new file mode 100644 index 000000000..187c11497 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/LargeTile.scale-400.png b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-400.png new file mode 100644 index 000000000..5e4fcd644 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LargeTile.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/LockScreenLogo.scale-200.png b/src/cascadia/CascadiaPackage/Images/LockScreenLogo.scale-200.png new file mode 100644 index 000000000..049e9f3f5 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/LockScreenLogo.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SmallTile.scale-100.png b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-100.png new file mode 100644 index 000000000..04fc0ef9e Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SmallTile.scale-125.png b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-125.png new file mode 100644 index 000000000..9697ca7a0 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SmallTile.scale-150.png b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-150.png new file mode 100644 index 000000000..0053392c9 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SmallTile.scale-200.png b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-200.png new file mode 100644 index 000000000..354ee74e2 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SmallTile.scale-400.png b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-400.png new file mode 100644 index 000000000..d910a79ca Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SmallTile.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-100.png b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-100.png new file mode 100644 index 000000000..2817a1fb6 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-125.png b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-125.png new file mode 100644 index 000000000..f47191372 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-150.png b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-150.png new file mode 100644 index 000000000..14bbabe61 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-200.png b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-200.png new file mode 100644 index 000000000..277cc6345 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-400.png b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-400.png new file mode 100644 index 000000000..6ba469bce Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/SplashScreen.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-100.png b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-100.png new file mode 100644 index 000000000..cb2206156 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-125.png b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-125.png new file mode 100644 index 000000000..072b5a266 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-150.png b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-150.png new file mode 100644 index 000000000..21c55017a Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-200.png b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-200.png new file mode 100644 index 000000000..9809eea3b Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-400.png b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-400.png new file mode 100644 index 000000000..3348a8de3 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square150x150Logo.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-16.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 000000000..c6ba2a8cf Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-256.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 000000000..95891c26d Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-32.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 000000000..7ea2557f5 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-48.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 000000000..a2d75e6a2 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-100.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-100.png new file mode 100644 index 000000000..f963d9744 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-125.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-125.png new file mode 100644 index 000000000..7918b1f17 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-150.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-150.png new file mode 100644 index 000000000..d08aaca1e Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-200.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-200.png new file mode 100644 index 000000000..74568d916 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-400.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-400.png new file mode 100644 index 000000000..cd966fc4e Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-16.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-16.png new file mode 100644 index 000000000..08b9ea134 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-16.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24.png new file mode 100644 index 000000000..a3d86e50e Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 000000000..ee78927eb Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-256.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-256.png new file mode 100644 index 000000000..5e2f32f44 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-256.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-32.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-32.png new file mode 100644 index 000000000..7121f26ea Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-32.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-48.png b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-48.png new file mode 100644 index 000000000..15a0929cf Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Square44x44Logo.targetsize-48.png differ diff --git a/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-100.png b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-100.png new file mode 100644 index 000000000..2f7faf804 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-125.png b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-125.png new file mode 100644 index 000000000..4774dd8fa Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-150.png b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-150.png new file mode 100644 index 000000000..32bfb681b Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-200.png b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-200.png new file mode 100644 index 000000000..648441928 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-400.png b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-400.png new file mode 100644 index 000000000..d97816577 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/StoreLogo.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-100.png b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-100.png new file mode 100644 index 000000000..e4c22ff15 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-100.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-125.png b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-125.png new file mode 100644 index 000000000..f870fd556 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-125.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-150.png b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-150.png new file mode 100644 index 000000000..8090686f3 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-150.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-200.png b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-200.png new file mode 100644 index 000000000..21489524a Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-200.png differ diff --git a/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-400.png b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-400.png new file mode 100644 index 000000000..277cc6345 Binary files /dev/null and b/src/cascadia/CascadiaPackage/Images/Wide310x150Logo.scale-400.png differ diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest new file mode 100644 index 000000000..92ce4eb4a --- /dev/null +++ b/src/cascadia/CascadiaPackage/Package.appxmanifest @@ -0,0 +1,63 @@ + + + + + + + + ms-resource:AppName + Microsoft Corporation + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw b/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw new file mode 100644 index 000000000..f759e568a --- /dev/null +++ b/src/cascadia/CascadiaPackage/Resources/en-US/Resources.resw @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Codename Cascadia + + + Windows Terminal (Preview) + + \ No newline at end of file diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.def b/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.vcxproj b/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.vcxproj new file mode 100644 index 000000000..a5a61ab39 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/Microsoft.UI.Xaml.Markup.vcxproj @@ -0,0 +1,68 @@ + + + + + + + + + DynamicLibrary + Console + + true + + + + + {015a0047-772d-4f1a-88c9-45c18f0adfb6} + Microsoft.UI.Xaml.Markup + Microsoft.UI.Xaml.Markup + + + + + $(OutDir)\$(TargetName)$(TargetExt) + WindowsApp.lib;Kernel32.lib;User32.lib;%(AdditionalDependencies) + + + + + + + XamlApplication.idl + + + + + + Create + + + XamlApplication.idl + + + + + + + + + + + + + + + + + true + + + + + + diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/ReadMe.md b/src/cascadia/Microsoft.UI.Xaml.Markup/ReadMe.md new file mode 100644 index 000000000..089dc8d5c --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/ReadMe.md @@ -0,0 +1,22 @@ +# Xaml Application for Win32 + +This project implements an Xaml application that is suited for Win32 projects. +A pseudo implementation of this object is: +``` +namespace Microsoft.UI.Xaml.Markup +{ + interface IXamlMetadataProviderContainer + { + Windows.Foundation.Collections.IVector Providers { get; }; + }; + + class XamlApplication : Windows.UI.Xaml.Application, IXamlMetadataProviderContainer, IDisposable + { + XamlApplication() + { + WindowsXamlManager.InitializeForThread(); + } + } +} +``` + diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.cpp b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.cpp new file mode 100644 index 000000000..9182ab920 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include "XamlApplication.h" + +namespace xaml = ::winrt::Windows::UI::Xaml; + +extern "C" { + WINBASEAPI HMODULE WINAPI LoadLibraryExW(_In_ LPCWSTR lpLibFileName, _Reserved_ HANDLE hFile, _In_ DWORD dwFlags); + WINBASEAPI HMODULE WINAPI GetModuleHandleW(_In_opt_ LPCWSTR lpModuleName); + WINUSERAPI BOOL WINAPI PeekMessageW(_Out_ LPMSG lpMsg, _In_opt_ HWND hWnd, _In_ UINT wMsgFilterMin, _In_ UINT wMsgFilterMax, _In_ UINT wRemoveMsg); + WINUSERAPI LRESULT WINAPI DispatchMessageW(_In_ CONST MSG* lpMsg); +} + +namespace winrt::Microsoft::UI::Xaml::Markup::implementation +{ + XamlApplication::XamlApplication(winrt::Windows::UI::Xaml::Markup::IXamlMetadataProvider parentProvider) + { + m_providers.Append(parentProvider); + } + + XamlApplication::XamlApplication() + { + } + + void XamlApplication::Close() + { + if (m_bIsClosed) + { + return; + } + + m_bIsClosed = true; + + m_providers.Clear(); + + Exit(); + { + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } + } + } + + XamlApplication::~XamlApplication() + { + Close(); + } + + xaml::Markup::IXamlType XamlApplication::GetXamlType(xaml::Interop::TypeName const& type) + { + for (const auto& provider : m_providers) + { + const auto xamlType = provider.GetXamlType(type); + if (xamlType != nullptr) + { + return xamlType; + } + } + + return nullptr; + } + + xaml::Markup::IXamlType XamlApplication::GetXamlType(winrt::hstring const& fullName) + { + for (const auto& provider : m_providers) + { + const auto xamlType = provider.GetXamlType(fullName); + if (xamlType != nullptr) + { + return xamlType; + } + } + + return nullptr; + } + + winrt::com_array XamlApplication::GetXmlnsDefinitions() + { + std::list definitions; + for (const auto& provider : m_providers) + { + auto defs = provider.GetXmlnsDefinitions(); + for (const auto& def : defs) + { + definitions.insert(definitions.begin(), def); + } + } + + return winrt::com_array(definitions.begin(), definitions.end()); + } + + winrt::Windows::Foundation::Collections::IVector XamlApplication::Providers() + { + return m_providers; + } +} + +namespace winrt::Microsoft::UI::Xaml::Markup::factory_implementation +{ + XamlApplication::XamlApplication() + { + // Workaround a bug where twinapi.appcore.dll and threadpoolwinrt.dll gets loaded after it has been unloaded + // because of a call to GetActivationFactory + const wchar_t* preloadDlls[] = { + L"twinapi.appcore.dll", + L"threadpoolwinrt.dll", + }; + for (auto dllName : preloadDlls) + { + const auto module = ::LoadLibraryExW(dllName, nullptr, 0); + m_preloadInstances.push_back(module); + } + } + + XamlApplication::~XamlApplication() + { + for (auto module : m_preloadInstances) + { + ::FreeLibrary(module); + } + m_preloadInstances.clear(); + } +} diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.h b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.h new file mode 100644 index 000000000..af9823f6f --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "XamlApplication.g.h" +#include +#include +#include +#include + +namespace winrt::Microsoft::UI::Xaml::Markup::implementation +{ + class XamlApplication : public XamlApplicationT + { + public: + XamlApplication(); + XamlApplication(winrt::Windows::UI::Xaml::Markup::IXamlMetadataProvider parentProvider); + ~XamlApplication(); + + void Close(); + + winrt::Windows::UI::Xaml::Markup::IXamlType GetXamlType(winrt::Windows::UI::Xaml::Interop::TypeName const& type); + winrt::Windows::UI::Xaml::Markup::IXamlType GetXamlType(winrt::hstring const& fullName); + winrt::com_array GetXmlnsDefinitions(); + + winrt::Windows::Foundation::Collections::IVector Providers(); + + private: + winrt::Windows::Foundation::Collections::IVector m_providers = winrt::single_threaded_vector(); + bool m_bIsClosed = false; + }; +} + +namespace winrt::Microsoft::UI::Xaml::Markup::factory_implementation +{ + class XamlApplication : public XamlApplicationT + { + public: + XamlApplication(); + ~XamlApplication(); + private: + std::vector m_preloadInstances; + }; +} diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.idl b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.idl new file mode 100644 index 000000000..19ba61752 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/XamlApplication.idl @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.UI.Xaml.Markup +{ + interface IXamlMetadataProviderContainer + { + Windows.Foundation.Collections.IVector Providers { get; }; + }; + + [default_interface] + unsealed runtimeclass XamlApplication : Windows.UI.Xaml.Application, IXamlMetadataProviderContainer, Windows.Foundation.IClosable + { + XamlApplication(); + protected XamlApplication(Windows.UI.Xaml.Markup.IXamlMetadataProvider parentProvider); + } +} diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/packages.config b/src/cascadia/Microsoft.UI.Xaml.Markup/packages.config new file mode 100644 index 000000000..4f7a6f98a --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/pch.cpp b/src/cascadia/Microsoft.UI.Xaml.Markup/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/Microsoft.UI.Xaml.Markup/pch.h b/src/cascadia/Microsoft.UI.Xaml.Markup/pch.h new file mode 100644 index 000000000..eb8e9db80 --- /dev/null +++ b/src/cascadia/Microsoft.UI.Xaml.Markup/pch.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +// Exclude rarely-used stuff from Windows headers +#define WIN32_LEAN_AND_MEAN +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +// To enable support for non-WinRT interfaces, unknwn.h must be included before any C++/WinRT headers. +#include + +#include + diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp new file mode 100644 index 000000000..0d8f39b27 --- /dev/null +++ b/src/cascadia/TerminalApp/App.cpp @@ -0,0 +1,877 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "App.h" +#include +#include +#include + +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace ::TerminalApp; + +// Note: Generate GUID using TlgGuid.exe tool +TRACELOGGING_DEFINE_PROVIDER( + g_hTerminalAppProvider, + "Microsoft.Windows.Terminal.App", + // {24a1622f-7da7-5c77-3303-d850bd1ab2ed} + (0x24a1622f, 0x7da7, 0x5c77, 0x33, 0x03, 0xd8, 0x50, 0xbd, 0x1a, 0xb2, 0xed), + TraceLoggingOptionMicrosoftTelemetry()); + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; +} + +namespace winrt::TerminalApp::implementation +{ + + App::App() : + App(winrt::TerminalApp::XamlMetaDataProvider()) + { + } + + App::App(Windows::UI::Xaml::Markup::IXamlMetadataProvider const& parentProvider) : + base_type(parentProvider), + _settings{ }, + _tabs{ }, + _loadedInitialSettings{ false } + { + // For your own sanity, it's better to do setup outside the ctor. + // If you do any setup in the ctor that ends up throwing an exception, + // then it might look like App just failed to activate, which will + // cause you to chase down the rabbit hole of "why is App not + // registered?" when it definitely is. + } + + // Method Description: + // - Build the UI for the terminal app. Before this method is called, it + // should not be assumed that the TerminalApp is usable. The Settings + // should be loaded before this is called, either with LoadSettings or + // GetLaunchDimensions (which will call LoadSettings) + // Arguments: + // - + // Return Value: + // - + void App::Create() + { + // Assert that we've already loaded our settings. We have to do + // this as a MTA, before the app is Create()'d + WINRT_ASSERT(_loadedInitialSettings); + TraceLoggingRegister(g_hTerminalAppProvider); + _Create(); + } + + App::~App() + { + TraceLoggingUnregister(g_hTerminalAppProvider); + } + + // Method Description: + // - Create all of the initial UI elements of the Terminal app. + // * Creates the tab bar, initially hidden. + // * Creates the tab content area, which is where we'll display the tabs/panes. + // * Initializes the first terminal control, using the default profile, + // and adds it to our list of tabs. + void App::_Create() + { + _tabView = MUX::Controls::TabView{}; + + _tabView.SelectionChanged({ this, &App::_OnTabSelectionChanged }); + _tabView.TabClosing({ this, &App::_OnTabClosing }); + _tabView.Items().VectorChanged({ this, &App::_OnTabItemsChanged }); + + _root = Controls::Grid{}; + + _tabRow = Controls::Grid{}; + _tabRow.Name(L"Tab Row"); + _tabContent = Controls::Grid{}; + _tabContent.Name(L"Tab Content"); + + // Set up two columns in the tabs row - one for the tabs themselves, and + // another for the settings button. + auto tabsColDef = Controls::ColumnDefinition(); + auto newTabBtnColDef = Controls::ColumnDefinition(); + + newTabBtnColDef.Width(GridLengthHelper::Auto()); + + _tabRow.ColumnDefinitions().Append(tabsColDef); + _tabRow.ColumnDefinitions().Append(newTabBtnColDef); + + // Set up two rows - one for the tabs, the other for the tab content, + // the terminal panes. + auto tabBarRowDef = Controls::RowDefinition(); + tabBarRowDef.Height(GridLengthHelper::Auto()); + _root.RowDefinitions().Append(tabBarRowDef); + _root.RowDefinitions().Append(Controls::RowDefinition{}); + + if (_settings->GlobalSettings().GetShowTabsInTitlebar() == false) + { + _root.Children().Append(_tabRow); + Controls::Grid::SetRow(_tabRow, 0); + } + _root.Children().Append(_tabContent); + Controls::Grid::SetRow(_tabContent, 1); + Controls::Grid::SetColumn(_tabView, 0); + + // Create the new tab button. + _newTabButton = Controls::SplitButton{}; + Controls::SymbolIcon newTabIco{}; + newTabIco.Symbol(Controls::Symbol::Add); + _newTabButton.Content(newTabIco); + Controls::Grid::SetRow(_newTabButton, 0); + Controls::Grid::SetColumn(_newTabButton, 1); + _newTabButton.VerticalAlignment(VerticalAlignment::Stretch); + _newTabButton.HorizontalAlignment(HorizontalAlignment::Left); + + // When the new tab button is clicked, open the default profile + _newTabButton.Click([this](auto&&, auto&&){ + this->_OpenNewTab(std::nullopt); + }); + + // Populate the new tab button's flyout with entries for each profile + _CreateNewTabFlyout(); + + _tabRow.Children().Append(_tabView); + _tabRow.Children().Append(_newTabButton); + + _tabContent.VerticalAlignment(VerticalAlignment::Stretch); + _tabContent.HorizontalAlignment(HorizontalAlignment::Stretch); + + // Here, we're doing the equivalent of defining the _tabRow as the + // following: We need to set the Background + // to that ThemeResource, so it'll be colored appropriately regardless + // of what theme the user has selected. + // We're looking up the Style we've defined in App.xaml, and applying it + // here. A ResourceDictionary is a Map, so + // you'll need to try_as to get the type we actually want. + auto res = Resources(); + IInspectable key = winrt::box_value(L"BackgroundGridThemeStyle"); + if (res.HasKey(key)) + { + IInspectable g = res.Lookup(key); + winrt::Windows::UI::Xaml::Style style = g.try_as(); + _tabRow.Style(style); + } + + // Apply the UI theme from our settings to our UI elements + _ApplyTheme(_settings->GlobalSettings().GetRequestedTheme()); + + _OpenNewTab(std::nullopt); + } + + // Method Description: + // - Get the size in pixels of the client area we'll need to launch this + // terminal app. This method will use the default profile's settings to do + // this calculation, as well as the _system_ dpi scaling. See also + // TermControl::GetProposedDimensions. + // Arguments: + // - + // Return Value: + // - a point containing the requested dimensions in pixels. + winrt::Windows::Foundation::Point App::GetLaunchDimensions(uint32_t dpi) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + // Use the default profile to determine how big of a window we need. + TerminalSettings settings = _settings->MakeSettings(std::nullopt); + + // TODO MSFT:21150597 - If the global setting "Always show tab bar" is + // set, then we'll need to add the height of the tab bar here. + + return TermControl::GetProposedDimensions(settings, dpi); + } + + + bool App::GetShowTabsInTitlebar() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings->GlobalSettings().GetShowTabsInTitlebar(); + } + + // Method Description: + // - Builds the flyout (dropdown) attached to the new tab button, and + // attaches it to the button. Populates the flyout with one entry per + // Profile, displaying the profile's name. Clicking each flyout item will + // open a new tab with that profile. + // Below the profiles are the static menu items: settings, feedback + void App::_CreateNewTabFlyout() + { + auto newTabFlyout = Controls::MenuFlyout{}; + for (int profileIndex = 0; profileIndex < _settings->GetProfiles().size(); profileIndex++) + { + const auto& profile = _settings->GetProfiles()[profileIndex]; + auto profileMenuItem = Controls::MenuFlyoutItem{}; + + auto profileName = profile.GetName(); + winrt::hstring hName{ profileName }; + profileMenuItem.Text(hName); + + // If there's an icon set for this profile, set it as the icon for + // this flyout item. + if (profile.HasIcon()) + { + profileMenuItem.Icon(_GetIconFromProfile(profile)); + } + + profileMenuItem.Click([this, profileIndex](auto&&, auto&&){ + this->_OpenNewTab({ profileIndex }); + }); + newTabFlyout.Items().Append(profileMenuItem); + } + + // add menu separator + auto separatorItem = Controls::MenuFlyoutSeparator{}; + newTabFlyout.Items().Append(separatorItem); + + // add static items + { + // Create the settings button. + auto settingsItem = Controls::MenuFlyoutItem{}; + settingsItem.Text(L"Settings"); + + Controls::SymbolIcon ico{}; + ico.Symbol(Controls::Symbol::Setting); + settingsItem.Icon(ico); + + settingsItem.Click({ this, &App::_SettingsButtonOnClick }); + newTabFlyout.Items().Append(settingsItem); + + // Create the feedback button. + auto feedbackFlyout = Controls::MenuFlyoutItem{}; + feedbackFlyout.Text(L"Feedback"); + + Controls::FontIcon feedbackIco{}; + feedbackIco.Glyph(L"\xE939"); + feedbackIco.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); + feedbackFlyout.Icon(feedbackIco); + + feedbackFlyout.Click({ this, &App::_FeedbackButtonOnClick }); + newTabFlyout.Items().Append(feedbackFlyout); + } + + _newTabButton.Flyout(newTabFlyout); + } + + // Function Description: + // - Called when the settings button is clicked. ShellExecutes the settings + // file, as to open it in the default editor for .json files. Does this in + // a background thread, as to not hang/crash the UI thread. + fire_and_forget LaunchSettings() + { + // This will switch the execution of the function to a background (not + // UI) thread. This is IMPORTANT, because the Windows.Storage API's + // (used for retrieving the path to the file) will crash on the UI + // thread, because the main thread is a STA. + co_await winrt::resume_background(); + + const auto settingsPath = CascadiaSettings::GetSettingsPath(); + ShellExecute(nullptr, L"open", settingsPath.c_str(), nullptr, nullptr, SW_SHOW); + } + + // Method Description: + // - Called when the settings button is clicked. Launches a background + // thread to open the settings file in the default JSON editor. + // Arguments: + // - + // Return Value: + // - + void App::_SettingsButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + LaunchSettings(); + } + + // Method Description: + // - Called when the feedback button is clicked. Launches the feedback hub + // to the list of all feedback for the Terminal app. + void App::_FeedbackButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + // If you want this to go to the new feedback page automatically, use &newFeedback=true + winrt::Windows::System::Launcher::LaunchUriAsync({ L"feedback-hub://?tabid=2&appid=Microsoft.WindowsTerminal_8wekyb3d8bbwe!App" }); + + } + + // Method Description: + // - Register our event handlers with the given keybindings object. This + // should be done regardless of what the events are actually bound to - + // this simply ensures the AppKeyBindings object will call us correctly + // for each event. + // Arguments: + // - bindings: A AppKeyBindings object to wire up with our event handlers + void App::_HookupKeyBindings(TerminalApp::AppKeyBindings bindings) noexcept + { + // Hook up the KeyBinding object's events to our handlers. + // They should all be hooked up here, regardless of whether or not + // there's an actual keychord for them. + bindings.NewTab([this]() { _OpenNewTab(std::nullopt); }); + bindings.CloseTab([this]() { _CloseFocusedTab(); }); + bindings.NewTabWithProfile([this](const auto index) { _OpenNewTab({ index }); }); + bindings.ScrollUp([this]() { _DoScroll(-1); }); + bindings.ScrollDown([this]() { _DoScroll(1); }); + bindings.NextTab([this]() { _SelectNextTab(true); }); + bindings.PrevTab([this]() { _SelectNextTab(false); }); + } + + // Method Description: + // - Initialized our settings. See CascadiaSettings for more details. + // Additionally hooks up our callbacks for keybinding events to the + // keybindings object. + // NOTE: This must be called from a MTA if we're running as a packaged + // application. The Windows.Storage APIs require a MTA. If this isn't + // happening during startup, it'll need to happen on a background thread. + void App::LoadSettings() + { + _settings = CascadiaSettings::LoadAll(); + + _HookupKeyBindings(_settings->GetKeybindings()); + + _loadedInitialSettings = true; + + // Register for directory change notification. + _RegisterSettingsChange(); + } + + // Method Description: + // - Registers for changes to the settings folder and upon a updated settings + // profile calls _ReloadSettings(). + // Arguments: + // - + // Return Value: + // - + void App::_RegisterSettingsChange() + { + // Make sure this hstring has a stack-local reference. If we don't it + // might get cleaned up before we parse the path. + const auto localPathCopy = CascadiaSettings::GetSettingsPath(); + + // Getting the containing folder. + std::filesystem::path fileParser = localPathCopy.c_str(); + const auto folder = fileParser.parent_path(); + + _reader.create(folder.c_str(), false, wil::FolderChangeEvents::All, + [this](wil::FolderChangeEvent event, PCWSTR fileModified) + { + // We want file modifications, AND when files are renamed to be + // profiles.json. This second case will ofentimes happen with text + // editors, who will write a temp file, then rename it to be the + // actual file you wrote. So listen for that too. + if (!(event == wil::FolderChangeEvent::Modified || + event == wil::FolderChangeEvent::RenameNewName)) + { + return; + } + + const auto localPathCopy = CascadiaSettings::GetSettingsPath(); + std::filesystem::path settingsParser = localPathCopy.c_str(); + std::filesystem::path modifiedParser = fileModified; + + // Getting basename (filename.ext) + const auto settingsBasename = settingsParser.filename(); + const auto modifiedBasename = modifiedParser.filename(); + + if (settingsBasename == modifiedBasename) + { + this->_ReloadSettings(); + } + }); + } + + // Method Description: + // - Reloads the settings from the profile.json. + // Arguments: + // - + // Return Value: + // - + void App::_ReloadSettings() + { + _settings = CascadiaSettings::LoadAll(); + // Re-wire the keybindings to their handlers, as we'll have created a + // new AppKeyBindings object. + _HookupKeyBindings(_settings->GetKeybindings()); + + auto profiles = _settings->GetProfiles(); + + for (auto &profile : profiles) + { + const GUID profileGuid = profile.GetGuid(); + TerminalSettings settings = _settings->MakeSettings(profileGuid); + + for (auto &tab : _tabs) + { + const auto term = tab->GetTerminalControl(); + const GUID tabProfile = tab->GetProfile(); + + if (profileGuid == tabProfile) + { + term.UpdateSettings(settings); + + // Update the icons of the tabs with this profile open. + auto tabViewItem = tab->GetTabViewItem(); + tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [profile, tabViewItem]() { + // _GetIconFromProfile has to run on the main thread + tabViewItem.Icon(App::_GetIconFromProfile(profile)); + }); + } + } + } + + + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { + // Refresh the UI theme + _ApplyTheme(_settings->GlobalSettings().GetRequestedTheme()); + + // repopulate the new tab button's flyout with entries for each + // profile, which might have changed + _CreateNewTabFlyout(); + }); + + } + + // Method Description: + // - Update the current theme of the application. This will manually update + // all of the elements in our UI to match the given theme. + // Arguments: + // - newTheme: The ElementTheme to apply to our elements. + void App::_ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme) + { + _root.RequestedTheme(newTheme); + _tabRow.RequestedTheme(newTheme); + } + + UIElement App::GetRoot() noexcept + { + return _root; + } + + UIElement App::GetTabs() noexcept + { + return _tabRow; + } + + void App::_SetFocusedTabIndex(int tabIndex) + { + auto tab = _tabs.at(tabIndex); + _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [tab, this](){ + auto tabViewItem = tab->GetTabViewItem(); + _tabView.SelectedItem(tabViewItem); + }); + } + + // Method Description: + // - Handle changes in tab layout. + void App::_UpdateTabView() + { + // Show tabs when there's more than 1, or the user has chosen to always + // show the tab bar. + const bool isVisible = _settings->GlobalSettings().GetShowTabsInTitlebar() || + (_tabs.size() > 1) || + _settings->GlobalSettings().GetAlwaysShowTabs(); + + // collapse/show the tabs themselves + _tabView.Visibility(isVisible ? Visibility::Visible : Visibility::Collapsed); + + // collapse/show the row that the tabs are in. + // NaN is the special value XAML uses for "Auto" sizing. + _tabRow.Height(isVisible ? NAN : 0); + } + + // Method Description: + // - Open a new tab. This will create the TerminalControl hosting the + // terminal, and add a new Tab to our list of tabs. The method can + // optionally be provided a profile index, which will be used to create + // a tab using the profile in that index. + // If no index is provided, the default profile will be used. + // Arguments: + // - profileIndex: an optional index into the list of profiles to use to + // initialize this tab up with. + void App::_OpenNewTab(std::optional profileIndex) + { + GUID profileGuid; + + if (profileIndex) + { + const auto realIndex = profileIndex.value(); + const auto profiles = _settings->GetProfiles(); + + // If we don't have that many profiles, then do nothing. + if (realIndex >= profiles.size()) + { + return; + } + + const auto& selectedProfile = profiles[realIndex]; + profileGuid = selectedProfile.GetGuid(); + } + else + { + // Getting Guid for default profile + const auto globalSettings = _settings->GlobalSettings(); + profileGuid = globalSettings.GetDefaultProfile(); + } + + TerminalSettings settings = _settings->MakeSettings(profileGuid); + _CreateNewTabFromSettings(profileGuid, settings); + + const int tabCount = static_cast(_tabs.size()); + TraceLoggingWrite( + g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider + "TabInformation", + TraceLoggingDescription("Event emitted upon new tab creation in TerminalApp"), + TraceLoggingInt32(tabCount, "TabCount", "Count of tabs curently opened in TerminalApp"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + } + + // Function Description: + // - Copies and processes the text data from the Windows Clipboard. + // Does some of this in a background thread, as to not hang/crash the UI thread. + // Arguments: + // - eventArgs: the PasteFromClipboard event sent from the TermControl + fire_and_forget PasteFromClipboard(PasteFromClipboardEventArgs eventArgs) + { + const DataPackageView data = Clipboard::GetContent(); + + // This will switch the execution of the function to a background (not + // UI) thread. This is IMPORTANT, because the getting the clipboard data + // will crash on the UI thread, because the main thread is a STA. + co_await winrt::resume_background(); + + hstring text = L""; + if (data.Contains(StandardDataFormats::Text())) + { + text = co_await data.GetTextAsync(); + } + eventArgs.HandleClipboardData(text); + } + + // Method Description: + // - Creates a new tab with the given settings. If the tab bar is not being + // currently displayed, it will be shown. + // Arguments: + // - settings: the TerminalSettings object to use to create the TerminalControl with. + void App::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings) + { + // Initialize the new tab + TermControl term{ settings }; + + // Add an event handler when the terminal's selection wants to be copied. + // When the text buffer data is retrieved, we'll copy the data into the Clipboard + term.CopyToClipboard([=](auto copiedData) { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [copiedData]() { + DataPackage dataPack = DataPackage(); + dataPack.RequestedOperation(DataPackageOperation::Copy); + dataPack.SetText(copiedData); + Clipboard::SetContent(dataPack); + + // TODO: MSFT 20642290 and 20642291 + // rtf copy and html copy + }); + }); + + // Add an event handler when the terminal wants to paste data from the Clipboard. + term.PasteFromClipboard([=](auto /*sender*/, auto eventArgs) { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [eventArgs]() { + PasteFromClipboard(eventArgs); + }); + }); + + // Add the new tab to the list of our tabs. + auto newTab = _tabs.emplace_back(std::make_shared(profileGuid, term)); + + // Add an event handler when the terminal's title changes. When the + // title changes, we'll bubble it up to listeners of our own title + // changed event, so they can handle it. + newTab->GetTerminalControl().TitleChanged([=](auto newTitle){ + // Only bubble the change if this tab is the focused tab. + if (_settings->GlobalSettings().GetShowTitleInTitlebar() && + newTab->IsFocused()) + { + _titleChangeHandlers(newTitle); + } + }); + + auto tabViewItem = newTab->GetTabViewItem(); + _tabView.Items().Append(tabViewItem); + + const auto* const profile = _settings->FindProfile(profileGuid); + + // Set this profile's tab to the icon the user specified + if (profile != nullptr && profile->HasIcon()) + { + tabViewItem.Icon(_GetIconFromProfile(*profile)); + } + + // Add an event handler when the terminal's connection is closed. + newTab->GetTerminalControl().ConnectionClosed([=]() { + _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [newTab, tabViewItem, this]() { + const GUID tabProfile = newTab->GetProfile(); + // Don't just capture this pointer, because the profile might + // get destroyed before this is called (case in point - + // reloading settings) + const auto* const p = _settings->FindProfile(tabProfile); + + // TODO: MSFT:21268795: Need a better story for what should happen when the last tab is closed. + if (p != nullptr && p->GetCloseOnExit() && _tabs.size() > 1) + { + _RemoveTabViewItem(tabViewItem); + } + }); + }); + + tabViewItem.PointerPressed({ this, &App::_OnTabClick }); + + // This is one way to set the tab's selected background color. + // tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), a Brush?); + + // This kicks off TabView::SelectionChanged, in response to which we'll attach the terminal's + // Xaml control to the Xaml root. + _tabView.SelectedItem(tabViewItem); + } + + // Method Description: + // - Returns the index in our list of tabs of the currently focused tab. If + // no tab is currently selected, returns -1. + // Return Value: + // - the index of the currently focused tab if there is one, else -1 + int App::_GetFocusedTabIndex() const + { + return _tabView.SelectedIndex(); + } + + // Method Description: + // - Close the currently focused tab. Focus will move to the left, if possible. + void App::_CloseFocusedTab() + { + if (_tabs.size() > 1) + { + int focusedTabIndex = _GetFocusedTabIndex(); + std::shared_ptr focusedTab{ _tabs[focusedTabIndex] }; + + // We're not calling _FocusTab here because it makes an async dispatch + // that is practically guaranteed to not happen before we delete the tab. + _tabView.SelectedIndex((focusedTabIndex > 0) ? focusedTabIndex - 1 : 1); + _tabView.Items().RemoveAt(focusedTabIndex); + _tabs.erase(_tabs.begin() + focusedTabIndex); + } + } + + // Method Description: + // - Move the viewport of the terminal of the currently focused tab up or + // down a number of lines. Negative values of `delta` will move the + // view up, and positive values will move the viewport down. + // Arguments: + // - delta: a number of lines to move the viewport relative to the current viewport. + void App::_DoScroll(int delta) + { + int focusedTabIndex = _GetFocusedTabIndex(); + _tabs[focusedTabIndex]->Scroll(delta); + } + + // Method Description: + // - Copy text from the focused terminal to the Windows Clipboard + // Arguments: + // - trimTrailingWhitespace: enable removing any whitespace from copied selection + // and get text to appear on separate lines. + void App::_CopyText(const bool trimTrailingWhitespace) + { + const int focusedTabIndex = _GetFocusedTabIndex(); + std::shared_ptr focusedTab{ _tabs[focusedTabIndex] }; + + const auto control = focusedTab->GetTerminalControl(); + control.CopySelectionToClipboard(trimTrailingWhitespace); + } + + // Method Description: + // - Sets focus to the tab to the right or left the currently selected tab. + void App::_SelectNextTab(const bool bMoveRight) + { + int focusedTabIndex = _GetFocusedTabIndex(); + auto tabCount = _tabs.size(); + // Wraparound math. By adding tabCount and then calculating modulo tabCount, + // we clamp the values to the range [0, tabCount) while still supporting moving + // leftward from 0 to tabCount - 1. + _SetFocusedTabIndex( + (tabCount + focusedTabIndex + (bMoveRight ? 1 : -1)) % tabCount + ); + } + + // Method Description: + // - Responds to the TabView control's Selection Changed event (to move a + // new terminal control into focus.) + // Arguments: + // - sender: the control that originated this event + // - eventArgs: the event's constituent arguments + void App::_OnTabSelectionChanged(const IInspectable& sender, const Controls::SelectionChangedEventArgs& eventArgs) + { + auto tabView = sender.as(); + auto selectedIndex = tabView.SelectedIndex(); + + // Unfocus all the tabs. + for (auto tab : _tabs) + { + tab->SetFocused(false); + } + + if (selectedIndex >= 0) + { + try + { + auto tab = _tabs.at(selectedIndex); + auto control = tab->GetTerminalControl().GetControl(); + + _tabContent.Children().Clear(); + _tabContent.Children().Append(control); + + tab->SetFocused(true); + _titleChangeHandlers(GetTitle()); + } + CATCH_LOG(); + } + } + + // Method Description: + // - Responds to the TabView control's Tab Closing event by removing + // the indicated tab from the set and focusing another one. + // The event is cancelled so App maintains control over the + // items in the tabview. + // Arguments: + // - sender: the control that originated this event + // - eventArgs: the event's constituent arguments + void App::_OnTabClosing(const IInspectable& sender, const MUX::Controls::TabViewTabClosingEventArgs& eventArgs) + { + // Don't allow the user to close the last tab .. + // .. yet. + if (_tabs.size() > 1) + { + const auto tabViewItem = eventArgs.Item(); + _RemoveTabViewItem(tabViewItem); + } + // If we don't cancel the event, the TabView will remove the item itself. + eventArgs.Cancel(true); + } + + // Method Description: + // - Responds to changes in the TabView's item list by changing the tabview's + // visibility. + // Arguments: + // - sender: the control that originated this event + // - eventArgs: the event's constituent arguments + void App::_OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs) + { + _UpdateTabView(); + } + + // Method Description: + // - Gets the title of the currently focused terminal control. If there + // isn't a control selected for any reason, returns "Windows Terminal" + // Arguments: + // - + // Return Value: + // - the title of the focused control if there is one, else "Windows Terminal" + hstring App::GetTitle() + { + if (_settings->GlobalSettings().GetShowTitleInTitlebar()) + { + auto selectedIndex = _tabView.SelectedIndex(); + if (selectedIndex >= 0) + { + try + { + auto tab = _tabs.at(selectedIndex); + return tab->GetTerminalControl().Title(); + } + CATCH_LOG(); + } + } + return { L"Windows Terminal" }; + } + + // Method Description: + // - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click + // Arguments: + // - sender: the control that originated this event (TabViewItem) + // - eventArgs: the event's constituent arguments + void App::_OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs) + { + if (eventArgs.GetCurrentPoint(_root).Properties().IsMiddleButtonPressed()) + { + _RemoveTabViewItem(sender); + eventArgs.Handled(true); + } + } + + // Method Description: + // - Removes the tab (both TerminalControl and XAML) + // Arguments: + // - tabViewItem: the TabViewItem in the TabView that is being removed. + void App::_RemoveTabViewItem(const IInspectable& tabViewItem) + { + uint32_t tabIndexFromControl = 0; + _tabView.Items().IndexOf(tabViewItem, tabIndexFromControl); + + if (tabIndexFromControl == _GetFocusedTabIndex()) + { + _tabView.SelectedIndex((tabIndexFromControl > 0) ? tabIndexFromControl - 1 : 1); + } + + // Removing the tab from the collection will destroy its control and disconnect its connection. + _tabs.erase(_tabs.begin() + tabIndexFromControl); + _tabView.Items().RemoveAt(tabIndexFromControl); + } + + // Method Description: + // - Gets a colored IconElement for the profile in question. If the profile + // has an `icon` set in the settings, this will return an icon with that + // image in it. Otherwise it will return a nullptr-initialized + // IconElement. + // Arguments: + // - profile: the profile to get the icon from + // Return Value: + // - an IconElement for the profile's icon, if it has one. + Controls::IconElement App::_GetIconFromProfile(const Profile& profile) + { + if (profile.HasIcon()) + { + auto path = profile.GetIconPath(); + winrt::hstring iconPath{ path }; + winrt::Windows::Foundation::Uri iconUri{ iconPath }; + Controls::BitmapIconSource iconSource; + // Make sure to set this to false, so we keep the RGB data of the + // image. Otherwise, the icon will be white for all the + // non-transparent pixels in the image. + iconSource.ShowAsMonochrome(false); + iconSource.UriSource(iconUri); + Controls::IconSourceElement elem; + elem.IconSource(iconSource); + return elem; + } + else + { + return { nullptr }; + } + } + + // -------------------------------- WinRT Events --------------------------------- + // Winrt events need a method for adding a callback to the event and removing the callback. + // These macros will define them both for you. + DEFINE_EVENT(App, TitleChanged, _titleChangeHandlers, TerminalControl::TitleChangedEventArgs); +} diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h new file mode 100644 index 000000000..87b391574 --- /dev/null +++ b/src/cascadia/TerminalApp/App.h @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "Tab.h" +#include "CascadiaSettings.h" +#include "App.g.h" +#include "../../cascadia/inc/cppwinrt_utils.h" + +#include + +#include + +#include +#include +#include + +#include + +namespace winrt::TerminalApp::implementation +{ + + // We dont use AppT as it does not provide access to protected constructors + template + using AppT_Override = App_base; + + struct App : AppT_Override + { + public: + App(); + + Windows::UI::Xaml::UIElement GetRoot() noexcept; + Windows::UI::Xaml::UIElement GetTabs() noexcept; + void Create(); + void LoadSettings(); + + Windows::Foundation::Point GetLaunchDimensions(uint32_t dpi); + bool GetShowTabsInTitlebar(); + + ~App(); + + hstring GetTitle(); + + // -------------------------------- WinRT Events --------------------------------- + DECLARE_EVENT(TitleChanged, _titleChangeHandlers, winrt::Microsoft::Terminal::TerminalControl::TitleChangedEventArgs); + + private: + App(Windows::UI::Xaml::Markup::IXamlMetadataProvider const& parentProvider); + + // If you add controls here, but forget to null them either here or in + // the ctor, you're going to have a bad time. It'll mysteriously fail to + // activate the app. + // ALSO: If you add any UIElements as roots here, make sure they're + // updated in _ApplyTheme. The two roots currently are _root and _tabRow + // (which is a root when the tabs are in the titlebar.) + Windows::UI::Xaml::Controls::Grid _root{ nullptr }; + Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr }; + Windows::UI::Xaml::Controls::Grid _tabRow{ nullptr }; + Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; + Windows::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; + + std::vector> _tabs; + + std::unique_ptr<::TerminalApp::CascadiaSettings> _settings; + + bool _loadedInitialSettings; + + wil::unique_folder_change_reader_nothrow _reader; + + void _Create(); + void _CreateNewTabFlyout(); + + void _LoadSettings(); + void _HookupKeyBindings(TerminalApp::AppKeyBindings bindings) noexcept; + + void _RegisterSettingsChange(); + void _ReloadSettings(); + + void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _FeedbackButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + + void _UpdateTabView(); + + void _CreateNewTabFromSettings(GUID profileGuid, winrt::Microsoft::Terminal::Settings::TerminalSettings settings); + + void _OpenNewTab(std::optional profileIndex); + void _CloseFocusedTab(); + void _SelectNextTab(const bool bMoveRight); + + void _SetFocusedTabIndex(int tabIndex); + int _GetFocusedTabIndex() const; + + void _DoScroll(int delta); + void _CopyText(const bool trimTrailingWhitespace); + // Todo: add more event implementations here + // MSFT:20641986: Add keybindings for New Window + + void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); + void _OnTabClosing(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabClosingEventArgs& eventArgs); + void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); + void _OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); + + void _RemoveTabViewItem(const IInspectable& tabViewItem); + + void _ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme); + + static Windows::UI::Xaml::Controls::IconElement _GetIconFromProfile(const ::TerminalApp::Profile& profile); + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + struct App : AppT + { + }; +} diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl new file mode 100644 index 000000000..4194e2022 --- /dev/null +++ b/src/cascadia/TerminalApp/App.idl @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + [default_interface] + runtimeclass App : Microsoft.UI.Xaml.Markup.XamlApplication + { + App(); + + // For your own sanity, it's better to do setup outside the ctor. + // If you do any setup in the ctor that ends up throwing an exception, + // then it might look like TermApp just failed to activate, which will + // cause you to chase down the rabbit hole of "why is TermApp not + // registered?" when it definitely is. + void Create(); + + void LoadSettings(); + + Windows.UI.Xaml.UIElement GetRoot(); + Windows.UI.Xaml.UIElement GetTabs(); + + Windows.Foundation.Point GetLaunchDimensions(UInt32 dpi); + Boolean GetShowTabsInTitlebar(); + + event Microsoft.Terminal.TerminalControl.TitleChangedEventArgs TitleChanged; + + String GetTitle(); + + // IXamlMetadataProvider, Xaml.Application: + // Application's composing initializer will register the composing class + // as the toplevel "active" Xaml application. This Application (through IXamlMetadataProvider) + // will be queried for all unknown types during all Xaml and XBF (Xaml Binary Format) parse + // operations. + } +} diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml new file mode 100644 index 000000000..ce79d196b --- /dev/null +++ b/src/cascadia/TerminalApp/App.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalApp/AppKeyBindings.cpp b/src/cascadia/TerminalApp/AppKeyBindings.cpp new file mode 100644 index 000000000..85fee19a3 --- /dev/null +++ b/src/cascadia/TerminalApp/AppKeyBindings.cpp @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "AppKeyBindings.h" + +using namespace winrt::Microsoft::Terminal; + +namespace winrt::TerminalApp::implementation +{ + void AppKeyBindings::SetKeyBinding(TerminalApp::ShortcutAction const& action, + Settings::KeyChord const& chord) + { + _keyShortcuts[chord] = action; + } + + bool AppKeyBindings::TryKeyChord(Settings::KeyChord const& kc) + { + const auto keyIter = _keyShortcuts.find(kc); + if (keyIter != _keyShortcuts.end()) + { + const auto action = keyIter->second; + return _DoAction(action); + } + return false; + } + + bool AppKeyBindings::_DoAction(ShortcutAction action) + { + switch (action) + { + case ShortcutAction::CopyText: + _CopyTextHandlers(); + return true; + case ShortcutAction::PasteText: + _PasteTextHandlers(); + return true; + case ShortcutAction::NewTab: + _NewTabHandlers(); + return true; + + case ShortcutAction::NewTabProfile0: + _NewTabWithProfileHandlers(0); + return true; + case ShortcutAction::NewTabProfile1: + _NewTabWithProfileHandlers(1); + return true; + case ShortcutAction::NewTabProfile2: + _NewTabWithProfileHandlers(2); + return true; + case ShortcutAction::NewTabProfile3: + _NewTabWithProfileHandlers(3); + return true; + case ShortcutAction::NewTabProfile4: + _NewTabWithProfileHandlers(4); + return true; + case ShortcutAction::NewTabProfile5: + _NewTabWithProfileHandlers(5); + return true; + case ShortcutAction::NewTabProfile6: + _NewTabWithProfileHandlers(6); + return true; + case ShortcutAction::NewTabProfile7: + _NewTabWithProfileHandlers(7); + return true; + case ShortcutAction::NewTabProfile8: + _NewTabWithProfileHandlers(8); + return true; + case ShortcutAction::NewTabProfile9: + _NewTabWithProfileHandlers(9); + return true; + + case ShortcutAction::NewWindow: + _NewWindowHandlers(); + return true; + case ShortcutAction::CloseWindow: + _CloseWindowHandlers(); + return true; + case ShortcutAction::CloseTab: + _CloseTabHandlers(); + return true; + + case ShortcutAction::ScrollUp: + _ScrollUpHandlers(); + return true; + case ShortcutAction::ScrollDown: + _ScrollDownHandlers(); + return true; + + case ShortcutAction::NextTab: + _NextTabHandlers(); + return true; + case ShortcutAction::PrevTab: + _PrevTabHandlers(); + return true; + } + return false; + } + + // -------------------------------- Events --------------------------------- + DEFINE_EVENT(AppKeyBindings, CopyText, _CopyTextHandlers, TerminalApp::CopyTextEventArgs); + DEFINE_EVENT(AppKeyBindings, PasteText, _PasteTextHandlers, TerminalApp::PasteTextEventArgs); + DEFINE_EVENT(AppKeyBindings, NewTab, _NewTabHandlers, TerminalApp::NewTabEventArgs); + DEFINE_EVENT(AppKeyBindings, NewTabWithProfile, _NewTabWithProfileHandlers, TerminalApp::NewTabWithProfileEventArgs); + DEFINE_EVENT(AppKeyBindings, NewWindow, _NewWindowHandlers, TerminalApp::NewWindowEventArgs); + DEFINE_EVENT(AppKeyBindings, CloseWindow, _CloseWindowHandlers, TerminalApp::CloseWindowEventArgs); + DEFINE_EVENT(AppKeyBindings, CloseTab, _CloseTabHandlers, TerminalApp::CloseTabEventArgs); + DEFINE_EVENT(AppKeyBindings, SwitchToTab, _SwitchToTabHandlers, TerminalApp::SwitchToTabEventArgs); + DEFINE_EVENT(AppKeyBindings, NextTab, _NextTabHandlers, TerminalApp::NextTabEventArgs); + DEFINE_EVENT(AppKeyBindings, PrevTab, _PrevTabHandlers, TerminalApp::PrevTabEventArgs); + DEFINE_EVENT(AppKeyBindings, IncreaseFontSize, _IncreaseFontSizeHandlers, TerminalApp::IncreaseFontSizeEventArgs); + DEFINE_EVENT(AppKeyBindings, DecreaseFontSize, _DecreaseFontSizeHandlers, TerminalApp::DecreaseFontSizeEventArgs); + DEFINE_EVENT(AppKeyBindings, ScrollUp, _ScrollUpHandlers, TerminalApp::ScrollUpEventArgs); + DEFINE_EVENT(AppKeyBindings, ScrollDown, _ScrollDownHandlers, TerminalApp::ScrollDownEventArgs); + + +} diff --git a/src/cascadia/TerminalApp/AppKeyBindings.h b/src/cascadia/TerminalApp/AppKeyBindings.h new file mode 100644 index 000000000..800a72ea5 --- /dev/null +++ b/src/cascadia/TerminalApp/AppKeyBindings.h @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "AppKeyBindings.g.h" +#include "..\inc\cppwinrt_utils.h" + +namespace winrt::TerminalApp::implementation +{ + struct KeyChordHash + { + std::size_t operator()(const winrt::Microsoft::Terminal::Settings::KeyChord& key) const + { + std::hash keyHash; + std::hash modifiersHash; + std::size_t hashedKey = keyHash(key.Vkey()); + std::size_t hashedMods = modifiersHash(key.Modifiers()); + return hashedKey ^ hashedMods; + } + }; + + struct KeyChordEquality + { + bool operator()(const winrt::Microsoft::Terminal::Settings::KeyChord& lhs, const winrt::Microsoft::Terminal::Settings::KeyChord& rhs) const + { + return lhs.Modifiers() == rhs.Modifiers() && lhs.Vkey() == rhs.Vkey(); + } + }; + + struct AppKeyBindings : AppKeyBindingsT + { + AppKeyBindings() = default; + + bool TryKeyChord(winrt::Microsoft::Terminal::Settings::KeyChord const& kc); + void SetKeyBinding(TerminalApp::ShortcutAction const& action, winrt::Microsoft::Terminal::Settings::KeyChord const& chord); + + DECLARE_EVENT(CopyText, _CopyTextHandlers, TerminalApp::CopyTextEventArgs); + DECLARE_EVENT(PasteText, _PasteTextHandlers, TerminalApp::PasteTextEventArgs); + DECLARE_EVENT(NewTab, _NewTabHandlers, TerminalApp::NewTabEventArgs); + DECLARE_EVENT(NewTabWithProfile, _NewTabWithProfileHandlers, TerminalApp::NewTabWithProfileEventArgs); + DECLARE_EVENT(NewWindow, _NewWindowHandlers, TerminalApp::NewWindowEventArgs); + DECLARE_EVENT(CloseWindow, _CloseWindowHandlers, TerminalApp::CloseWindowEventArgs); + DECLARE_EVENT(CloseTab, _CloseTabHandlers, TerminalApp::CloseTabEventArgs); + DECLARE_EVENT(SwitchToTab, _SwitchToTabHandlers, TerminalApp::SwitchToTabEventArgs); + DECLARE_EVENT(NextTab, _NextTabHandlers, TerminalApp::NextTabEventArgs); + DECLARE_EVENT(PrevTab, _PrevTabHandlers, TerminalApp::PrevTabEventArgs); + DECLARE_EVENT(IncreaseFontSize, _IncreaseFontSizeHandlers, TerminalApp::IncreaseFontSizeEventArgs); + DECLARE_EVENT(DecreaseFontSize, _DecreaseFontSizeHandlers, TerminalApp::DecreaseFontSizeEventArgs); + DECLARE_EVENT(ScrollUp, _ScrollUpHandlers, TerminalApp::ScrollUpEventArgs); + DECLARE_EVENT(ScrollDown, _ScrollDownHandlers, TerminalApp::ScrollDownEventArgs); + + private: + std::unordered_map _keyShortcuts; + bool _DoAction(ShortcutAction action); + + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + struct AppKeyBindings : AppKeyBindingsT + { + }; +} diff --git a/src/cascadia/TerminalApp/AppKeyBindings.idl b/src/cascadia/TerminalApp/AppKeyBindings.idl new file mode 100644 index 000000000..00212eda6 --- /dev/null +++ b/src/cascadia/TerminalApp/AppKeyBindings.idl @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace TerminalApp +{ + enum ShortcutAction + { + CopyText = 0, + PasteText, + NewTab, + NewTabProfile0, + NewTabProfile1, + NewTabProfile2, + NewTabProfile3, + NewTabProfile4, + NewTabProfile5, + NewTabProfile6, + NewTabProfile7, + NewTabProfile8, + NewTabProfile9, + NewWindow, + CloseWindow, + CloseTab, + SwitchToTab, + NextTab, + PrevTab, + IncreaseFontSize, + DecreaseFontSize, + ScrollUp, + ScrollDown + }; + + delegate void CopyTextEventArgs(); + delegate void PasteTextEventArgs(); + delegate void NewTabEventArgs(); + delegate void NewTabWithProfileEventArgs(Int32 profileIndex); + delegate void NewWindowEventArgs(); + delegate void CloseWindowEventArgs(); + delegate void CloseTabEventArgs(); + delegate void SwitchToTabEventArgs(); + delegate void NextTabEventArgs(); + delegate void PrevTabEventArgs(); + delegate void IncreaseFontSizeEventArgs(); + delegate void DecreaseFontSizeEventArgs(); + delegate void ScrollUpEventArgs(); + delegate void ScrollDownEventArgs(); + + [default_interface] + runtimeclass AppKeyBindings : Microsoft.Terminal.Settings.IKeyBindings + { + AppKeyBindings(); + + void SetKeyBinding(ShortcutAction action, Microsoft.Terminal.Settings.KeyChord chord); + + event CopyTextEventArgs CopyText; + event PasteTextEventArgs PasteText; + event NewTabEventArgs NewTab; + event NewTabWithProfileEventArgs NewTabWithProfile; + event NewWindowEventArgs NewWindow; + event CloseWindowEventArgs CloseWindow; + event CloseTabEventArgs CloseTab; + event SwitchToTabEventArgs SwitchToTab; + event NextTabEventArgs NextTab; + event PrevTabEventArgs PrevTab; + event IncreaseFontSizeEventArgs IncreaseFontSize; + event DecreaseFontSizeEventArgs DecreaseFontSize; + event ScrollUpEventArgs ScrollUp; + event ScrollDownEventArgs ScrollDown; + } +} diff --git a/src/cascadia/TerminalApp/CascadiaSettings.cpp b/src/cascadia/TerminalApp/CascadiaSettings.cpp new file mode 100644 index 000000000..aa41e1602 --- /dev/null +++ b/src/cascadia/TerminalApp/CascadiaSettings.cpp @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include +#include +#include "CascadiaSettings.h" +#include "../../types/inc/utils.hpp" + +using namespace winrt::Microsoft::Terminal::Settings; +using namespace ::TerminalApp; +using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::TerminalApp; + +CascadiaSettings::CascadiaSettings() : + _globals{}, + _profiles{} +{ + +} + +CascadiaSettings::~CascadiaSettings() +{ + +} + +ColorScheme _CreateCampbellScheme() +{ + ColorScheme campbellScheme { L"Campbell", + RGB(242, 242, 242), + RGB(12, 12, 12) }; + auto& campbellTable = campbellScheme.GetTable(); + auto campbellSpan = gsl::span(&campbellTable[0], gsl::narrow(COLOR_TABLE_SIZE)); + Microsoft::Console::Utils::InitializeCampbellColorTable(campbellSpan); + Microsoft::Console::Utils::SetColorTableAlpha(campbellSpan, 0xff); + + return campbellScheme; +} + +ColorScheme _CreateSolarizedDarkScheme() +{ + + ColorScheme solarizedDarkScheme { L"Solarized Dark", + RGB(253, 246, 227), + RGB( 7, 54, 66) }; + auto& solarizedDarkTable = solarizedDarkScheme.GetTable(); + auto solarizedDarkSpan = gsl::span(&solarizedDarkTable[0], gsl::narrow(COLOR_TABLE_SIZE)); + solarizedDarkTable[0] = RGB( 7, 54, 66); + solarizedDarkTable[1] = RGB(211, 1, 2); + solarizedDarkTable[2] = RGB(133, 153, 0); + solarizedDarkTable[3] = RGB(181, 137, 0); + solarizedDarkTable[4] = RGB( 38, 139, 210); + solarizedDarkTable[5] = RGB(211, 54, 130); + solarizedDarkTable[6] = RGB( 42, 161, 152); + solarizedDarkTable[7] = RGB(238, 232, 213); + solarizedDarkTable[8] = RGB( 0, 43, 54); + solarizedDarkTable[9] = RGB(203, 75, 22); + solarizedDarkTable[10] = RGB( 88, 110, 117); + solarizedDarkTable[11] = RGB(101, 123, 131); + solarizedDarkTable[12] = RGB(131, 148, 150); + solarizedDarkTable[13] = RGB(108, 113, 196); + solarizedDarkTable[14] = RGB(147, 161, 161); + solarizedDarkTable[15] = RGB(253, 246, 227); + Microsoft::Console::Utils::SetColorTableAlpha(solarizedDarkSpan, 0xff); + + return solarizedDarkScheme; +} + +ColorScheme _CreateSolarizedLightScheme() +{ + ColorScheme solarizedLightScheme { L"Solarized Light", + RGB( 7, 54, 66), + RGB(253, 246, 227) }; + auto& solarizedLightTable = solarizedLightScheme.GetTable(); + auto solarizedLightSpan = gsl::span(&solarizedLightTable[0], gsl::narrow(COLOR_TABLE_SIZE)); + solarizedLightTable[0] = RGB( 7, 54, 66); + solarizedLightTable[1] = RGB(211, 1, 2); + solarizedLightTable[2] = RGB(133, 153, 0); + solarizedLightTable[3] = RGB(181, 137, 0); + solarizedLightTable[4] = RGB( 38, 139, 210); + solarizedLightTable[5] = RGB(211, 54, 130); + solarizedLightTable[6] = RGB( 42, 161, 152); + solarizedLightTable[7] = RGB(238, 232, 213); + solarizedLightTable[8] = RGB( 0, 43, 54); + solarizedLightTable[9] = RGB(203, 75, 22); + solarizedLightTable[10] = RGB( 88, 110, 117); + solarizedLightTable[11] = RGB(101, 123, 131); + solarizedLightTable[12] = RGB(131, 148, 150); + solarizedLightTable[13] = RGB(108, 113, 196); + solarizedLightTable[14] = RGB(147, 161, 161); + solarizedLightTable[15] = RGB(253, 246, 227); + Microsoft::Console::Utils::SetColorTableAlpha(solarizedLightSpan, 0xff); + + return solarizedLightScheme; +} + +// Method Description: +// - Create the set of schemes to use as the default schemes. Currently creates +// three default color schemes - Campbell (the new cmd color scheme), +// Solarized Dark and Solarized Light. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_CreateDefaultSchemes() +{ + _globals.GetColorSchemes().emplace_back(_CreateCampbellScheme()); + _globals.GetColorSchemes().emplace_back(_CreateSolarizedDarkScheme()); + _globals.GetColorSchemes().emplace_back(_CreateSolarizedLightScheme()); + +} + +// Method Description: +// - Create a set of profiles to use as the "default" profiles when initializing +// the terminal. Currently, we create two profiles: one for cmd.exe, and +// one for powershell. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_CreateDefaultProfiles() +{ + Profile defaultProfile{}; + defaultProfile.SetFontFace(L"Consolas"); + defaultProfile.SetCommandline(L"cmd.exe"); + defaultProfile.SetColorScheme({ L"Campbell" }); + defaultProfile.SetAcrylicOpacity(0.75); + defaultProfile.SetUseAcrylic(true); + defaultProfile.SetName(L"cmd"); + + _globals.SetDefaultProfile(defaultProfile.GetGuid()); + + Profile powershellProfile{}; + // If the user has installed PowerShell Core, we add PowerShell Core as a default. + // PowerShell Core default folder is "%PROGRAMFILES%\PowerShell\[Version]\". + std::wstring psCmdline = L"powershell.exe"; + std::filesystem::path psCoreCmdline{}; + if (_IsPowerShellCoreInstalled(L"%ProgramFiles%", psCoreCmdline)) + { + psCmdline = psCoreCmdline; + } + else if (_IsPowerShellCoreInstalled(L"%ProgramFiles(x86)%", psCoreCmdline)) + { + psCmdline = psCoreCmdline; + } + powershellProfile.SetFontFace(L"Courier New"); + powershellProfile.SetCommandline(psCmdline); + powershellProfile.SetColorScheme({ L"Campbell" }); + powershellProfile.SetDefaultBackground(RGB(1, 36, 86)); + powershellProfile.SetUseAcrylic(false); + powershellProfile.SetName(L"PowerShell"); + + _profiles.emplace_back(defaultProfile); + _profiles.emplace_back(powershellProfile); +} + +// Method Description: +// - Set up some default keybindings for the terminal. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_CreateDefaultKeybindings() +{ + AppKeyBindings keyBindings = _globals.GetKeybindings(); + // Set up spme basic default keybindings + // TODO:MSFT:20700157 read our settings from some source, and configure + // keychord,action pairings from that file + keyBindings.SetKeyBinding(ShortcutAction::NewTab, + KeyChord{ KeyModifiers::Ctrl, + static_cast('T') }); + + keyBindings.SetKeyBinding(ShortcutAction::CloseTab, + KeyChord{ KeyModifiers::Ctrl, + static_cast('W') }); + + keyBindings.SetKeyBinding(ShortcutAction::NextTab, + KeyChord{ KeyModifiers::Ctrl, + VK_TAB }); + + keyBindings.SetKeyBinding(ShortcutAction::PrevTab, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + VK_TAB }); + + // Yes these are offset by one. + // Ideally, you'd want C-S-1 to open the _first_ profile, which is index 0 + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile0, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('1') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile1, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('2') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile2, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('3') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile3, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('4') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile4, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('5') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile5, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('6') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile6, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('7') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile7, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('8') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile8, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('9') }); + keyBindings.SetKeyBinding(ShortcutAction::NewTabProfile9, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + static_cast('0') }); + + keyBindings.SetKeyBinding(ShortcutAction::ScrollUp, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + VK_PRIOR }); + keyBindings.SetKeyBinding(ShortcutAction::ScrollDown, + KeyChord{ KeyModifiers::Ctrl | KeyModifiers::Shift, + VK_NEXT }); +} + +// Method Description: +// - Initialize this object with default color schemes, profiles, and keybindings. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::_CreateDefaults() +{ + _CreateDefaultProfiles(); + _CreateDefaultSchemes(); + _CreateDefaultKeybindings(); +} + +// Method Description: +// - Finds a profile that matches the given GUID. If there is no profile in this +// settings object that matches, returns nullptr. +// Arguments: +// - profileGuid: the GUID of the profile to return. +// Return Value: +// - a non-ownership pointer to the profile matching the given guid, or nullptr +// if there is no match. +const Profile* CascadiaSettings::FindProfile(GUID profileGuid) const noexcept +{ + for (auto& profile : _profiles) + { + if (profile.GetGuid() == profileGuid) + { + return &profile; + } + } + return nullptr; +} + +// Method Description: +// - Create a TerminalSettings object from the given profile. +// If the profileGuidArg is not provided, this method will use the default +// profile. +// The TerminalSettings object that is created can be used to initialize both +// the Control's settings, and the Core settings of the terminal. +// Arguments: +// - profileGuidArg: an optional GUID to use to lookup the profile to create the +// settings from. If this arg is not provided, or the GUID does not match a +// profile, then this method will use the default profile. +// Return Value: +// - +TerminalSettings CascadiaSettings::MakeSettings(std::optional profileGuidArg) const +{ + GUID profileGuid = profileGuidArg ? profileGuidArg.value() : _globals.GetDefaultProfile(); + const Profile* const profile = FindProfile(profileGuid); + if (profile == nullptr) + { + throw E_INVALIDARG; + } + + TerminalSettings result = profile->CreateTerminalSettings(_globals.GetColorSchemes()); + + // Place our appropriate global settings into the Terminal Settings + _globals.ApplyToSettings(result); + + return result; +} + +// Method Description: +// - Returns an iterable collection of all of our Profiles. +// Arguments: +// - +// Return Value: +// - an iterable collection of all of our Profiles. +std::basic_string_view CascadiaSettings::GetProfiles() const noexcept +{ + return { &_profiles[0], _profiles.size() }; +} + +// Method Description: +// - Returns the globally configured keybindings +// Arguments: +// - +// Return Value: +// - the globally configured keybindings +AppKeyBindings CascadiaSettings::GetKeybindings() const noexcept +{ + return _globals.GetKeybindings(); +} + +// Method Description: +// - Get a reference to our global settings +// Arguments: +// - +// Return Value: +// - a reference to our global settings +GlobalAppSettings& CascadiaSettings::GlobalSettings() +{ + return _globals; +} + +// Function Description: +// - Returns true if the user has installed PowerShell Core. +// Arguments: +// - A string that contains an environment-variable string in the form: %variableName%. +// - A ref of a path that receives the result of PowerShell Core pwsh.exe full path. +// Return Value: +// - true or false. +bool CascadiaSettings::_IsPowerShellCoreInstalled(std::wstring_view programFileEnv, std::filesystem::path& cmdline) +{ + std::filesystem::path psCorePath = ExpandEnvironmentVariableString(programFileEnv.data()); + psCorePath /= L"PowerShell"; + if (std::filesystem::exists(psCorePath)) + { + for (auto& p : std::filesystem::directory_iterator(psCorePath)) + { + psCorePath = p.path(); + psCorePath /= L"pwsh.exe"; + if (std::filesystem::exists(psCorePath)) + { + cmdline = psCorePath; + return true; + } + } + } + return false; +} + +// Function Description: +// - Get a environment variable string. +// Arguments: +// - A string that contains an environment-variable string in the form: %variableName%. +// Return Value: +// - a string of the expending environment-variable string. +std::wstring CascadiaSettings::ExpandEnvironmentVariableString(std::wstring_view source) +{ + std::wstring result{}; + DWORD requiredSize = 0; + do + { + result.resize(requiredSize); + requiredSize = ::ExpandEnvironmentStringsW(source.data(), result.data(), result.size()); + } while (requiredSize != result.size()); + + // Trim the terminating null character + result.resize(requiredSize-1); + return result; +} diff --git a/src/cascadia/TerminalApp/CascadiaSettings.h b/src/cascadia/TerminalApp/CascadiaSettings.h new file mode 100644 index 000000000..102d20883 --- /dev/null +++ b/src/cascadia/TerminalApp/CascadiaSettings.h @@ -0,0 +1,72 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CascadiaSettings.hpp + +Abstract: +- This class acts as the container for all app settings. It's composed of two + parts: Globals, which are app-wide settings, and Profiles, which contain + a set of settings that apply to a single instance of the terminal. + Also contains the logic for serializing and deserializing this object. + +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once +#include +#include "GlobalAppSettings.h" +#include "Profile.h" + + +namespace TerminalApp +{ + class CascadiaSettings; +}; + +class TerminalApp::CascadiaSettings final +{ + +public: + CascadiaSettings(); + ~CascadiaSettings(); + + static std::unique_ptr LoadAll(const bool saveOnLoad = true); + void SaveAll() const; + + winrt::Microsoft::Terminal::Settings::TerminalSettings MakeSettings(std::optional profileGuid) const; + + GlobalAppSettings& GlobalSettings(); + + std::basic_string_view GetProfiles() const noexcept; + + winrt::TerminalApp::AppKeyBindings GetKeybindings() const noexcept; + + winrt::Windows::Data::Json::JsonObject ToJson() const; + static std::unique_ptr FromJson(winrt::Windows::Data::Json::JsonObject json); + + static winrt::hstring GetSettingsPath(); + + const Profile* FindProfile(GUID profileGuid) const noexcept; +private: + GlobalAppSettings _globals; + std::vector _profiles; + + + void _CreateDefaultKeybindings(); + void _CreateDefaultSchemes(); + void _CreateDefaultProfiles(); + void _CreateDefaults(); + + static bool _IsPackaged(); + static void _SaveAsPackagedApp(const winrt::hstring content); + static void _SaveAsUnpackagedApp(const winrt::hstring content); + static std::wstring _GetFullPathToUnpackagedSettingsFile(); + static winrt::hstring _GetPackagedSettingsPath(); + static std::optional _LoadAsPackagedApp(); + static std::optional _LoadAsUnpackagedApp(); + static bool _IsPowerShellCoreInstalled(std::wstring_view programFileEnv, std::filesystem::path& cmdline); + static std::wstring ExpandEnvironmentVariableString(std::wstring_view source); +}; diff --git a/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp new file mode 100644 index 000000000..5adc4b38d --- /dev/null +++ b/src/cascadia/TerminalApp/CascadiaSettingsSerialization.cpp @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include +#include "CascadiaSettings.h" +#include "../../types/inc/utils.hpp" +#include +#include +#include +#include + +using namespace ::TerminalApp; +using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::Data::Json; +using namespace winrt::Windows::Storage; +using namespace winrt::Windows::Storage::Streams; +using namespace ::Microsoft::Console; + +static const std::wstring FILENAME { L"profiles.json" }; +static const std::wstring SETTINGS_FOLDER_NAME{ L"\\Microsoft\\Windows Terminal\\" }; + +static const std::wstring PROFILES_KEY{ L"profiles" }; +static const std::wstring KEYBINDINGS_KEY{ L"keybindings" }; +static const std::wstring SCHEMES_KEY{ L"schemes" }; + +// Method Description: +// - Creates a CascadiaSettings from whatever's saved on disk, or instantiates +// a new one with the default values. If we're running as a packaged app, +// it will load the settings from our packaged localappdata. If we're +// running as an unpackaged application, it will read it from the path +// we've set under localappdata. +// Arguments: +// - saveOnLoad: If true, we'll write the settings back out after we load them, +// to make sure the schema is updated. +// Return Value: +// - a unique_ptr containing a new CascadiaSettings object. +std::unique_ptr CascadiaSettings::LoadAll(const bool saveOnLoad) +{ + std::unique_ptr resultPtr; + std::optional fileData = _IsPackaged() ? + _LoadAsPackagedApp() : _LoadAsUnpackagedApp(); + + const bool foundFile = fileData.has_value(); + if (foundFile) + { + const auto actualData = fileData.value(); + + JsonValue root{ nullptr }; + bool parsedSuccessfully = JsonValue::TryParse(actualData, root); + // TODO:MSFT:20737698 - Display an error if we failed to parse settings + if (parsedSuccessfully) + { + JsonObject obj = root.GetObjectW(); + resultPtr = FromJson(obj); + + // Update profile only if it has changed. + if (saveOnLoad) + { + const JsonObject json = resultPtr->ToJson(); + auto serializedSettings = json.Stringify(); + + if (actualData != serializedSettings) + { + resultPtr->SaveAll(); + } + } + } + else + { + // Until 20737698 is done, throw an error, so debugging can trace + // the exception here, instead of later on in unrelated code + THROW_HR(E_INVALIDARG); + } + } + else + { + resultPtr = std::make_unique(); + resultPtr->_CreateDefaults(); + + // The settings file does not exist. Let's commit one. + resultPtr->SaveAll(); + } + + return resultPtr; +} + +// Method Description: +// - Serialize this settings structure, and save it to a file. The location of +// the file changes depending whether we're running as a packaged +// application or not. +// Arguments: +// - +// Return Value: +// - +void CascadiaSettings::SaveAll() const +{ + const JsonObject json = ToJson(); + auto serializedSettings = json.Stringify(); + + if (_IsPackaged()) + { + _SaveAsPackagedApp(serializedSettings); + } + else + { + _SaveAsUnpackagedApp(serializedSettings); + } +} + +// Method Description: +// - Serialize this object to a JsonObject. +// Arguments: +// - +// Return Value: +// - a JsonObject which is an equivalent serialization of this object. +JsonObject CascadiaSettings::ToJson() const +{ + // _globals.ToJson will initialize the settings object will all the global + // settings in the root of the object. + winrt::Windows::Data::Json::JsonObject jsonObject = _globals.ToJson(); + + JsonArray schemesArray{}; + const auto& colorSchemes = _globals.GetColorSchemes(); + for (auto& scheme : colorSchemes) + { + schemesArray.Append(scheme.ToJson()); + } + + JsonArray profilesArray{}; + for (auto& profile : _profiles) + { + profilesArray.Append(profile.ToJson()); + } + + jsonObject.Insert(PROFILES_KEY, profilesArray); + jsonObject.Insert(SCHEMES_KEY, schemesArray); + + return jsonObject; +} + +// Method Description: +// - Create a new instance of this class from a serialized JsonObject. +// Arguments: +// - json: an object which should be a serialization of a CascadiaSettings object. +// Return Value: +// - a new CascadiaSettings instance created from the values in `json` +std::unique_ptr CascadiaSettings::FromJson(JsonObject json) +{ + std::unique_ptr resultPtr = std::make_unique(); + + resultPtr->_globals = GlobalAppSettings::FromJson(json); + + // TODO:MSFT:20737698 - Display an error if we failed to parse settings + // What should we do here if these keys aren't found?For default profile, + // we could always pick the first profile and just set that as the default. + // Finding no schemes is probably fine, unless of course one profile + // references a scheme. We could fail with come error saying the + // profiles file is corrupted. + // Not having any profiles is also bad - should we say the file is corrupted? + // Or should we just recreate the default profiles? + + auto& resultSchemes = resultPtr->_globals.GetColorSchemes(); + if (json.HasKey(SCHEMES_KEY)) + { + auto schemes = json.GetNamedArray(SCHEMES_KEY); + for (auto schemeJson : schemes) + { + if (schemeJson.ValueType() == JsonValueType::Object) + { + auto schemeObj = schemeJson.GetObjectW(); + auto scheme = ColorScheme::FromJson(schemeObj); + resultSchemes.emplace_back(std::move(scheme)); + } + } + } + + if (json.HasKey(PROFILES_KEY)) + { + auto profiles = json.GetNamedArray(PROFILES_KEY); + for (auto profileJson : profiles) + { + if (profileJson.ValueType() == JsonValueType::Object) + { + auto profileObj = profileJson.GetObjectW(); + auto profile = Profile::FromJson(profileObj); + resultPtr->_profiles.emplace_back(std::move(profile)); + } + } + } + + // TODO:MSFT:20700157 + // Load the keybindings from the file as well + resultPtr->_CreateDefaultKeybindings(); + + return resultPtr; +} + +// Function Description: +// - Returns true if we're running in a packaged context. If we are, then we +// have to use the Windows.Storage API's to save/load our files. If we're +// not, then we won't be able to use those API's. +// Arguments: +// - +// Return Value: +// - true iff we're running in a packaged context. +bool CascadiaSettings::_IsPackaged() +{ + UINT32 length = 0; + LONG rc = GetCurrentPackageFullName(&length, NULL); + return rc != APPMODEL_ERROR_NO_PACKAGE; +} + +// Method Description: +// - Writes the given content to our settings file as UTF-8 encoded using the Windows.Storage +// APIS's. This will only work within the context of an application with +// package identity, so make sure to call _IsPackaged before calling this method. +// Will overwrite any existing content in the file. +// Arguments: +// - content: the given string of content to write to the file. +// Return Value: +// - +void CascadiaSettings::_SaveAsPackagedApp(const winrt::hstring content) +{ + auto curr = ApplicationData::Current(); + auto folder = curr.RoamingFolder(); + + auto file_async = folder.CreateFileAsync(FILENAME, + CreationCollisionOption::ReplaceExisting); + + auto file = file_async.get(); + + DataWriter dw = DataWriter(); + + // DataWriter will convert UTF-16 string to UTF-8 (expected settings file encoding) + dw.UnicodeEncoding(UnicodeEncoding::Utf8); + + dw.WriteString(content); + + FileIO::WriteBufferAsync(file, dw.DetachBuffer()).get(); +} + +// Method Description: +// - Writes the given content in UTF-8 to our settings file using the Win32 APIS's. +// Will overwrite any existing content in the file. +// Arguments: +// - content: the given string of content to write to the file. +// Return Value: +// - +// This can throw an exception if we fail to open the file for writing, or we +// fail to write the file +void CascadiaSettings::_SaveAsUnpackagedApp(const winrt::hstring content) +{ + // convert UTF-16 to UTF-8 + auto contentString = winrt::to_string(content); + + // Get path to output file + // In this scenario, the settings file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json + std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile(); + + auto hOut = CreateFileW(pathToSettingsFile.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hOut == INVALID_HANDLE_VALUE) + { + THROW_LAST_ERROR(); + } + THROW_LAST_ERROR_IF(!WriteFile(hOut, contentString.c_str(), gsl::narrow(contentString.length()), 0, 0)); + CloseHandle(hOut); +} + +// Method Description: +// - Computes the path to the settings file if the app is run unpackaged. +// Will create any intermediate directories if they don't exist. +// The file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json +// Arguments: +// - +// Return Value: +// - A string containing the path to the unpackaged settings file +// This can throw an exception if it fails to get the roaming app data folder. +std::wstring CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() +{ + wil::unique_cotaskmem_string roamingAppDataFolder; + if (FAILED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, 0, &roamingAppDataFolder))) + { + THROW_LAST_ERROR(); + } + + std::wstring parentDirectoryForSettingsFile(roamingAppDataFolder.get()); + parentDirectoryForSettingsFile.append(SETTINGS_FOLDER_NAME); + + // Create the directory if it doesn't exist + wil::CreateDirectoryDeep(parentDirectoryForSettingsFile.c_str()); + + std::wstring pathToSettingsFile(parentDirectoryForSettingsFile); + pathToSettingsFile.append(FILENAME); + + return pathToSettingsFile; +} + +// Method Description: +// - Reads the content of our settings file using the Windows.Storage +// APIS's. This will only work within the context of an application with +// package identity, so make sure to call _IsPackaged before calling this method. +// Arguments: +// - +// Return Value: +// - an optional with the content of the file if we were able to open it, +// otherwise the optional will be empty +std::optional CascadiaSettings::_LoadAsPackagedApp() +{ + auto curr = ApplicationData::Current(); + auto folder = curr.RoamingFolder(); + auto file_async = folder.TryGetItemAsync(FILENAME); + auto file = file_async.get(); + + if (file == nullptr) + { + return std::nullopt; + } + const auto storageFile = file.as(); + + // settings file is UTF-8 without BOM + return { FileIO::ReadTextAsync(storageFile, UnicodeEncoding::Utf8).get() }; +} + + +// Method Description: +// - Reads the content in UTF-8 enconding of our settings file using the Win32 APIs +// Arguments: +// - +// Return Value: +// - an optional with the content of the file if we were able to open it, +// otherwise the optional will be empty. +// If the file exists, but we fail to read it, this can throw an exception +// from reading the file +std::optional CascadiaSettings::_LoadAsUnpackagedApp() +{ + std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile(); + const auto hFile = CreateFileW(pathToSettingsFile.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + // If the file doesn't exist, that's fine. Just log the error and return + // nullopt - we'll create the defaults. + LOG_LAST_ERROR(); + return std::nullopt; + } + + // fileSize is in bytes + const auto fileSize = GetFileSize(hFile, nullptr); + THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); + + auto utf8buffer = std::make_unique(fileSize); + + DWORD bytesRead = 0; + THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr)); + CloseHandle(hFile); + + // convert buffer to UTF-8 string + std::string utf8string(utf8buffer.get(), fileSize); + + // UTF-8 to UTF-16 + const winrt::hstring fileData = winrt::to_hstring(utf8string); + + return { fileData }; +} + +// function Description: +// - Returns the full path to the settings file, either within the application +// package, or in it's unpackaged location. +// Arguments: +// - +// Return Value: +// - the full path to the settings file +winrt::hstring CascadiaSettings::GetSettingsPath() +{ + return _IsPackaged() ? CascadiaSettings::_GetPackagedSettingsPath() : + winrt::hstring{ CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() }; +} + +// Function Description: +// - Get the full path to settings file in it's packaged location. +// Arguments: +// - +// Return Value: +// - the full path to the packaged settings file. +winrt::hstring CascadiaSettings::_GetPackagedSettingsPath() +{ + const auto curr = ApplicationData::Current(); + const auto folder = curr.RoamingFolder(); + const auto file_async = folder.TryGetItemAsync(FILENAME); + const auto file = file_async.get(); + return file.Path(); +} diff --git a/src/cascadia/TerminalApp/ColorScheme.cpp b/src/cascadia/TerminalApp/ColorScheme.cpp new file mode 100644 index 000000000..943b3da27 --- /dev/null +++ b/src/cascadia/TerminalApp/ColorScheme.cpp @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ColorScheme.h" +#include "../../types/inc/Utils.hpp" + +using namespace TerminalApp; +using namespace ::Microsoft::Console; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::Microsoft::Terminal::TerminalControl; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::Data::Json; + +static const std::wstring NAME_KEY{ L"name" }; +static const std::wstring TABLE_KEY{ L"colors" }; +static const std::wstring FOREGROUND_KEY{ L"foreground" }; +static const std::wstring BACKGROUND_KEY{ L"background" }; + +ColorScheme::ColorScheme() : + _schemeName{ L"" }, + _table{ }, + _defaultForeground{ RGB(242, 242, 242) }, + _defaultBackground{ RGB(12, 12, 12) } +{ + +} + +ColorScheme::ColorScheme(std::wstring name, COLORREF defaultFg, COLORREF defaultBg) : + _schemeName{ name }, + _table{ }, + _defaultForeground{ defaultFg }, + _defaultBackground{ defaultBg } +{ + +} + +ColorScheme::~ColorScheme() +{ + +} + +// Method Description: +// - Apply our values to the given TerminalSettings object. Sets the foreground, +// background, and color table of the settings object. +// Arguments: +// - terminalSettings: the object to apply our settings to. +// Return Value: +// - +void ColorScheme::ApplyScheme(TerminalSettings terminalSettings) const +{ + terminalSettings.DefaultForeground(_defaultForeground); + terminalSettings.DefaultBackground(_defaultBackground); + + for (int i = 0; i < _table.size(); i++) + { + terminalSettings.SetColorTableEntry(i, _table[i]); + } +} + +// Method Description: +// - Serialize this object to a JsonObject. +// Arguments: +// - +// Return Value: +// - a JsonObject which is an equivalent serialization of this object. +JsonObject ColorScheme::ToJson() const +{ + winrt::Windows::Data::Json::JsonObject jsonObject; + + auto fg = JsonValue::CreateStringValue(Utils::ColorToHexString(_defaultForeground)); + auto bg = JsonValue::CreateStringValue(Utils::ColorToHexString(_defaultBackground)); + auto name = JsonValue::CreateStringValue(_schemeName); + JsonArray tableArray{}; + for (auto& color : _table) + { + auto s = Utils::ColorToHexString(color); + tableArray.Append(JsonValue::CreateStringValue(s)); + } + + jsonObject.Insert(NAME_KEY, name); + jsonObject.Insert(FOREGROUND_KEY, fg); + jsonObject.Insert(BACKGROUND_KEY, bg); + jsonObject.Insert(TABLE_KEY, tableArray); + + return jsonObject; +} + +// Method Description: +// - Create a new instance of this class from a serialized JsonObject. +// Arguments: +// - json: an object which should be a serialization of a ColorScheme object. +// Return Value: +// - a new ColorScheme instance created from the values in `json` +ColorScheme ColorScheme::FromJson(winrt::Windows::Data::Json::JsonObject json) +{ + ColorScheme result{}; + + if (json.HasKey(NAME_KEY)) + { + result._schemeName = json.GetNamedString(NAME_KEY); + } + if (json.HasKey(FOREGROUND_KEY)) + { + const auto fgString = json.GetNamedString(FOREGROUND_KEY); + const auto color = Utils::ColorFromHexString(fgString.c_str()); + result._defaultForeground = color; + } + if (json.HasKey(BACKGROUND_KEY)) + { + const auto bgString = json.GetNamedString(BACKGROUND_KEY); + const auto color = Utils::ColorFromHexString(bgString.c_str()); + result._defaultBackground = color; + } + if (json.HasKey(TABLE_KEY)) + { + const auto table = json.GetNamedArray(TABLE_KEY); + int i = 0; + + for (auto v : table) + { + if (v.ValueType() == JsonValueType::String) + { + auto str = v.GetString(); + auto color = Utils::ColorFromHexString(str.c_str()); + result._table[i] = color; + } + i++; + } + } + + return result; +} + +std::wstring_view ColorScheme::GetName() const noexcept +{ + return { _schemeName }; +} + +std::array& ColorScheme::GetTable() noexcept +{ + return _table; +} + +COLORREF ColorScheme::GetForeground() const noexcept +{ + return _defaultForeground; +} + +COLORREF ColorScheme::GetBackground() const noexcept +{ + return _defaultBackground; +} diff --git a/src/cascadia/TerminalApp/ColorScheme.h b/src/cascadia/TerminalApp/ColorScheme.h new file mode 100644 index 000000000..afc1818c0 --- /dev/null +++ b/src/cascadia/TerminalApp/ColorScheme.h @@ -0,0 +1,52 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ColorScheme.hpp + +Abstract: +- A color scheme is a single set of colors to use as the terminal colors. These + schemes are named, and can be used to quickly change all the colors of the + terminal to another scheme. + +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once +#include +#include +#include +#include "../../inc/conattrs.hpp" +#include + +namespace TerminalApp +{ + class ColorScheme; +}; + +class TerminalApp::ColorScheme +{ + +public: + ColorScheme(); + ColorScheme(std::wstring name, COLORREF defaultFg, COLORREF defaultBg); + ~ColorScheme(); + + void ApplyScheme(winrt::Microsoft::Terminal::Settings::TerminalSettings terminalSettings) const; + + winrt::Windows::Data::Json::JsonObject ToJson() const; + static ColorScheme FromJson(winrt::Windows::Data::Json::JsonObject json); + + std::wstring_view GetName() const noexcept; + std::array& GetTable() noexcept; + COLORREF GetForeground() const noexcept; + COLORREF GetBackground() const noexcept; + +private: + std::wstring _schemeName; + std::array _table; + COLORREF _defaultForeground; + COLORREF _defaultBackground; +}; diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp new file mode 100644 index 000000000..365049f6b --- /dev/null +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "GlobalAppSettings.h" +#include "../../types/inc/Utils.hpp" +#include "../../inc/DefaultSettings.h" + +using namespace TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::Data::Json; +using namespace winrt::Windows::UI::Xaml; +using namespace ::Microsoft::Console; + +static const std::wstring DEFAULTPROFILE_KEY{ L"defaultProfile" }; +static const std::wstring ALWAYS_SHOW_TABS_KEY{ L"alwaysShowTabs" }; +static const std::wstring INITIALROWS_KEY{ L"initialRows" }; +static const std::wstring INITIALCOLS_KEY{ L"initialCols" }; +static const std::wstring SHOW_TITLE_IN_TITLEBAR_KEY{ L"showTerminalTitleInTitlebar" }; +static const std::wstring REQUESTED_THEME_KEY{ L"requestedTheme" }; + +static const std::wstring SHOW_TABS_IN_TITLEBAR_KEY{ L"experimental_showTabsInTitlebar" }; + +static const std::wstring LIGHT_THEME_VALUE{ L"light" }; +static const std::wstring DARK_THEME_VALUE{ L"dark" }; +static const std::wstring SYSTEM_THEME_VALUE{ L"system" }; + +GlobalAppSettings::GlobalAppSettings() : + _keybindings{}, + _colorSchemes{}, + _defaultProfile{}, + _alwaysShowTabs{ false }, + _initialRows{ DEFAULT_ROWS }, + _initialCols{ DEFAULT_COLS }, + _showTitleInTitlebar{ true }, + _showTabsInTitlebar{ false }, + _requestedTheme{ ElementTheme::Default } +{ + +} + +GlobalAppSettings::~GlobalAppSettings() +{ + +} + +const std::vector& GlobalAppSettings::GetColorSchemes() const noexcept +{ + return _colorSchemes; +} + + +std::vector& GlobalAppSettings::GetColorSchemes() noexcept +{ + return _colorSchemes; +} + +void GlobalAppSettings::SetDefaultProfile(const GUID defaultProfile) noexcept +{ + _defaultProfile = defaultProfile; +} + +GUID GlobalAppSettings::GetDefaultProfile() const noexcept +{ + return _defaultProfile; +} + +AppKeyBindings GlobalAppSettings::GetKeybindings() const noexcept +{ + return _keybindings; +} + +bool GlobalAppSettings::GetAlwaysShowTabs() const noexcept +{ + return _alwaysShowTabs; +} + +void GlobalAppSettings::SetAlwaysShowTabs(const bool showTabs) noexcept +{ + _alwaysShowTabs = showTabs; +} + +bool GlobalAppSettings::GetShowTitleInTitlebar() const noexcept +{ + return _showTitleInTitlebar; +} + +void GlobalAppSettings::SetShowTitleInTitlebar(const bool showTitleInTitlebar) noexcept +{ + _showTitleInTitlebar = showTitleInTitlebar; +} + +ElementTheme GlobalAppSettings::GetRequestedTheme() const noexcept +{ + return _requestedTheme; +} + + +#pragma region ExperimentalSettings +bool GlobalAppSettings::GetShowTabsInTitlebar() const noexcept +{ + return _showTabsInTitlebar; +} + +void GlobalAppSettings::SetShowTabsInTitlebar(const bool showTabsInTitlebar) noexcept +{ + _showTabsInTitlebar = showTabsInTitlebar; +} +#pragma endregion + +// Method Description: +// - Applies appropriate settings from the globals into the given TerminalSettings. +// Arguments: +// - settings: a TerminalSettings object to add global property values to. +// Return Value: +// - +void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexcept +{ + settings.KeyBindings(GetKeybindings()); + settings.InitialRows(_initialRows); + settings.InitialCols(_initialCols); +} + +// Method Description: +// - Serialize this object to a JsonObject. +// Arguments: +// - +// Return Value: +// - a JsonObject which is an equivalent serialization of this object. +JsonObject GlobalAppSettings::ToJson() const +{ + winrt::Windows::Data::Json::JsonObject jsonObject; + + const auto guidStr = Utils::GuidToString(_defaultProfile); + const auto defaultProfile = JsonValue::CreateStringValue(guidStr); + const auto initialRows = JsonValue::CreateNumberValue(_initialRows); + const auto initialCols = JsonValue::CreateNumberValue(_initialCols); + + jsonObject.Insert(DEFAULTPROFILE_KEY, defaultProfile); + jsonObject.Insert(INITIALROWS_KEY, initialRows); + jsonObject.Insert(INITIALCOLS_KEY, initialCols); + jsonObject.Insert(ALWAYS_SHOW_TABS_KEY, + JsonValue::CreateBooleanValue(_alwaysShowTabs)); + jsonObject.Insert(SHOW_TITLE_IN_TITLEBAR_KEY, + JsonValue::CreateBooleanValue(_showTitleInTitlebar)); + + jsonObject.Insert(SHOW_TABS_IN_TITLEBAR_KEY, + JsonValue::CreateBooleanValue(_showTabsInTitlebar)); + if (_requestedTheme != ElementTheme::Default) + { + jsonObject.Insert(REQUESTED_THEME_KEY, + JsonValue::CreateStringValue(_SerializeTheme(_requestedTheme))); + } + + return jsonObject; +} + +// Method Description: +// - Create a new instance of this class from a serialized JsonObject. +// Arguments: +// - json: an object which should be a serialization of a GlobalAppSettings object. +// Return Value: +// - a new GlobalAppSettings instance created from the values in `json` +GlobalAppSettings GlobalAppSettings::FromJson(winrt::Windows::Data::Json::JsonObject json) +{ + GlobalAppSettings result{}; + + if (json.HasKey(DEFAULTPROFILE_KEY)) + { + auto guidString = json.GetNamedString(DEFAULTPROFILE_KEY); + auto guid = Utils::GuidFromString(guidString.c_str()); + result._defaultProfile = guid; + } + + if (json.HasKey(ALWAYS_SHOW_TABS_KEY)) + { + result._alwaysShowTabs = json.GetNamedBoolean(ALWAYS_SHOW_TABS_KEY); + } + if (json.HasKey(INITIALROWS_KEY)) + { + result._initialRows = static_cast(json.GetNamedNumber(INITIALROWS_KEY)); + } + if (json.HasKey(INITIALCOLS_KEY)) + { + result._initialCols = static_cast(json.GetNamedNumber(INITIALCOLS_KEY)); + } + + if (json.HasKey(SHOW_TITLE_IN_TITLEBAR_KEY)) + { + result._showTitleInTitlebar = json.GetNamedBoolean(SHOW_TITLE_IN_TITLEBAR_KEY); + } + + if (json.HasKey(SHOW_TABS_IN_TITLEBAR_KEY)) + { + result._showTabsInTitlebar = json.GetNamedBoolean(SHOW_TABS_IN_TITLEBAR_KEY); + } + + if (json.HasKey(REQUESTED_THEME_KEY)) + { + const auto themeStr = json.GetNamedString(REQUESTED_THEME_KEY); + result._requestedTheme = _ParseTheme(themeStr.c_str()); + } + + return result; +} + +// Method Description: +// - Helper function for converting a user-specified cursor style corresponding +// CursorStyle enum value +// Arguments: +// - themeString: The string value from the settings file to parse +// Return Value: +// - The corresponding enum value which maps to the string provided by the user +ElementTheme GlobalAppSettings::_ParseTheme(const std::wstring& themeString) noexcept +{ + if (themeString == LIGHT_THEME_VALUE) + { + return ElementTheme::Light; + } + else if (themeString == DARK_THEME_VALUE) + { + return ElementTheme::Dark; + } + // default behavior for invalid data or SYSTEM_THEME_VALUE + return ElementTheme::Default; +} + +// Method Description: +// - Helper function for converting a CursorStyle to it's corresponding string +// value. +// Arguments: +// - theme: The enum value to convert to a string. +// Return Value: +// - The string value for the given CursorStyle +std::wstring GlobalAppSettings::_SerializeTheme(const ElementTheme theme) noexcept +{ + switch (theme) + { + case ElementTheme::Light: + return LIGHT_THEME_VALUE; + case ElementTheme::Dark: + return DARK_THEME_VALUE; + default: + return SYSTEM_THEME_VALUE; + } +} diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h new file mode 100644 index 000000000..6e20593aa --- /dev/null +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -0,0 +1,74 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CascadiaSettings.hpp + +Abstract: +- This class encapsulates all of the settings that are global to the app, and + not a part of any particular profile. + +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once +#include "AppKeyBindings.h" +#include "ColorScheme.h" + +namespace TerminalApp +{ + class GlobalAppSettings; +}; + +class TerminalApp::GlobalAppSettings final +{ + +public: + GlobalAppSettings(); + ~GlobalAppSettings(); + + const std::vector& GetColorSchemes() const noexcept; + std::vector& GetColorSchemes() noexcept; + void SetDefaultProfile(const GUID defaultProfile) noexcept; + GUID GetDefaultProfile() const noexcept; + + winrt::TerminalApp::AppKeyBindings GetKeybindings() const noexcept; + + bool GetAlwaysShowTabs() const noexcept; + void SetAlwaysShowTabs(const bool showTabs) noexcept; + + bool GetShowTitleInTitlebar() const noexcept; + void SetShowTitleInTitlebar(const bool showTitleInTitlebar) noexcept; + + bool GetShowTabsInTitlebar() const noexcept; + void SetShowTabsInTitlebar(const bool showTabsInTitlebar) noexcept; + + winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme() const noexcept; + + winrt::Windows::Data::Json::JsonObject ToJson() const; + static GlobalAppSettings FromJson(winrt::Windows::Data::Json::JsonObject json); + + void ApplyToSettings(winrt::Microsoft::Terminal::Settings::TerminalSettings& settings) const noexcept; + +private: + GUID _defaultProfile; + winrt::TerminalApp::AppKeyBindings _keybindings; + + std::vector _colorSchemes; + + int32_t _initialRows; + int32_t _initialCols; + + bool _showStatusline; + bool _alwaysShowTabs; + bool _showTitleInTitlebar; + + bool _showTabsInTitlebar; + winrt::Windows::UI::Xaml::ElementTheme _requestedTheme; + + static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept; + static std::wstring _SerializeTheme(const winrt::Windows::UI::Xaml::ElementTheme theme) noexcept; + +}; diff --git a/src/cascadia/TerminalApp/Profile.cpp b/src/cascadia/TerminalApp/Profile.cpp new file mode 100644 index 000000000..6f816d75b --- /dev/null +++ b/src/cascadia/TerminalApp/Profile.cpp @@ -0,0 +1,592 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Profile.h" +#include "../../types/inc/Utils.hpp" +#include + +using namespace TerminalApp; +using namespace winrt::Microsoft::Terminal::Settings; +using namespace winrt::TerminalApp; +using namespace winrt::Windows::Data::Json; +using namespace ::Microsoft::Console; + + +static const std::wstring NAME_KEY{ L"name" }; +static const std::wstring GUID_KEY{ L"guid" }; +static const std::wstring COLORSCHEME_KEY{ L"colorscheme" }; + +static const std::wstring FOREGROUND_KEY{ L"foreground" }; +static const std::wstring BACKGROUND_KEY{ L"background" }; +static const std::wstring COLORTABLE_KEY{ L"colorTable" }; +static const std::wstring HISTORYSIZE_KEY{ L"historySize" }; +static const std::wstring SNAPONINPUT_KEY{ L"snapOnInput" }; +static const std::wstring CURSORCOLOR_KEY{ L"cursorColor" }; +static const std::wstring CURSORSHAPE_KEY{ L"cursorShape" }; +static const std::wstring CURSORHEIGHT_KEY{ L"cursorHeight" }; + +static const std::wstring COMMANDLINE_KEY{ L"commandline" }; +static const std::wstring FONTFACE_KEY{ L"fontFace" }; +static const std::wstring FONTSIZE_KEY{ L"fontSize" }; +static const std::wstring ACRYLICTRANSPARENCY_KEY{ L"acrylicOpacity" }; +static const std::wstring USEACRYLIC_KEY{ L"useAcrylic" }; +static const std::wstring SCROLLBARSTATE_KEY{ L"scrollbarState" }; +static const std::wstring CLOSEONEXIT_KEY{ L"closeOnExit" }; +static const std::wstring PADDING_KEY{ L"padding" }; +static const std::wstring STARTINGDIRECTORY_KEY{ L"startingDirectory" }; +static const std::wstring ICON_KEY{ L"icon" }; + +// Possible values for Scrollbar state +static const std::wstring ALWAYS_VISIBLE{ L"visible" }; +static const std::wstring ALWAYS_HIDE{ L"hidden" }; + +// Possible values for Cursor Shape +static const std::wstring CURSORSHAPE_VINTAGE{ L"vintage" }; +static const std::wstring CURSORSHAPE_BAR{ L"bar" }; +static const std::wstring CURSORSHAPE_UNDERSCORE{ L"underscore" }; +static const std::wstring CURSORSHAPE_FILLEDBOX{ L"filledBox" }; +static const std::wstring CURSORSHAPE_EMPTYBOX{ L"emptyBox" }; + +Profile::Profile() : + _guid{}, + _name{ L"Default" }, + _schemeName{}, + + _defaultForeground{ }, + _defaultBackground{ }, + _colorTable{}, + _historySize{ DEFAULT_HISTORY_SIZE }, + _snapOnInput{ true }, + _cursorColor{ DEFAULT_CURSOR_COLOR }, + _cursorShape{ CursorStyle::Bar }, + _cursorHeight{ DEFAULT_CURSOR_HEIGHT }, + + _commandline{ L"cmd.exe" }, + _startingDirectory{ }, + _fontFace{ DEFAULT_FONT_FACE }, + _fontSize{ DEFAULT_FONT_SIZE }, + _acrylicTransparency{ 0.5 }, + _useAcrylic{ false }, + _scrollbarState{ }, + _closeOnExit{ false }, + _padding{ DEFAULT_PADDING }, + _icon{ } +{ + UuidCreate(&_guid); +} + +Profile::~Profile() +{ + +} + +GUID Profile::GetGuid() const noexcept +{ + return _guid; +} + +// Function Description: +// - Searches a list of color schemes to find one matching the given name. Will +//return the first match in the list, if the list has multiple schemes with the same name. +// Arguments: +// - schemes: a list of schemes to search +// - schemeName: the name of the sceme to look for +// Return Value: +// - a non-ownership pointer to the matching scheme if we found one, else nullptr +const ColorScheme* _FindScheme(const std::vector& schemes, + const std::wstring& schemeName) +{ + for (auto& scheme : schemes) + { + if (scheme.GetName() == schemeName) + { + return &scheme; + } + } + return nullptr; +} + +// Method Description: +// - Create a TerminalSettings from this object. Apply our settings, as well as +// any colors from our colorscheme, if we have one. +// Arguments: +// - schemes: a list of schemes to look for our color scheme in, if we have one. +// Return Value: +// - a new TerminalSettings object with our settings in it. +TerminalSettings Profile::CreateTerminalSettings(const std::vector& schemes) const +{ + TerminalSettings terminalSettings{}; + + // Fill in the Terminal Setting's CoreSettings from the profile + for (int i = 0; i < _colorTable.size(); i++) + { + terminalSettings.SetColorTableEntry(i, _colorTable[i]); + } + terminalSettings.HistorySize(_historySize); + terminalSettings.SnapOnInput(_snapOnInput); + terminalSettings.CursorColor(_cursorColor); + terminalSettings.CursorHeight(_cursorHeight); + terminalSettings.CursorShape(_cursorShape); + + // Fill in the remaining properties from the profile + terminalSettings.UseAcrylic(_useAcrylic); + terminalSettings.CloseOnExit(_closeOnExit); + terminalSettings.TintOpacity(_acrylicTransparency); + + terminalSettings.FontFace(_fontFace); + terminalSettings.FontSize(_fontSize); + terminalSettings.Padding(_padding); + + terminalSettings.Commandline(winrt::to_hstring(_commandline.c_str())); + + if (_startingDirectory) + { + const auto evaluatedDirectory = Profile::EvaluateStartingDirectory(_startingDirectory.value()); + terminalSettings.StartingDirectory(winrt::to_hstring(evaluatedDirectory.c_str())); + } + + if (_schemeName) + { + const ColorScheme* const matchingScheme = _FindScheme(schemes, _schemeName.value()); + if (matchingScheme) + { + matchingScheme->ApplyScheme(terminalSettings); + } + } + if (_defaultForeground) + { + terminalSettings.DefaultForeground(_defaultForeground.value()); + } + if (_defaultBackground) + { + terminalSettings.DefaultBackground(_defaultBackground.value()); + } + + if (_scrollbarState) + { + ScrollbarState result = ParseScrollbarState(_scrollbarState.value()); + terminalSettings.ScrollState(result); + } + + return terminalSettings; +} + +// Method Description: +// - Serialize this object to a JsonObject. +// Arguments: +// - +// Return Value: +// - a JsonObject which is an equivalent serialization of this object. +JsonObject Profile::ToJson() const +{ + winrt::Windows::Data::Json::JsonObject jsonObject; + + // Profile-specific settings + const auto guidStr = Utils::GuidToString(_guid); + const auto guid = JsonValue::CreateStringValue(guidStr); + const auto name = JsonValue::CreateStringValue(_name); + + // Core Settings + const auto historySize = JsonValue::CreateNumberValue(_historySize); + const auto snapOnInput = JsonValue::CreateBooleanValue(_snapOnInput); + const auto cursorColor = JsonValue::CreateStringValue(Utils::ColorToHexString(_cursorColor)); + + // Control Settings + const auto cmdline = JsonValue::CreateStringValue(_commandline); + const auto fontFace = JsonValue::CreateStringValue(_fontFace); + const auto fontSize = JsonValue::CreateNumberValue(_fontSize); + const auto acrylicTransparency = JsonValue::CreateNumberValue(_acrylicTransparency); + const auto useAcrylic = JsonValue::CreateBooleanValue(_useAcrylic); + const auto closeOnExit = JsonValue::CreateBooleanValue(_closeOnExit); + const auto padding = JsonValue::CreateStringValue(_padding); + + if (_startingDirectory) + { + const auto startingDirectory = JsonValue::CreateStringValue(_startingDirectory.value()); + jsonObject.Insert(STARTINGDIRECTORY_KEY, startingDirectory); + } + + jsonObject.Insert(GUID_KEY, guid); + jsonObject.Insert(NAME_KEY, name); + + // Core Settings + if (_defaultForeground) + { + const auto defaultForeground = JsonValue::CreateStringValue(Utils::ColorToHexString(_defaultForeground.value())); + jsonObject.Insert(FOREGROUND_KEY, defaultForeground); + } + if (_defaultBackground) + { + const auto defaultBackground = JsonValue::CreateStringValue(Utils::ColorToHexString(_defaultBackground.value())); + jsonObject.Insert(BACKGROUND_KEY, defaultBackground); + } + if (_schemeName) + { + const auto scheme = JsonValue::CreateStringValue(_schemeName.value()); + jsonObject.Insert(COLORSCHEME_KEY, scheme); + } + else + { + JsonArray tableArray{}; + for (auto& color : _colorTable) + { + auto s = Utils::ColorToHexString(color); + tableArray.Append(JsonValue::CreateStringValue(s)); + } + + jsonObject.Insert(COLORTABLE_KEY, tableArray); + + } + jsonObject.Insert(HISTORYSIZE_KEY, historySize); + jsonObject.Insert(SNAPONINPUT_KEY, snapOnInput); + jsonObject.Insert(CURSORCOLOR_KEY, cursorColor); + + // Only add the cursor height property if we're a legacy-style cursor. + if (_cursorShape == CursorStyle::Vintage) + { + jsonObject.Insert(CURSORHEIGHT_KEY, JsonValue::CreateNumberValue(_cursorHeight)); + } + jsonObject.Insert(CURSORSHAPE_KEY, JsonValue::CreateStringValue(_SerializeCursorStyle(_cursorShape))); + + // Control Settings + jsonObject.Insert(COMMANDLINE_KEY, cmdline); + jsonObject.Insert(FONTFACE_KEY, fontFace); + jsonObject.Insert(FONTSIZE_KEY, fontSize); + jsonObject.Insert(ACRYLICTRANSPARENCY_KEY, acrylicTransparency); + jsonObject.Insert(USEACRYLIC_KEY, useAcrylic); + jsonObject.Insert(CLOSEONEXIT_KEY, closeOnExit); + jsonObject.Insert(PADDING_KEY, padding); + + if (_scrollbarState) + { + const auto scrollbarState = JsonValue::CreateStringValue(_scrollbarState.value()); + jsonObject.Insert(SCROLLBARSTATE_KEY, scrollbarState); + } + + if (_icon) + { + const auto icon = JsonValue::CreateStringValue(_icon.value()); + jsonObject.Insert(ICON_KEY, icon); + } + + return jsonObject; +} + +// Method Description: +// - Create a new instance of this class from a serialized JsonObject. +// Arguments: +// - json: an object which should be a serialization of a Profile object. +// Return Value: +// - a new Profile instance created from the values in `json` +Profile Profile::FromJson(winrt::Windows::Data::Json::JsonObject json) +{ + Profile result{}; + + // Profile-specific Settings + if (json.HasKey(NAME_KEY)) + { + result._name = json.GetNamedString(NAME_KEY); + } + if (json.HasKey(GUID_KEY)) + { + const auto guidString = json.GetNamedString(GUID_KEY); + // TODO: MSFT:20737698 - if this fails, display an approriate error + const auto guid = Utils::GuidFromString(guidString.c_str()); + result._guid = guid; + } + + // Core Settings + if (json.HasKey(FOREGROUND_KEY)) + { + const auto fgString = json.GetNamedString(FOREGROUND_KEY); + // TODO: MSFT:20737698 - if this fails, display an approriate error + const auto color = Utils::ColorFromHexString(fgString.c_str()); + result._defaultForeground = color; + } + if (json.HasKey(BACKGROUND_KEY)) + { + const auto bgString = json.GetNamedString(BACKGROUND_KEY); + // TODO: MSFT:20737698 - if this fails, display an approriate error + const auto color = Utils::ColorFromHexString(bgString.c_str()); + result._defaultBackground = color; + } + if (json.HasKey(COLORSCHEME_KEY)) + { + result._schemeName = json.GetNamedString(COLORSCHEME_KEY); + } + else + { + if (json.HasKey(COLORTABLE_KEY)) + { + const auto table = json.GetNamedArray(COLORTABLE_KEY); + int i = 0; + for (auto v : table) + { + if (v.ValueType() == JsonValueType::String) + { + const auto str = v.GetString(); + // TODO: MSFT:20737698 - if this fails, display an approriate error + const auto color = Utils::ColorFromHexString(str.c_str()); + result._colorTable[i] = color; + } + i++; + } + } + } + if (json.HasKey(HISTORYSIZE_KEY)) + { + // TODO:MSFT:20642297 - Use a sentinel value (-1) for "Infinite scrollback" + result._historySize = static_cast(json.GetNamedNumber(HISTORYSIZE_KEY)); + } + if (json.HasKey(SNAPONINPUT_KEY)) + { + result._snapOnInput = json.GetNamedBoolean(SNAPONINPUT_KEY); + } + if (json.HasKey(CURSORCOLOR_KEY)) + { + const auto cursorString = json.GetNamedString(CURSORCOLOR_KEY); + // TODO: MSFT:20737698 - if this fails, display an approriate error + const auto color = Utils::ColorFromHexString(cursorString.c_str()); + result._cursorColor = color; + } + if (json.HasKey(CURSORHEIGHT_KEY)) + { + result._cursorHeight = json.GetNamedNumber(CURSORHEIGHT_KEY); + } + if (json.HasKey(CURSORSHAPE_KEY)) + { + const auto shapeString = json.GetNamedString(CURSORSHAPE_KEY); + result._cursorShape = _ParseCursorShape(shapeString.c_str()); + } + + // Control Settings + if (json.HasKey(COMMANDLINE_KEY)) + { + result._commandline = json.GetNamedString(COMMANDLINE_KEY); + } + if (json.HasKey(FONTFACE_KEY)) + { + result._fontFace = json.GetNamedString(FONTFACE_KEY); + } + if (json.HasKey(FONTSIZE_KEY)) + { + result._fontSize = static_cast(json.GetNamedNumber(FONTSIZE_KEY)); + } + if (json.HasKey(ACRYLICTRANSPARENCY_KEY)) + { + result._acrylicTransparency = json.GetNamedNumber(ACRYLICTRANSPARENCY_KEY); + } + if (json.HasKey(USEACRYLIC_KEY)) + { + result._useAcrylic = json.GetNamedBoolean(USEACRYLIC_KEY); + } + if (json.HasKey(CLOSEONEXIT_KEY)) + { + result._closeOnExit = json.GetNamedBoolean(CLOSEONEXIT_KEY); + } + if (json.HasKey(PADDING_KEY)) + { + result._padding = json.GetNamedString(PADDING_KEY); + } + if (json.HasKey(SCROLLBARSTATE_KEY)) + { + result._scrollbarState = json.GetNamedString(SCROLLBARSTATE_KEY); + } + if (json.HasKey(STARTINGDIRECTORY_KEY)) + { + result._startingDirectory = json.GetNamedString(STARTINGDIRECTORY_KEY); + } + if (json.HasKey(ICON_KEY)) + { + result._icon = json.GetNamedString(ICON_KEY); + } + + return result; +} + + + +void Profile::SetFontFace(std::wstring fontFace) noexcept +{ + _fontFace = fontFace; +} + +void Profile::SetColorScheme(std::optional schemeName) noexcept +{ + _schemeName = schemeName; +} + +void Profile::SetAcrylicOpacity(double opacity) noexcept +{ + _acrylicTransparency = opacity; +} + +void Profile::SetCommandline(std::wstring cmdline) noexcept +{ + _commandline = cmdline; +} + +void Profile::SetName(std::wstring name) noexcept +{ + _name = name; +} + +void Profile::SetUseAcrylic(bool useAcrylic) noexcept +{ + _useAcrylic = useAcrylic; +} + +void Profile::SetDefaultForeground(COLORREF defaultForeground) noexcept +{ + _defaultForeground = defaultForeground; +} + +void Profile::SetDefaultBackground(COLORREF defaultBackground) noexcept +{ + _defaultBackground = defaultBackground; +} + +bool Profile::HasIcon() const noexcept +{ + return _icon.has_value(); +} + +// Method Description: +// - Returns this profile's icon path, if one is set. Otherwise returns the empty string. +// Return Value: +// - this profile's icon path, if one is set. Otherwise returns the empty string. +std::wstring_view Profile::GetIconPath() const noexcept +{ + return HasIcon() ? + std::wstring_view{ _icon.value().c_str(), _icon.value().size() } : + std::wstring_view{ L"", 0 }; +} + +// Method Description: +// - Returns the name of this profile. +// Arguments: +// - +// Return Value: +// - the name of this profile +std::wstring_view Profile::GetName() const noexcept +{ + return _name; +} + +bool Profile::GetCloseOnExit() const noexcept +{ + return _closeOnExit; +} + +// Method Description: +// - Helper function for expanding any environment variables in a user-supplied starting directory and validating the resulting path +// Arguments: +// - The value from the profiles.json file +// Return Value: +// - The directory string with any environment variables expanded. If the resulting path is invalid, +// - the function returns an evaluated version of %userprofile% to avoid blocking the session from starting. +std::wstring Profile::EvaluateStartingDirectory(const std::wstring& directory) +{ + // First expand path + DWORD numCharsInput = ExpandEnvironmentStrings(directory.c_str(), nullptr, 0); + std::unique_ptr evaluatedPath = std::make_unique(numCharsInput); + THROW_LAST_ERROR_IF(0 == ExpandEnvironmentStrings(directory.c_str(), evaluatedPath.get(), numCharsInput)); + + // Validate that the resulting path is legitimate + const DWORD dwFileAttributes = GetFileAttributes(evaluatedPath.get()); + if ((dwFileAttributes != INVALID_FILE_ATTRIBUTES) && (WI_IsFlagSet(dwFileAttributes, FILE_ATTRIBUTE_DIRECTORY))) + { + return std::wstring(evaluatedPath.get(), numCharsInput); + } + else + { + // In the event where the user supplied a path that can't be resolved, use a reasonable default (in this case, %userprofile%) + const DWORD numCharsDefault = ExpandEnvironmentStrings(DEFAULT_STARTING_DIRECTORY.c_str(), nullptr, 0); + std::unique_ptr defaultPath = std::make_unique(numCharsDefault); + THROW_LAST_ERROR_IF(0 == ExpandEnvironmentStrings(DEFAULT_STARTING_DIRECTORY.c_str(), defaultPath.get(), numCharsDefault)); + + return std::wstring(defaultPath.get(), numCharsDefault); + } +} + +// Method Description: +// - Helper function for converting a user-specified scrollbar state to its corresponding enum +// Arguments: +// - The value from the profiles.json file +// Return Value: +// - The corresponding enum value which maps to the string provided by the user +ScrollbarState Profile::ParseScrollbarState(const std::wstring& scrollbarState) +{ + if (scrollbarState == ALWAYS_VISIBLE) + { + return ScrollbarState::Visible; + } + else if (scrollbarState == ALWAYS_HIDE) + { + return ScrollbarState::Hidden; + } + else + { + // default behavior for invalid data + return ScrollbarState::Visible; + } +} + +// Method Description: +// - Helper function for converting a user-specified cursor style corresponding +// CursorStyle enum value +// Arguments: +// - cursorShapeString: The string value from the settings file to parse +// Return Value: +// - The corresponding enum value which maps to the string provided by the user +CursorStyle Profile::_ParseCursorShape(const std::wstring& cursorShapeString) +{ + if (cursorShapeString == CURSORSHAPE_VINTAGE) + { + return CursorStyle::Vintage; + } + else if (cursorShapeString == CURSORSHAPE_BAR) + { + return CursorStyle::Bar; + } + else if (cursorShapeString == CURSORSHAPE_UNDERSCORE) + { + return CursorStyle::Underscore; + } + else if (cursorShapeString == CURSORSHAPE_FILLEDBOX) + { + return CursorStyle::FilledBox; + } + else if (cursorShapeString == CURSORSHAPE_EMPTYBOX) + { + return CursorStyle::EmptyBox; + } + // default behavior for invalid data + return CursorStyle::Bar; +} + +// Method Description: +// - Helper function for converting a CursorStyle to it's corresponding string +// value. +// Arguments: +// - cursorShape: The enum value to convert to a string. +// Return Value: +// - The string value for the given CursorStyle +std::wstring Profile::_SerializeCursorStyle(const CursorStyle cursorShape) +{ + switch (cursorShape) + { + case CursorStyle::Underscore: + return CURSORSHAPE_UNDERSCORE; + case CursorStyle::FilledBox: + return CURSORSHAPE_FILLEDBOX; + case CursorStyle::EmptyBox: + return CURSORSHAPE_EMPTYBOX; + case CursorStyle::Vintage: + return CURSORSHAPE_VINTAGE; + default: + case CursorStyle::Bar: + return CURSORSHAPE_BAR; + } +} diff --git a/src/cascadia/TerminalApp/Profile.h b/src/cascadia/TerminalApp/Profile.h new file mode 100644 index 000000000..ddee3aa8e --- /dev/null +++ b/src/cascadia/TerminalApp/Profile.h @@ -0,0 +1,88 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Profile.hpp + +Abstract: +- A profile acts as a single set of terminal settings. Many tabs or panes could + exist side-by-side with different profiles simultaneously. + +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once +#include "ColorScheme.h" + +namespace TerminalApp +{ + class Profile; +}; + +class TerminalApp::Profile final +{ + +public: + Profile(); + ~Profile(); + + winrt::Microsoft::Terminal::Settings::TerminalSettings CreateTerminalSettings(const std::vector<::TerminalApp::ColorScheme>& schemes) const; + + winrt::Windows::Data::Json::JsonObject ToJson() const; + static Profile FromJson(winrt::Windows::Data::Json::JsonObject json); + + GUID GetGuid() const noexcept; + std::wstring_view GetName() const noexcept; + + void SetFontFace(std::wstring fontFace) noexcept; + void SetColorScheme(std::optional schemeName) noexcept; + void SetAcrylicOpacity(double opacity) noexcept; + void SetCommandline(std::wstring cmdline) noexcept; + void SetName(std::wstring name) noexcept; + void SetUseAcrylic(bool useAcrylic) noexcept; + void SetDefaultForeground(COLORREF defaultForeground) noexcept; + void SetDefaultBackground(COLORREF defaultBackground) noexcept; + + bool HasIcon() const noexcept; + std::wstring_view GetIconPath() const noexcept; + + bool GetCloseOnExit() const noexcept; + +private: + + static std::wstring EvaluateStartingDirectory(const std::wstring& directory); + + static winrt::Microsoft::Terminal::Settings::ScrollbarState ParseScrollbarState(const std::wstring& scrollbarState); + static winrt::Microsoft::Terminal::Settings::CursorStyle _ParseCursorShape(const std::wstring& cursorShapeString); + static std::wstring _SerializeCursorStyle(const winrt::Microsoft::Terminal::Settings::CursorStyle cursorShape); + + GUID _guid; + std::wstring _name; + + // If this is set, then our colors should come from the associated color scheme + std::optional _schemeName; + + std::optional _defaultForeground; + std::optional _defaultBackground; + std::array _colorTable; + int32_t _historySize; + bool _snapOnInput; + uint32_t _cursorColor; + uint32_t _cursorHeight; + winrt::Microsoft::Terminal::Settings::CursorStyle _cursorShape; + + std::wstring _commandline; + std::wstring _fontFace; + std::optional _startingDirectory; + int32_t _fontSize; + double _acrylicTransparency; + bool _useAcrylic; + + std::optional _scrollbarState; + bool _closeOnExit; + std::wstring _padding; + + std::optional _icon; +}; diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp new file mode 100644 index 000000000..48f71ab60 --- /dev/null +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Tab.h" + +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; + +Tab::Tab(GUID profile, winrt::Microsoft::Terminal::TerminalControl::TermControl control) : + _control{ control }, + _focused{ false }, + _profile{ profile }, + _tabViewItem{ nullptr } +{ + _MakeTabViewItem(); +} + +Tab::~Tab() +{ + // When we're destructed, winrt will automatically decrement the refcount + // of our terminalcontrol. + // Assuming that refcount hits 0, it'll destruct it on it's own, including + // calling Close on the terminal and connection. +} + +void Tab::_MakeTabViewItem() +{ + _tabViewItem = ::winrt::Microsoft::UI::Xaml::Controls::TabViewItem{}; + const auto title = _control.Title(); + + _tabViewItem.Header(title); + + _control.TitleChanged([=](auto newTitle){ + _tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ + _tabViewItem.Header(newTitle); + }); + }); +} + +winrt::Microsoft::Terminal::TerminalControl::TermControl Tab::GetTerminalControl() +{ + return _control; +} + +winrt::Microsoft::UI::Xaml::Controls::TabViewItem Tab::GetTabViewItem() +{ + return _tabViewItem; +} + + +bool Tab::IsFocused() +{ + return _focused; +} + +void Tab::SetFocused(bool focused) +{ + _focused = focused; + + if (_focused) + { + _Focus(); + } +} + +GUID Tab::GetProfile() const noexcept +{ + return _profile; +} + +void Tab::_Focus() +{ + _focused = true; + _control.GetControl().Focus(FocusState::Programmatic); +} + +// Method Description: +// - Move the viewport of the terminal up or down a number of lines. Negative +// values of `delta` will move the view up, and positive values will move +// the viewport down. +// Arguments: +// - delta: a number of lines to move the viewport relative to the current viewport. +// Return Value: +// - +void Tab::Scroll(int delta) +{ + _control.GetControl().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [=](){ + const auto currentOffset = _control.GetScrollOffset(); + _control.ScrollViewport(currentOffset + delta); + }); +} diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h new file mode 100644 index 000000000..da3f4e3b6 --- /dev/null +++ b/src/cascadia/TerminalApp/Tab.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include +#include + +class Tab +{ + +public: + Tab(GUID profile, winrt::Microsoft::Terminal::TerminalControl::TermControl control); + ~Tab(); + + winrt::Microsoft::UI::Xaml::Controls::TabViewItem GetTabViewItem(); + winrt::Microsoft::Terminal::TerminalControl::TermControl GetTerminalControl(); + + bool IsFocused(); + void SetFocused(bool focused); + + GUID GetProfile() const noexcept; + + void Scroll(int delta); + +private: + winrt::Microsoft::Terminal::TerminalControl::TermControl _control; + bool _focused; + GUID _profile; + winrt::Microsoft::UI::Xaml::Controls::TabViewItem _tabViewItem; + + void _MakeTabViewItem(); + void _Focus(); +}; diff --git a/src/cascadia/TerminalApp/TerminalApp.def b/src/cascadia/TerminalApp/TerminalApp.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalApp.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/TerminalApp/TerminalApp.vcxproj b/src/cascadia/TerminalApp/TerminalApp.vcxproj new file mode 100644 index 000000000..6a70d00dd --- /dev/null +++ b/src/cascadia/TerminalApp/TerminalApp.vcxproj @@ -0,0 +1,131 @@ + + + + + + + DynamicLibrary + Console + + true + + + + {CA5CAD1A-44BD-4AC7-AC72-F16E576FDD12} + TerminalApp + TerminalApp + + + + + + + Designer + + + + + + + + + + + + + + + AppKeyBindings.idl + + + App.xaml + + + + + + + + + + + + + Create + + + AppKeyBindings.idl + + + App.xaml + + + + + + + + App.xaml + + + + + + + + + + + + + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + + + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + + + + + {015a0047-772d-4f1a-88c9-45c18f0adfb6} + true + true + + + + + + true + + + + WindowsApp.lib;shell32.lib;%(AdditionalDependencies) + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/cascadia/TerminalApp/packages.config b/src/cascadia/TerminalApp/packages.config new file mode 100644 index 000000000..cbbd56478 --- /dev/null +++ b/src/cascadia/TerminalApp/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/cascadia/TerminalApp/pch.cpp b/src/cascadia/TerminalApp/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/src/cascadia/TerminalApp/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalApp/pch.h b/src/cascadia/TerminalApp/pch.h new file mode 100644 index 000000000..53279a09a --- /dev/null +++ b/src/cascadia/TerminalApp/pch.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +// Including TraceLogging essentials for the binary +#include +#include +TRACELOGGING_DECLARE_PROVIDER(g_hTerminalWin32Provider); +#include +#include diff --git a/src/cascadia/TerminalConnection/ConhostConnection.cpp b/src/cascadia/TerminalConnection/ConhostConnection.cpp new file mode 100644 index 000000000..8dd984982 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConhostConnection.cpp @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ConhostConnection.h" +#include "windows.h" +#include +// STARTF_USESTDHANDLES is only defined in WINAPI_PARTITION_DESKTOP +// We're just gonna manually define it for this prototyping code +#ifndef STARTF_USESTDHANDLES +#define STARTF_USESTDHANDLES 0x00000100 +#endif + +#include + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + ConhostConnection::ConhostConnection(hstring const& commandline, + hstring const& startingDirectory, + uint32_t initialRows, + uint32_t initialCols) : + _connected{ false }, + _inPipe{ INVALID_HANDLE_VALUE }, + _outPipe{ INVALID_HANDLE_VALUE }, + _signalPipe{ INVALID_HANDLE_VALUE }, + _outputThreadId{ 0 }, + _hOutputThread{ INVALID_HANDLE_VALUE }, + _piConhost{ 0 }, + _closing{ false } + { + _commandline = commandline; + _startingDirectory = startingDirectory; + _initialRows = initialRows; + _initialCols = initialCols; + + } + + winrt::event_token ConhostConnection::TerminalOutput(Microsoft::Terminal::TerminalConnection::TerminalOutputEventArgs const& handler) + { + return _outputHandlers.add(handler); + } + + void ConhostConnection::TerminalOutput(winrt::event_token const& token) noexcept + { + _outputHandlers.remove(token); + } + + winrt::event_token ConhostConnection::TerminalDisconnected(Microsoft::Terminal::TerminalConnection::TerminalDisconnectedEventArgs const& handler) + { + return _disconnectHandlers.add(handler); + } + + void ConhostConnection::TerminalDisconnected(winrt::event_token const& token) noexcept + { + _disconnectHandlers.remove(token); + } + + void ConhostConnection::Start() + { + std::wstring cmdline = _commandline.c_str(); + std::optional startingDirectory; + if (!_startingDirectory.empty()) + { + startingDirectory = _startingDirectory; + } + + CreateConPty(cmdline, + startingDirectory, + static_cast(_initialCols), + static_cast(_initialRows), + &_inPipe, + &_outPipe, + &_signalPipe, + &_piConhost); + + _connected = true; + + // Create our own output handling thread + // Each console needs to make sure to drain the output from it's backing host. + _outputThreadId = (DWORD)-1; + _hOutputThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)StaticOutputThreadProc, + this, + 0, + &_outputThreadId); + } + + void ConhostConnection::WriteInput(hstring const& data) + { + if (!_connected || _closing) + { + return; + } + + // convert from UTF-16LE to UTF-8 as ConPty expects UTF-8 + std::string str = winrt::to_string(data); + bool fSuccess = !!WriteFile(_inPipe, str.c_str(), (DWORD)str.length(), nullptr, nullptr); + fSuccess; + } + + void ConhostConnection::Resize(uint32_t rows, uint32_t columns) + { + if (!_connected) + { + _initialRows = rows; + _initialCols = columns; + } + else if (!_closing) + { + SignalResizeWindow(_signalPipe, static_cast(columns), static_cast(rows)); + } + } + + void ConhostConnection::Close() + { + if (!_connected) return; + if (_closing) return; + _closing = true; + // TODO: + // terminate the output thread + // Close our handles + // Close the Pseudoconsole + // terminate our processes + CloseHandle(_signalPipe); + CloseHandle(_inPipe); + CloseHandle(_outPipe); + // What? CreateThread is in app partition but TerminateThread isn't? + //TerminateThread(_hOutputThread, 0); + TerminateProcess(_piConhost.hProcess, 0); + CloseHandle(_piConhost.hProcess); + } + + DWORD ConhostConnection::StaticOutputThreadProc(LPVOID lpParameter) + { + ConhostConnection* const pInstance = (ConhostConnection*)lpParameter; + return pInstance->_OutputThread(); + } + + DWORD ConhostConnection::_OutputThread() + { + const size_t bufferSize = 4096; + BYTE buffer[bufferSize]; + DWORD dwRead; + while (true) + { + dwRead = 0; + bool fSuccess = false; + + fSuccess = !!ReadFile(_outPipe, buffer, bufferSize, &dwRead, nullptr); + if (!fSuccess) + { + if (_closing) + { + // This is okay, break out to kill the thread + return 0; + } + else + { + _disconnectHandlers(); + return (DWORD)-1; + } + + } + if (dwRead == 0) continue; + // Convert buffer to hstring + char* pchStr = (char*)(buffer); + std::string str{pchStr, dwRead}; + auto hstr = winrt::to_hstring(str); + + // Pass the output to our registered event handlers + _outputHandlers(hstr); + } + } +} diff --git a/src/cascadia/TerminalConnection/ConhostConnection.h b/src/cascadia/TerminalConnection/ConhostConnection.h new file mode 100644 index 000000000..92f7f4cf5 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConhostConnection.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "ConhostConnection.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct ConhostConnection : ConhostConnectionT + { + ConhostConnection(const hstring& cmdline, const hstring& startingDirectory, uint32_t rows, uint32_t cols); + + winrt::event_token TerminalOutput(TerminalConnection::TerminalOutputEventArgs const& handler); + void TerminalOutput(winrt::event_token const& token) noexcept; + winrt::event_token TerminalDisconnected(TerminalConnection::TerminalDisconnectedEventArgs const& handler); + void TerminalDisconnected(winrt::event_token const& token) noexcept; + void Start(); + void WriteInput(hstring const& data); + void Resize(uint32_t rows, uint32_t columns); + void Close(); + + private: + winrt::event _outputHandlers; + winrt::event _disconnectHandlers; + + uint32_t _initialRows; + uint32_t _initialCols; + hstring _commandline; + hstring _startingDirectory; + + bool _connected; + HANDLE _inPipe; // The pipe for writing input to + HANDLE _outPipe; // The pipe for reading output from + HANDLE _signalPipe; + //HPCON _hPC; + DWORD _outputThreadId; + HANDLE _hOutputThread; + PROCESS_INFORMATION _piConhost; + bool _closing; + + static DWORD StaticOutputThreadProc(LPVOID lpParameter); + DWORD _OutputThread(); + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + struct ConhostConnection : ConhostConnectionT + { + }; +} diff --git a/src/cascadia/TerminalConnection/ConhostConnection.idl b/src/cascadia/TerminalConnection/ConhostConnection.idl new file mode 100644 index 000000000..28ce3bcf8 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConhostConnection.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass ConhostConnection : ITerminalConnection + { + ConhostConnection(String cmdline, String startingDirectory, UInt32 rows, UInt32 columns); + }; + +} diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp new file mode 100644 index 000000000..6ca4df089 --- /dev/null +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ConptyConnection.h" + +#include + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + ConptyConnection::ConptyConnection(hstring const& commandline, + uint32_t initialRows, + uint32_t initialCols) : + _connected{ false }, + _inPipe{ INVALID_HANDLE_VALUE }, + _outPipe{ INVALID_HANDLE_VALUE }, + _hPC{ INVALID_HANDLE_VALUE }, + _outputThreadId{ 0 }, + _hOutputThread{ INVALID_HANDLE_VALUE }, + _piClient{ 0 } + { + _commandline = commandline; + _initialRows = initialRows; + _initialCols = initialCols; + + } + + winrt::event_token ConptyConnection::TerminalOutput(TerminalConnection::TerminalOutputEventArgs const& handler) + { + return _outputHandlers.add(handler); + } + + void ConptyConnection::TerminalOutput(winrt::event_token const& token) noexcept + { + _outputHandlers.remove(token); + } + + winrt::event_token ConptyConnection::TerminalDisconnected(TerminalConnection::TerminalDisconnectedEventArgs const& handler) + { + handler; + + throw hresult_not_implemented(); + } + + void ConptyConnection::TerminalDisconnected(winrt::event_token const& token) noexcept + { + token; + + // throw hresult_not_implemented(); + } + + void ConptyConnection::Start() + { + _CreatePseudoConsole(); + + _connected = true; + + // Create our own output handling thread + // Each console needs to make sure to drain the output from it's backing host. + _outputThreadId = (DWORD)-1; + _hOutputThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)StaticOutputThreadProc, + this, + 0, + &_outputThreadId); + + //// When we recieve some data: + //hstring outputFromConpty = L"hello world"; + ////TerminalOutputEventArgs args(outputFromConpty); + ////_outputHandlers(*this, args); + //_outputHandlers(outputFromConpty); + } + + void ConptyConnection::WriteInput(hstring const& data) + { + data; + + throw hresult_not_implemented(); + } + + void ConptyConnection::Resize(uint32_t rows, uint32_t columns) + { + rows; + columns; + + throw hresult_not_implemented(); + } + + void ConptyConnection::Close() + { + // TODO: + // terminate the output thread + // Close our handles + // Close the Pseudoconsole + // terminate our processes + throw hresult_not_implemented(); + } + + + // Function Description: + // - Sample function which combines the creation of some basic anonymous pipes + // and passes them to CreatePseudoConsole. + // Arguments: + // - size: The size of the conpty to create, in characters. + // - phInput: Receives the handle to the newly-created anonymous pipe for writing input to the conpty. + // - phOutput: Receives the handle to the newly-created anonymous pipe for reading the output of the conpty. + // - phPty: Receives a token value to identify this conpty + HRESULT _CreatePseudoConsoleAndHandles(COORD size, + _In_ DWORD dwFlags, + _Out_ HANDLE* phInput, + _Out_ HANDLE* phOutput, + _Out_ HPCON* phPC) + { + if(phPC == NULL || phInput == NULL || phOutput == NULL) + { + return E_INVALIDARG; + } + + HANDLE outPipeOurSide; + HANDLE inPipeOurSide; + HANDLE outPipePseudoConsoleSide; + HANDLE inPipePseudoConsoleSide; + + HRESULT hr = S_OK; + if (!CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, NULL, 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + if (SUCCEEDED(hr)) + { + if (!CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, NULL, 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + if (SUCCEEDED(hr)) + { + hr = CreatePseudoConsole(size, inPipePseudoConsoleSide, outPipePseudoConsoleSide, dwFlags, phPC); + if (FAILED(hr)) + { + CloseHandle(inPipeOurSide); + CloseHandle(outPipeOurSide); + } + else + { + *phInput = inPipeOurSide; + *phOutput = outPipeOurSide; + } + CloseHandle(outPipePseudoConsoleSide); + } + else + { + CloseHandle(inPipeOurSide); + } + CloseHandle(inPipePseudoConsoleSide); + } + return hr; + } + + // Prepares the `lpAttributeList` member of a STARTUPINFOEX for attaching a + // client application to a pseudoconsole. + // Prior to calling this function, hPty should be initialized with a call to + // CreatePseudoConsole, and the pAttrList should be initialized with a call + // to InitializeProcThreadAttributeList. The caller of + // InitializeProcThreadAttributeList should add one to the dwAttributeCount + // param when creating the attribute list for usage by this function. + HRESULT _AttachPseudoConsole(HPCON hPC, LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList) + { + BOOL fSuccess = UpdateProcThreadAttribute(lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + hPC, + sizeof(HPCON), + NULL, + NULL); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); + } + + void ConptyConnection::_CreatePseudoConsole() + { + bool fSuccess; + + COORD dimensions{_initialCols, _initialRows}; + THROW_IF_FAILED(_CreatePseudoConsoleAndHandles(dimensions, 0, &_inPipe, &_outPipe, &_hPC)); + + STARTUPINFOEX siEx; + siEx = { 0 }; + siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); + size_t size; + InitializeProcThreadAttributeList(NULL, 1, 0, (PSIZE_T)&size); + BYTE* attrList = new BYTE[size]; + siEx.lpAttributeList = reinterpret_cast(attrList); + fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, (PSIZE_T)&size); + THROW_LAST_ERROR_IF(!fSuccess); + + THROW_IF_FAILED(_AttachPseudoConsole(_hPC, siEx.lpAttributeList)); + + std::wstring realCommand = _commandline.c_str();//winrt::to_string(_commandline); + if (realCommand == L""){ + realCommand = L"cmd.exe"; + } + + std::unique_ptr mutableCommandline = std::make_unique(realCommand.length() + 1); + THROW_IF_NULL_ALLOC(mutableCommandline); + + HRESULT hr = StringCchCopy(mutableCommandline.get(), realCommand.length()+1, realCommand.c_str()); + THROW_IF_FAILED(hr); + fSuccess = !!CreateProcessW( + nullptr, + mutableCommandline.get(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + true, // bInheritHandles + EXTENDED_STARTUPINFO_PRESENT, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &siEx.StartupInfo, // lpStartupInfo + &_piClient // lpProcessInformation + ); + THROW_LAST_ERROR_IF(!fSuccess); + DeleteProcThreadAttributeList(siEx.lpAttributeList); + } + + + DWORD ConptyConnection::StaticOutputThreadProc(LPVOID lpParameter) + { + ConptyConnection* const pInstance = (ConptyConnection*)lpParameter; + return pInstance->_OutputThread(); + } + + DWORD ConptyConnection::_OutputThread() + { + BYTE buffer[256]; + DWORD dwRead; + while (true) + { + dwRead = 0; + bool fSuccess = false; + + fSuccess = !!ReadFile(_outPipe, buffer, ARRAYSIZE(buffer), &dwRead, nullptr); + + THROW_LAST_ERROR_IF(!fSuccess); + + // Convert buffer to hstring + char* pchStr = (char*)(buffer); + std::string str{pchStr, dwRead}; + auto hstr = winrt::to_hstring(str); + + // Pass the output to our registered event handlers + _outputHandlers(hstr); + + // if (this->_active) + // { + // _pfnReadCallback(buffer, dwRead); + // } + } + } +} diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h new file mode 100644 index 000000000..c247eedea --- /dev/null +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "ConptyConnection.g.h" +// Note that the ConptyConnection is no longer a part of this project +// Until there's platform-level support for full-trust universal applications, +// all ProcThreadAttribute things will be unusable. Unfortunately, this means +// we'll be unable to use the conpty API directly. +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct ConptyConnection : ConptyConnectionT + { + ConptyConnection() = delete; + ConptyConnection(hstring const& commandline, uint32_t initialRows, uint32_t initialCols); + + winrt::event_token TerminalOutput(TerminalConnection::TerminalOutputEventArgs const& handler); + void TerminalOutput(winrt::event_token const& token) noexcept; + winrt::event_token TerminalDisconnected(TerminalConnection::TerminalDisconnectedEventArgs const& handler); + void TerminalDisconnected(winrt::event_token const& token) noexcept; + void Start(); + void WriteInput(hstring const& data); + void Resize(uint32_t rows, uint32_t columns); + void Close(); + + private: + winrt::event _outputHandlers; + + uint32_t _initialRows; + uint32_t _initialCols; + hstring _commandline; + + bool _connected; + HANDLE _inPipe; // The pipe for writing input to + HANDLE _outPipe; // The pipe for reading output from + HPCON _hPC; + DWORD _outputThreadId; + HANDLE _hOutputThread; + PROCESS_INFORMATION _piClient; + + static DWORD StaticOutputThreadProc(LPVOID lpParameter); + void _CreatePseudoConsole(); + DWORD _OutputThread(); + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + struct ConptyConnection : ConptyConnectionT + { + }; +} diff --git a/src/cascadia/TerminalConnection/ConptyConnection.idl b/src/cascadia/TerminalConnection/ConptyConnection.idl new file mode 100644 index 000000000..7e477ce9d --- /dev/null +++ b/src/cascadia/TerminalConnection/ConptyConnection.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass ConptyConnection : ITerminalConnection + { + ConptyConnection(String commandline, UInt32 initialRows, UInt32 initialCols); + }; + +} diff --git a/src/cascadia/TerminalConnection/EchoConnection.cpp b/src/cascadia/TerminalConnection/EchoConnection.cpp new file mode 100644 index 000000000..a444f9bfd --- /dev/null +++ b/src/cascadia/TerminalConnection/EchoConnection.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "EchoConnection.h" +#include + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + EchoConnection::EchoConnection() + { + } + + winrt::event_token EchoConnection::TerminalOutput(TerminalConnection::TerminalOutputEventArgs const& handler) + { + return _outputHandlers.add(handler); + } + + void EchoConnection::TerminalOutput(winrt::event_token const& token) noexcept + { + _outputHandlers.remove(token); + } + + winrt::event_token EchoConnection::TerminalDisconnected(TerminalConnection::TerminalDisconnectedEventArgs const& handler) + { + handler; + throw hresult_not_implemented(); + } + + void EchoConnection::TerminalDisconnected(winrt::event_token const& token) noexcept + { + token; + } + + void EchoConnection::Start() + { + } + + void EchoConnection::WriteInput(hstring const& data) + { + std::wstringstream prettyPrint; + for (wchar_t wch : data) + { + if (wch < 0x20) + { + prettyPrint << L"^" << (wchar_t)(wch+0x40); + } + else if (wch == 0x7f) + { + prettyPrint << L"0x7f"; + } + else + { + prettyPrint << wch; + } + } + _outputHandlers(prettyPrint.str()); + } + + void EchoConnection::Resize(uint32_t rows, uint32_t columns) + { + rows; + columns; + + throw hresult_not_implemented(); + } + + void EchoConnection::Close() + { + throw hresult_not_implemented(); + } +} diff --git a/src/cascadia/TerminalConnection/EchoConnection.h b/src/cascadia/TerminalConnection/EchoConnection.h new file mode 100644 index 000000000..0f60a5633 --- /dev/null +++ b/src/cascadia/TerminalConnection/EchoConnection.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "EchoConnection.g.h" + +namespace winrt::Microsoft::Terminal::TerminalConnection::implementation +{ + struct EchoConnection : EchoConnectionT + { + EchoConnection(); + + winrt::event_token TerminalOutput(TerminalConnection::TerminalOutputEventArgs const& handler); + void TerminalOutput(winrt::event_token const& token) noexcept; + winrt::event_token TerminalDisconnected(TerminalConnection::TerminalDisconnectedEventArgs const& handler); + void TerminalDisconnected(winrt::event_token const& token) noexcept; + void Start(); + void WriteInput(hstring const& data); + void Resize(uint32_t rows, uint32_t columns); + void Close(); + + private: + winrt::event _outputHandlers; + }; +} + +namespace winrt::Microsoft::Terminal::TerminalConnection::factory_implementation +{ + struct EchoConnection : EchoConnectionT + { + }; +} diff --git a/src/cascadia/TerminalConnection/EchoConnection.idl b/src/cascadia/TerminalConnection/EchoConnection.idl new file mode 100644 index 000000000..411fcf84d --- /dev/null +++ b/src/cascadia/TerminalConnection/EchoConnection.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ITerminalConnection.idl"; + +namespace Microsoft.Terminal.TerminalConnection +{ + [default_interface] + runtimeclass EchoConnection : ITerminalConnection + { + EchoConnection(); + }; + +} diff --git a/src/cascadia/TerminalConnection/ITerminalConnection.idl b/src/cascadia/TerminalConnection/ITerminalConnection.idl new file mode 100644 index 000000000..679623629 --- /dev/null +++ b/src/cascadia/TerminalConnection/ITerminalConnection.idl @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.TerminalConnection +{ + delegate void TerminalOutputEventArgs(String output); + delegate void TerminalDisconnectedEventArgs(); + + interface ITerminalConnection + { + event TerminalOutputEventArgs TerminalOutput; + event TerminalDisconnectedEventArgs TerminalDisconnected; + + void Start(); + void WriteInput(String data); + void Resize(UInt32 rows, UInt32 columns); + void Close(); + }; + +} diff --git a/src/cascadia/TerminalConnection/TerminalConnection.def b/src/cascadia/TerminalConnection/TerminalConnection.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/src/cascadia/TerminalConnection/TerminalConnection.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj new file mode 100644 index 000000000..5f7beebd3 --- /dev/null +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj @@ -0,0 +1,66 @@ + + + + + + DynamicLibrary + Console + true + + + + + {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} + TerminalConnection + Microsoft.Terminal.TerminalConnection + + + + + + ConhostConnection.idl + + + EchoConnection.idl + + + + + Create + + + ConhostConnection.idl + + + EchoConnection.idl + + + + + + + + + + + + + + + kernel32.lib;%(AdditionalDependencies) + + + + + + + true + + + + + diff --git a/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters new file mode 100644 index 000000000..0de151cdc --- /dev/null +++ b/src/cascadia/TerminalConnection/TerminalConnection.vcxproj.filters @@ -0,0 +1,35 @@ + + + + + accd3aa8-1ba0-4223-9bbe-0c431709210b + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {926ab91d-31b4-48c3-b9a4-e681349f27f0} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/TerminalConnection/packages.config b/src/cascadia/TerminalConnection/packages.config new file mode 100644 index 000000000..4f7a6f98a --- /dev/null +++ b/src/cascadia/TerminalConnection/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/TerminalConnection/pch.cpp b/src/cascadia/TerminalConnection/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/src/cascadia/TerminalConnection/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalConnection/pch.h b/src/cascadia/TerminalConnection/pch.h new file mode 100644 index 000000000..4417301bb --- /dev/null +++ b/src/cascadia/TerminalConnection/pch.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#include "winrt/Windows.Foundation.h" +#include diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp new file mode 100644 index 000000000..fe2608e3c --- /dev/null +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -0,0 +1,1139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TermControl.h" +#include +#include +#include +#include + +using namespace ::Microsoft::Console::Types; +using namespace ::Microsoft::Terminal::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; +using namespace winrt::Microsoft::Terminal::Settings; + +namespace winrt::Microsoft::Terminal::TerminalControl::implementation +{ + + TermControl::TermControl() : + TermControl(Settings::TerminalSettings{}) + { + } + + TermControl::TermControl(Settings::IControlSettings settings) : + _connection{ TerminalConnection::ConhostConnection(winrt::to_hstring("cmd.exe"), winrt::hstring(), 30, 80) }, + _initializedTerminal{ false }, + _root{ nullptr }, + _controlRoot{ nullptr }, + _swapChainPanel{ nullptr }, + _settings{ settings }, + _closing{ false }, + _lastScrollOffset{ std::nullopt }, + _desiredFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 }, + _actualFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false }, + _touchAnchor{ std::nullopt }, + _leadingSurrogate{} + { + _Create(); + } + + void TermControl::_Create() + { + // Create a dummy UserControl to use as the "root" of our control we'll + // build manually. + Controls::UserControl myControl; + _controlRoot = myControl; + + Controls::Grid container; + + Controls::ColumnDefinition contentColumn{}; + Controls::ColumnDefinition scrollbarColumn{}; + contentColumn.Width(GridLength{ 1.0, GridUnitType::Star }); + scrollbarColumn.Width(GridLength{ 1.0, GridUnitType::Auto }); + + container.ColumnDefinitions().Append(contentColumn); + container.ColumnDefinitions().Append(scrollbarColumn); + + _scrollBar = Controls::Primitives::ScrollBar{}; + _scrollBar.Orientation(Controls::Orientation::Vertical); + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + _scrollBar.HorizontalAlignment(HorizontalAlignment::Right); + _scrollBar.VerticalAlignment(VerticalAlignment::Stretch); + + // Initialize the scrollbar with some placeholder values. + // The scrollbar will be updated with real values on _Initialize + _scrollBar.Maximum(1); + _scrollBar.ViewportSize(10); + _scrollBar.IsTabStop(false); + _scrollBar.SmallChange(1); + _scrollBar.LargeChange(4); + _scrollBar.Visibility(Visibility::Visible); + + // Create the SwapChainPanel that will display our content + Controls::SwapChainPanel swapChainPanel; + swapChainPanel.SetValue(FrameworkElement::HorizontalAlignmentProperty(), + box_value(HorizontalAlignment::Stretch)); + swapChainPanel.SetValue(FrameworkElement::HorizontalAlignmentProperty(), + box_value(HorizontalAlignment::Stretch)); + + + swapChainPanel.SizeChanged({ this, &TermControl::_SwapChainSizeChanged }); + swapChainPanel.CompositionScaleChanged({ this, &TermControl::_SwapChainScaleChanged }); + + // Initialize the terminal only once the swapchainpanel is loaded - that + // way, we'll be able to query the real pixel size it got on layout + swapChainPanel.Loaded([this] (auto /*s*/, auto /*e*/){ + _InitializeTerminal(); + }); + + container.Children().Append(swapChainPanel); + container.Children().Append(_scrollBar); + Controls::Grid::SetColumn(swapChainPanel, 0); + Controls::Grid::SetColumn(_scrollBar, 1); + + _root = container; + _swapChainPanel = swapChainPanel; + _controlRoot.Content(_root); + + _ApplyUISettings(); + _ApplyConnectionSettings(); + + // These are important: + // 1. When we get tapped, focus us + _controlRoot.Tapped([this](auto&, auto& e) { + _controlRoot.Focus(FocusState::Pointer); + e.Handled(true); + }); + // 2. Make sure we can be focused (why this isn't `Focusable` I'll never know) + _controlRoot.IsTabStop(true); + // 3. Actually not sure about this one. Maybe it isn't necessary either. + _controlRoot.AllowFocusOnInteraction(true); + + // DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that. + + // Subscribe to the connection's disconnected event and call our connection closed handlers. + _connection.TerminalDisconnected([=]() { + _connectionClosedHandlers(); + }); + } + + // Method Description: + // - Given new settings for this profile, applies the settings to the current terminal. + // Arguments: + // - newSettings: New settings values for the profile in this terminal. + // Return Value: + // - + void TermControl::UpdateSettings(Settings::IControlSettings newSettings) + { + _settings = newSettings; + + // Dispatch a call to the UI thread to apply the new settings to the + // terminal. + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal,[this](){ + // Update our control settings + _ApplyUISettings(); + // Update the terminal core with it's new Core settings + _terminal->UpdateSettings(_settings); + + // Refresh our font with the renderer + _UpdateFont(); + + const auto width = _swapChainPanel.ActualWidth(); + const auto height = _swapChainPanel.ActualHeight(); + if (width != 0 && height != 0) + { + // If the font size changed, or the _swapchainPanel's size changed + // for any reason, we'll need to make sure to also resize the + // buffer. _DoResize will invalidate everything for us. + auto lock = _terminal->LockForWriting(); + _DoResize(width, height); + } + }); + } + + // Method Description: + // - Style our UI elements based on the values in our _settings, and set up + // other control-specific settings. This method will be called whenever + // the settings are reloaded. + // * Sets up the background of the control with the provided BG color, + // acrylic or not, and if acrylic, then uses the opacity from _settings. + // - Core settings will be passed to the terminal in _InitializeTerminal + // Arguments: + // - + // Return Value: + // - + void TermControl::_ApplyUISettings() + { + winrt::Windows::UI::Color bgColor{}; + uint32_t bg = _settings.DefaultBackground(); + const auto R = GetRValue(bg); + const auto G = GetGValue(bg); + const auto B = GetBValue(bg); + bgColor.R = R; + bgColor.G = G; + bgColor.B = B; + bgColor.A = 255; + + if (_settings.UseAcrylic()) + { + Media::AcrylicBrush acrylic{}; + acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + acrylic.FallbackColor(bgColor); + acrylic.TintColor(bgColor); + acrylic.TintOpacity(_settings.TintOpacity()); + _root.Background(acrylic); + + // If we're acrylic, we want to make sure that the default BG color + // is transparent, so we can see the acrylic effect on text with the + // default BG color. + _settings.DefaultBackground(ARGB(0, R, G, B)); + } + else + { + Media::SolidColorBrush solidColor{}; + solidColor.Color(bgColor); + _root.Background(solidColor); + _settings.DefaultBackground(RGB(R, G, B)); + } + + // Apply padding to the root Grid + auto thickness = _ParseThicknessFromPadding(_settings.Padding()); + _root.Padding(thickness); + + // Initialize our font information. + const auto* fontFace = _settings.FontFace().c_str(); + const short fontHeight = gsl::narrow(_settings.FontSize()); + // The font width doesn't terribly matter, we'll only be using the + // height to look it up + // The other params here also largely don't matter. + // The family is only used to determine if the font is truetype or + // not, but DX doesn't use that info at all. + // The Codepage is additionally not actually used by the DX engine at all. + _actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + } + + // Method Description: + // - Create a connection based on the values in our settings object. + // * Gets the commandline and working directory out of the _settings and + // creates a ConhostConnection with the given commandline and starting + // directory. + void TermControl::_ApplyConnectionSettings() + { + _connection = TerminalConnection::ConhostConnection(_settings.Commandline(), _settings.StartingDirectory(), 30, 80); + } + + TermControl::~TermControl() + { + _closing = true; + // Don't let anyone else do something to the buffer. + auto lock = _terminal->LockForWriting(); + + if (_connection != nullptr) + { + _connection.Close(); + } + + _renderer->TriggerTeardown(); + + _swapChainPanel = nullptr; + _root = nullptr; + _connection = nullptr; + } + + UIElement TermControl::GetRoot() + { + return _root; + } + + Controls::UserControl TermControl::GetControl() + { + return _controlRoot; + } + + void TermControl::SwapChainChanged() + { + if (!_initializedTerminal) + { + return; + } + + auto chain = _renderEngine->GetSwapChain(); + _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [=]() + { + auto lock = _terminal->LockForWriting(); + auto nativePanel = _swapChainPanel.as(); + nativePanel->SetSwapChain(chain.Get()); + }); + } + + void TermControl::_InitializeTerminal() + { + if (_initializedTerminal) + { + return; + } + + const auto windowWidth = _swapChainPanel.ActualWidth(); // Width() and Height() are NaN? + const auto windowHeight = _swapChainPanel.ActualHeight(); + + _terminal = new ::Microsoft::Terminal::Core::Terminal(); + + // First create the render thread. + auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); + // Stash a local pointer to the render thread, so we can enable it after + // we hand off ownership to the renderer. + auto* const localPointerToThread = renderThread.get(); + _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal, nullptr, 0, std::move(renderThread)); + ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; + + // Set up the DX Engine + auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); + _renderer->AddRenderEngine(dxEngine.get()); + + // Initialize our font with the renderer + // We don't have to care about DPI. We'll get a change message immediately if it's not 96 + // and react accordingly. + _UpdateFont(); + + const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; + + // Fist set up the dx engine with the window size in pixels. + // Then, using the font, get the number of characters that can fit. + // Resize our terminal connection to match that size, and initialize the terminal with that size. + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); + THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); + const auto width = vp.Width(); + const auto height = vp.Height(); + _connection.Resize(height, width); + + // Override the default width and height to match the size of the swapChainPanel + _settings.InitialCols(width); + _settings.InitialRows(height); + + _terminal->CreateFromSettings(_settings, renderTarget); + + // Tell the DX Engine to notify us when the swap chain changes. + dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this)); + + THROW_IF_FAILED(dxEngine->Enable()); + _renderEngine = std::move(dxEngine); + + auto onRecieveOutputFn = [this](const hstring str) { + _terminal->Write(str.c_str()); + }; + _connectionOutputEventToken = _connection.TerminalOutput(onRecieveOutputFn); + + auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1); + _terminal->SetWriteInputCallback(inputFn); + + THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); + + auto chain = _renderEngine->GetSwapChain(); + _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [this, chain]() + { + _terminal->LockConsole(); + auto nativePanel = _swapChainPanel.as(); + nativePanel->SetSwapChain(chain.Get()); + _terminal->UnlockConsole(); + }); + + // Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height + auto bottom = _terminal->GetViewport().BottomExclusive(); + auto bufferHeight = bottom; + + const auto originalMaximum = _scrollBar.Maximum(); + const auto originalMinimum = _scrollBar.Minimum(); + const auto originalValue = _scrollBar.Value(); + const auto originalViewportSize = _scrollBar.ViewportSize(); + + _scrollBar.Maximum(bufferHeight - bufferHeight); + _scrollBar.Minimum(0); + _scrollBar.Value(0); + _scrollBar.ViewportSize(bufferHeight); + _scrollBar.ValueChanged({ this, &TermControl::_ScrollbarChangeHandler }); + + // Apply settings for scrollbar + if (_settings.ScrollState() == ScrollbarState::Visible) + { + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + } + else if (_settings.ScrollState() == ScrollbarState::Hidden) + { + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None); + + // In the scenario where the user has turned off the OS setting to automatically hide scollbars, the + // Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to + // achieve the intended effect. + _scrollBar.Visibility(Visibility::Collapsed); + } + else + { + // Default behavior + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + } + + _root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler }); + _root.PointerPressed({ this, &TermControl::_MouseClickHandler }); + _root.PointerMoved({ this, &TermControl::_MouseMovedHandler }); + _root.PointerReleased({ this, &TermControl::_PointerReleasedHandler }); + + localPointerToThread->EnablePainting(); + + // No matter what order these guys are in, The KeyDown's will fire + // before the CharacterRecieved, so we can't easily get characters + // first, then fallback to getting keys from vkeys. + // TODO: This apparently handles keys and characters correctly, though + // I'd keep an eye on it, and test more. + // I presume that the characters that aren't translated by terminalInput + // just end up getting ignored, and the rest of the input comes + // through CharacterRecieved. + // I don't believe there's a difference between KeyDown and + // PreviewKeyDown for our purposes + // These two handlers _must_ be on _controlRoot, not _root. + _controlRoot.PreviewKeyDown({this, &TermControl::_KeyDownHandler }); + _controlRoot.CharacterReceived({this, &TermControl::_CharacterHandler }); + + auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); + _terminal->SetTitleChangedCallback(pfnTitleChanged); + + auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); + + // Focus the control here. If we do it up above (in _Create_), then the + // focus won't actually get passed to us. I believe this is because + // we're not technically a part of the UI tree yet, so focusing us + // becomes a no-op. + _controlRoot.Focus(FocusState::Programmatic); + + _connection.Start(); + _initializedTerminal = true; + } + + void TermControl::_CharacterHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, + Input::CharacterReceivedRoutedEventArgs const& e) + { + if (_closing) + { + return; + } + + const auto ch = e.Character(); + + // We want Backspace to be handled by _KeyDownHandler, so the + // terminalInput can translate it into a \x7f. So, do nothing here, so + // we don't end up sending both a BS and a DEL to the terminal. + // Also skip processing DEL here, which will hit here when Ctrl+Bkspc is + // pressed, but after it's handled by the _KeyDownHandler below. + if (ch == UNICODE_BACKSPACE || ch == UNICODE_DEL) + { + return; + } + else if (Utf16Parser::IsLeadingSurrogate(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 + auto hstr = to_hstring(_leadingSurrogate.value()); + _connection.WriteInput(hstr); + } + // 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()) + { + std::wstring wstr; + wstr.reserve(2); + wstr.push_back(_leadingSurrogate.value()); + wstr.push_back(ch); + _leadingSurrogate.reset(); + + auto hstr = to_hstring(wstr.c_str()); + _connection.WriteInput(hstr); + } + else + { + auto hstr = to_hstring(ch); + _connection.WriteInput(hstr); + } + e.Handled(true); + } + + void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, + Input::KeyRoutedEventArgs const& e) + { + // mark event as handled and do nothing if... + // - closing + // - key modifier is pressed + // NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below. + if (_closing || + e.OriginalKey() == VirtualKey::Control || + e.OriginalKey() == VirtualKey::Shift || + e.OriginalKey() == VirtualKey::Menu) + { + e.Handled(true); + return; + } + + auto modifiers = _GetPressedModifierKeys(); + + const auto vkey = static_cast(e.OriginalKey()); + + bool handled = false; + auto bindings = _settings.KeyBindings(); + if (bindings) + { + KeyChord chord(modifiers, vkey); + handled = bindings.TryKeyChord(chord); + } + + if (!handled) + { + _terminal->ClearSelection(); + // If the terminal translated the key, mark the event as handled. + // This will prevent the system from trying to get the character out + // of it and sending us a CharacterRecieved event. + handled = _terminal->SendKeyEvent(vkey, + WI_IsFlagSet(modifiers, KeyModifiers::Ctrl), + WI_IsFlagSet(modifiers, KeyModifiers::Alt), + WI_IsFlagSet(modifiers, KeyModifiers::Shift)); + } + + e.Handled(handled); + } + + // Method Description: + // - handle a mouse click event. Begin selection process. + // Arguments: + // - sender: not used + // - args: event data + void TermControl::_MouseClickHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + const auto ptr = args.Pointer(); + const auto point = args.GetCurrentPoint(_root); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse) + { + const auto modifiers = args.KeyModifiers(); + const auto altEnabled = WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu); + const auto shiftEnabled = WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift); + + if (point.Properties().IsLeftButtonPressed()) + { + const auto cursorPosition = point.Position(); + + const auto fontSize = _actualFont.GetSize(); + + const COORD terminalPosition = { + static_cast(cursorPosition.X / fontSize.X), + static_cast(cursorPosition.Y / fontSize.Y) + }; + + // save location before rendering + _terminal->SetSelectionAnchor(terminalPosition); + + // handle ALT key + _terminal->SetBoxSelection(altEnabled); + + _renderer->TriggerSelection(); + } + else if (point.Properties().IsRightButtonPressed()) + { + // copy selection, if one exists + if (_terminal->IsSelectionActive()) + { + CopySelectionToClipboard(!shiftEnabled); + } + // paste selection, otherwise + else + { + // attach TermControl::_SendInputToConnection() as the clipboardDataHandler. + // This is called when the clipboard data is loaded. + auto clipboardDataHandler = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1); + auto pasteArgs = winrt::make_self(clipboardDataHandler); + + // send paste event up to TermApp + _clipboardPasteHandlers(*this, *pasteArgs); + } + } + } + else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + { + const auto contactRect = point.Properties().ContactRect(); + // Set our touch rect, to start a pan. + _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; + } + + args.Handled(true); + } + + // Method Description: + // - handle a mouse moved event. Specifically handling mouse drag to update selection process. + // Arguments: + // - sender: not used + // - args: event data + void TermControl::_MouseMovedHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + const auto ptr = args.Pointer(); + const auto point = args.GetCurrentPoint(_root); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse) + { + if (point.Properties().IsLeftButtonPressed()) + { + const auto cursorPosition = point.Position(); + + const auto fontSize = _actualFont.GetSize(); + + const COORD terminalPosition = { + static_cast(cursorPosition.X / fontSize.X), + static_cast(cursorPosition.Y / fontSize.Y) + }; + + // save location (for rendering) + render + _terminal->SetEndSelectionPosition(terminalPosition); + _renderer->TriggerSelection(); + } + } + else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor) + { + const auto contactRect = point.Properties().ContactRect(); + winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y }; + const auto anchor = _touchAnchor.value(); + + // Get the difference between the point we've dragged to and the start of the touch. + const float fontHeight = float(_actualFont.GetSize().Y); + + const float dy = newTouchPoint.Y - anchor.Y; + + // If we've moved more than one row of text, we'll want to scroll the viewport + if (std::abs(dy) > fontHeight) + { + // Multiply by -1, because moving the touch point down will + // create a positive delta, but we want the viewport to move up, + // so we'll need a negative scroll amount (and the inverse for + // panning down) + const float numRows = -1.0f * (dy / fontHeight); + + const auto currentOffset = this->GetScrollOffset(); + const double newValue = (numRows) + (currentOffset); + + // Clear our expected scroll offset. The viewport will now move + // in response to our user input. + _lastScrollOffset = std::nullopt; + _scrollBar.Value(static_cast(newValue)); + + // Use this point as our new scroll anchor. + _touchAnchor = newTouchPoint; + } + } + args.Handled(true); + } + + // Method Description: + // - Event handler for the PointerReleased event. We use this to de-anchor + // touch events, to stop scrolling via touch. + // Arguments: + // - sender: not used + // - args: event data + // Return Value: + // - + void TermControl::_PointerReleasedHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + const auto ptr = args.Pointer(); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + { + _touchAnchor = std::nullopt; + } + + args.Handled(true); + } + + // Method Description: + // - Event handler for the PointerWheelChanged event. This is raised in + // response to mouse wheel changes. Depending upon what modifier keys are + // pressed, different actions will take place. + // Arguments: + // - args: the event args containing information about t`he mouse wheel event. + void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + auto delta = args.GetCurrentPoint(_root).Properties().MouseWheelDelta(); + // Get the state of the Ctrl & Shift keys + const auto modifiers = args.KeyModifiers(); + const auto ctrlPressed = WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control); + const auto shiftPressed = WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift); + + if (ctrlPressed && shiftPressed) + { + _MouseTransparencyHandler(delta); + } + else if (ctrlPressed) + { + _MouseZoomHandler(delta); + } + else + { + _MouseScrollHandler(delta); + } + } + + // Method Description: + // - Adjust the opacity of the acrylic background in response to a mouse + // scrolling event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseTransparencyHandler(const double mouseDelta) + { + // Transparency is on a scale of [0.0,1.0], so only increment by .01. + const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01; + + if (_settings.UseAcrylic()) + { + try + { + auto acrylicBrush = _root.Background().as(); + acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta); + } + CATCH_LOG(); + } + } + + // Method Description: + // - Adjust the font size of the terminal in response to a mouse scrolling + // event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseZoomHandler(const double mouseDelta) + { + const auto fontDelta = mouseDelta < 0 ? -1 : 1; + try + { + // Make sure we have a non-zero font size + const auto newSize = std::max(gsl::narrow(_desiredFont.GetEngineSize().Y + fontDelta), static_cast(1)); + const auto* fontFace = _settings.FontFace().c_str(); + _actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + + // Refresh our font with the renderer + _UpdateFont(); + // Resize the terminal's BUFFER to match the new font size. This does + // NOT change the size of the window, because that can lead to more + // problems (like what happens when you change the font size while the + // window is maximized?) + auto lock = _terminal->LockForWriting(); + _DoResize(_swapChainPanel.ActualWidth(), _swapChainPanel.ActualHeight()); + } + CATCH_LOG(); + } + + // Method Description: + // - Scroll the visible viewport in response to a mouse wheel event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseScrollHandler(const double mouseDelta) + { + const auto currentOffset = this->GetScrollOffset(); + + // negative = down, positive = up + // However, for us, the signs are flipped. + const auto rowDelta = mouseDelta < 0 ? 1.0 : -1.0; + + // TODO: Should we be getting some setting from the system + // for number of lines scrolled? + // With one of the precision mouses, one click is always a multiple of 120, + // but the "smooth scrolling" mode results in non-int values + + // Conhost seems to use four lines at a time, so we'll emulate that for now. + double newValue = (4 * rowDelta) + (currentOffset); + + // Clear our expected scroll offset. The viewport will now move in + // response to our user input. + _lastScrollOffset = std::nullopt; + // The scroll bar's ValueChanged handler will actually move the viewport + // for us. + _scrollBar.Value(static_cast(newValue)); + } + + void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, + Controls::Primitives::RangeBaseValueChangedEventArgs const& args) + { + const auto newValue = args.NewValue(); + + // If we've stored a lastScrollOffset, that means the terminal has + // initiated some scrolling operation. We're responding to that event here. + if (_lastScrollOffset.has_value()) + { + // If this event's offset is the same as the last offset message + // we've sent, then clear out the expected offset. We do this + // because in that case, the message we're replying to was the + // last scroll event we raised. + // Regardless, we're going to ignore this message, because the + // terminal is already in the scroll position it wants. + const auto ourLastOffset = _lastScrollOffset.value(); + if (newValue == ourLastOffset) + { + _lastScrollOffset = std::nullopt; + } + } + else + { + // This is a scroll event that wasn't initiated by the termnial + // itself - it was initiated by the mouse wheel, or the scrollbar. + this->ScrollViewport(static_cast(newValue)); + } + } + + void TermControl::_SendInputToConnection(const std::wstring& wstr) + { + _connection.WriteInput(wstr); + } + + // Method Description: + // - Update the font with the renderer. This will be called either when the + // font changes or the DPI changes, as DPI changes will necessitate a + // font change. This method will *not* change the buffer/viewport size + // to account for the new glyph dimensions. Callers should make sure to + // appropriately call _DoResize after this method is called. + void TermControl::_UpdateFont() + { + auto lock = _terminal->LockForWriting(); + + const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); + + // TODO: MSFT:20895307 If the font doesn't exist, this doesn't + // actually fail. We need a way to gracefully fallback. + _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); + + } + + // Method Description: + // - Triggered when the swapchain changes size. We use this to resize the + // terminal buffers to match the new visible size. + // Arguments: + // - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel + void TermControl::_SwapChainSizeChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, + SizeChangedEventArgs const& e) + { + if (!_initializedTerminal) + { + return; + } + + auto lock = _terminal->LockForWriting(); + + const auto foundationSize = e.NewSize(); + + _DoResize(foundationSize.Width, foundationSize.Height); + } + + void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, + Windows::Foundation::IInspectable const& /*args*/) + { + const auto scale = sender.CompositionScaleX(); + const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI); + + // TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change? + THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi)); + _renderer->TriggerRedrawAll(); + } + + // Method Description: + // - Process a resize event that was initiated by the user. This can either be due to the user resizing the window (causing the swapchain to resize) or due to the DPI changing (causing us to need to resize the buffer to match) + // Arguments: + // - newWidth: the new width of the swapchain, in pixels. + // - newHeight: the new height of the swapchain, in pixels. + void TermControl::_DoResize(const double newWidth, const double newHeight) + { + SIZE size; + size.cx = static_cast(newWidth); + size.cy = static_cast(newHeight); + + // Tell the dx engine that our window is now the new size. + THROW_IF_FAILED(_renderEngine->SetWindowSize(size)); + + // Invalidate everything + _renderer->TriggerRedrawAll(); + + // Convert our new dimensions to characters + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, + { static_cast(size.cx), static_cast(size.cy) }); + const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels); + + // If this function succeeds with S_FALSE, then the terminal didn't + // actually change size. No need to notify the connection of this + // no-op. + // TODO: MSFT:20642295 Resizing the buffer will corrupt it + // I believe we'll need support for CSI 2J, and additionally I think + // we're resetting the viewport to the top + const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); + if (SUCCEEDED(hr) && hr != S_FALSE) + { + _connection.Resize(vp.Height(), vp.Width()); + } + } + + void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr) + { + _titleChangedHandlers(winrt::hstring{ wstr }); + } + + // Method Description: + // - Update the postion and size of the scrollbar to match the given + // viewport top, viewport height, and buffer size. + // The change will be actually handled in _ScrollbarChangeHandler. + // This should be done on the UI thread. Make sure the caller is calling + // us in a RunAsync block. + // Arguments: + // - viewTop: the top of the visible viewport, in rows. 0 indicates the top + // of the buffer. + // - viewHeight: the height of the viewport in rows. + // - bufferSize: the length of the buffer, in rows + void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar, + const int viewTop, + const int viewHeight, + const int bufferSize) + { + const auto hiddenContent = bufferSize - viewHeight; + scrollBar.Maximum(hiddenContent); + scrollBar.Minimum(0); + scrollBar.ViewportSize(viewHeight); + + scrollBar.Value(viewTop); + } + + // Method Description: + // - Update the postion and size of the scrollbar to match the given + // viewport top, viewport height, and buffer size. + // Additionally fires a ScrollPositionChanged event for anyone who's + // registered an event handler for us. + // Arguments: + // - viewTop: the top of the visible viewport, in rows. 0 indicates the top + // of the buffer. + // - viewHeight: the height of the viewport in rows. + // - bufferSize: the length of the buffer, in rows + void TermControl::_TerminalScrollPositionChanged(const int viewTop, + const int viewHeight, + const int bufferSize) + { + // Update our scrollbar + _scrollBar.Dispatcher().RunAsync(CoreDispatcherPriority::Low, [=]() { + _ScrollbarUpdater(_scrollBar, viewTop, viewHeight, bufferSize); + }); + + // Set this value as our next expected scroll position. + _lastScrollOffset = { viewTop }; + _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); + } + + hstring TermControl::Title() + { + if (!_initializedTerminal) return L""; + + hstring hstr(_terminal->GetConsoleTitle()); + return hstr; + } + + // Method Description: + // - get text from buffer and send it to the Windows Clipboard (CascadiaWin32:main.cpp). Also removes rendering of selection. + // Arguments: + // - trimTrailingWhitespace: enable removing any whitespace from copied selection + // and get text to appear on separate lines. + void TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace) + { + // extract text from buffer + const auto copiedData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace); + + _terminal->ClearSelection(); + _renderer->TriggerSelection(); + + // send data up for clipboard + _clipboardCopyHandlers(copiedData); + } + + void TermControl::Close() + { + if (!_closing) + { + this->~TermControl(); + } + } + + void TermControl::ScrollViewport(int viewTop) + { + _terminal->UserScrollViewport(viewTop); + } + + int TermControl::GetScrollOffset() + { + return _terminal->GetScrollOffset(); + } + + // Function Description: + // - Determines how much space (in pixels) an app would need to reserve to + // create a control with the settings stored in the settings param. This + // accounts for things like the font size and face, the initialRows and + // initialCols, and scrollbar visibility. The returned sized is based upon + // the provided DPI value + // Arguments: + // - settings: A IControlSettings with the settings to get the pixel size of. + // - dpi: The DPI we should create the terminal at. This affects things such + // as font size, scrollbar and other control scaling, etc. Make sure the + // caller knows what monitor the control is about to appear on. + // Return Value: + // - a point containing the requested dimensions in pixels. + winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) + { + // Initialize our font information. + const auto* fontFace = settings.FontFace().c_str(); + const short fontHeight = gsl::narrow(settings.FontSize()); + // The font width doesn't terribly matter, we'll only be using the + // height to look it up + // The other params here also largely don't matter. + // The family is only used to determine if the font is truetype or + // not, but DX doesn't use that info at all. + // The Codepage is additionally not actually used by the DX engine at all. + FontInfo actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; + FontInfoDesired desiredFont = { actualFont }; + + const auto cols = settings.InitialCols(); + const auto rows = settings.InitialRows(); + + // Create a DX engine and initialize it with our font and DPI. We'll + // then use it to measure how much space the requested rows and columns + // will take up. + // TODO: MSFT:21254947 - use a static function to do this instead of + // instantiating a DxEngine + auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); + THROW_IF_FAILED(dxEngine->UpdateDpi(dpi)); + THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont)); + + const float scale = dxEngine->GetScaling(); + const auto fontSize = actualFont.GetSize(); + + // Manually multiply by the scaling factor. The DX engine doesn't + // actually store the scaled font size in the fontInfo.GetSize() + // property when the DX engine is in Composition mode (which it is for + // the Terminal). At runtime, this is fine, as we'll transform + // everything by our scaling, so it'll work out. However, right now we + // need to get the exact pixel count. + const float fFontWidth = gsl::narrow(fontSize.X * scale); + const float fFontHeight = gsl::narrow(fontSize.Y * scale); + + // UWP XAML scrollbars aren't guaranteed to be the same size as the + // ComCtl scrollbars, but it's certainly close enough. + const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); + + float width = gsl::narrow(cols * fFontWidth); + + // Reserve additional space if scrollbar is intended to be visible + if (settings.ScrollState() == ScrollbarState::Visible) + { + width += scrollbarSize; + } + + const float height = gsl::narrow(rows * fFontHeight); + + return { width, height }; + } + + // Method Description: + // - Create XAML Thickness object based on padding props provided. + // Used for controlling the TermControl XAML Grid container's Padding prop. + // Arguments: + // - padding: 2D padding values + // Single Double value provides uniform padding + // Two Double values provide isometric horizontal & vertical padding + // Four Double values provide independent padding for 4 sides of the bounding rectangle + // Return Value: + // - Windows::UI::Xaml::Thickness object + Windows::UI::Xaml::Thickness TermControl::_ParseThicknessFromPadding(const hstring padding) + { + const wchar_t singleCharDelim = L','; + std::wstringstream tokenStream(padding.c_str()); + std::wstring token; + uint8_t paddingPropIndex = 0; + std::array thicknessArr = {}; + size_t* idx = nullptr; + + // Get padding values till we run out of delimiter separated values in the stream + // or we hit max number of allowable values (= 4) for the bounding rectangle + // Non-numeral values detected will default to 0 + // std::getline will not throw exception unless flags are set on the wstringstream + // std::stod will throw invalid_argument expection if the input is an invalid double value + // std::stod will throw out_of_range expection if the input value is more than DBL_MAX + try + { + for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++) + { + // std::stod internall calls wcstod which handles whitespace prefix (which is ignored) + // & stops the scan when first char outside the range of radix is encountered + // We'll be permissive till the extent that stod function allows us to be by default + // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail + thicknessArr[paddingPropIndex] = std::stod(token, idx); + } + } + catch (...) + { + // If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding + paddingPropIndex = 0; + LOG_CAUGHT_EXCEPTION(); + } + + switch (paddingPropIndex) + { + case 1: return ThicknessHelper::FromUniformLength(thicknessArr[0]); + case 2: return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[0], thicknessArr[1]); + // No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom + case 4: return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[2], thicknessArr[3]); + default: return Thickness(); + } + } + + // Method Description: + // - Get the modifier keys that are currently pressed. This can be used to + // find out which modifiers (ctrl, alt, shift) are pressed in events that + // don't necessarily include that state. + // Return Value: + // - a KeyModifiers value with flags set for each key that's pressed. + Settings::KeyModifiers TermControl::_GetPressedModifierKeys() const{ + CoreWindow window = CoreWindow::GetForCurrentThread(); + // DONT USE + // != CoreVirtualKeyStates::None + // OR + // == CoreVirtualKeyStates::Down + // Sometimes with the key down, the state is Down | Locked. + // Sometimes with the key up, the state is Locked. + // IsFlagSet(Down) is the only correct solution. + const auto ctrlKeyState = window.GetKeyState(VirtualKey::Control); + const auto shiftKeyState = window.GetKeyState(VirtualKey::Shift); + const auto altKeyState = window.GetKeyState(VirtualKey::Menu); + + const auto ctrl = WI_IsFlagSet(ctrlKeyState, CoreVirtualKeyStates::Down); + const auto shift = WI_IsFlagSet(shiftKeyState, CoreVirtualKeyStates::Down); + const auto alt = WI_IsFlagSet(altKeyState, CoreVirtualKeyStates::Down); + + return KeyModifiers{ (ctrl ? Settings::KeyModifiers::Ctrl : Settings::KeyModifiers::None) | + (alt ? Settings::KeyModifiers::Alt : Settings::KeyModifiers::None) | + (shift ? Settings::KeyModifiers::Shift : Settings::KeyModifiers::None) }; + } + + // -------------------------------- WinRT Events --------------------------------- + // Winrt events need a method for adding a callback to the event and removing the callback. + // These macros will define them both for you. + DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); + DEFINE_EVENT(TermControl, ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs); + DEFINE_EVENT(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::CopyToClipboardEventArgs); + DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); + + DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); +} diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h new file mode 100644 index 000000000..17d037949 --- /dev/null +++ b/src/cascadia/TerminalControl/TermControl.h @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "TermControl.g.h" +#include "PasteFromClipboardEventArgs.g.h" +#include +#include +#include "../../renderer/base/Renderer.hpp" +#include "../../renderer/dx/DxRenderer.hpp" +#include "../../cascadia/TerminalCore/Terminal.hpp" +#include "../../cascadia/inc/cppwinrt_utils.h" + +namespace winrt::Microsoft::Terminal::TerminalControl::implementation +{ + struct PasteFromClipboardEventArgs : + public PasteFromClipboardEventArgsT + { + public: + PasteFromClipboardEventArgs(std::function clipboardDataHandler) : m_clipboardDataHandler(clipboardDataHandler) { } + + void HandleClipboardData(hstring value) + { + m_clipboardDataHandler(static_cast(value)); + }; + + private: + std::function m_clipboardDataHandler; + }; + + struct TermControl : TermControlT + { + TermControl(); + TermControl(Settings::IControlSettings settings); + + Windows::UI::Xaml::UIElement GetRoot(); + Windows::UI::Xaml::Controls::UserControl GetControl(); + void UpdateSettings(Settings::IControlSettings newSettings); + + hstring Title(); + void CopySelectionToClipboard(bool trimTrailingWhitespace); + void Close(); + + void ScrollViewport(int viewTop); + int GetScrollOffset(); + + void SwapChainChanged(); + ~TermControl(); + + static Windows::Foundation::Point GetProposedDimensions(Microsoft::Terminal::Settings::IControlSettings const& settings, const uint32_t dpi); + + // -------------------------------- WinRT Events --------------------------------- + DECLARE_EVENT(TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); + DECLARE_EVENT(ConnectionClosed, _connectionClosedHandlers, TerminalControl::ConnectionClosedEventArgs); + DECLARE_EVENT(ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); + DECLARE_EVENT(CopyToClipboard, _clipboardCopyHandlers, TerminalControl::CopyToClipboardEventArgs); + + DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); + + private: + TerminalConnection::ITerminalConnection _connection; + bool _initializedTerminal; + + Windows::UI::Xaml::Controls::UserControl _controlRoot; + Windows::UI::Xaml::Controls::Grid _root; + Windows::UI::Xaml::Controls::SwapChainPanel _swapChainPanel; + Windows::UI::Xaml::Controls::Primitives::ScrollBar _scrollBar; + event_token _connectionOutputEventToken; + + ::Microsoft::Terminal::Core::Terminal* _terminal; + + std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer; + std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine; + + Settings::IControlSettings _settings; + bool _closing; + + FontInfoDesired _desiredFont; + FontInfo _actualFont; + + std::optional _lastScrollOffset; + + // storage location for the leading surrogate of a utf-16 surrogate pair + std::optional _leadingSurrogate; + + // If this is set, then we assume we are in the middle of panning the + // viewport via touch input. + std::optional _touchAnchor; + + void _Create(); + void _ApplyUISettings(); + void _ApplyConnectionSettings(); + void _InitializeTerminal(); + void _UpdateFont(); + void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); + void _CharacterHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs const& e); + void _MouseClickHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _MouseMovedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _MouseWheelHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); + void _ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs const& e); + + void _SendInputToConnection(const std::wstring& wstr); + void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e); + void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args); + void _DoResize(const double newWidth, const double newHeight); + void _TerminalTitleChanged(const std::wstring_view& wstr); + void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize); + + void _MouseScrollHandler(const double delta); + void _MouseZoomHandler(const double delta); + void _MouseTransparencyHandler(const double delta); + + void _ScrollbarUpdater(Windows::UI::Xaml::Controls::Primitives::ScrollBar scrollbar, const int viewTop, const int viewHeight, const int bufferSize); + Windows::UI::Xaml::Thickness _ParseThicknessFromPadding(const hstring padding); + + Settings::KeyModifiers _GetPressedModifierKeys() const; + + }; +} + +namespace winrt::Microsoft::Terminal::TerminalControl::factory_implementation +{ + struct TermControl : TermControlT + { + }; +} diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl new file mode 100644 index 000000000..2a5ccfc06 --- /dev/null +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.TerminalControl +{ + delegate void TitleChangedEventArgs(String newTitle); + delegate void ConnectionClosedEventArgs(); + delegate void ScrollPositionChangedEventArgs(Int32 viewTop, Int32 viewHeight, Int32 bufferLength); + delegate void CopyToClipboardEventArgs(String copiedData); + + runtimeclass PasteFromClipboardEventArgs + { + void HandleClipboardData(String data); + } + + [default_interface] + runtimeclass TermControl + { + TermControl(); + TermControl(Microsoft.Terminal.Settings.IControlSettings settings); + + static Windows.Foundation.Point GetProposedDimensions(Microsoft.Terminal.Settings.IControlSettings settings, UInt32 dpi); + + Windows.UI.Xaml.UIElement GetRoot(); + Windows.UI.Xaml.Controls.UserControl GetControl(); + void UpdateSettings(Microsoft.Terminal.Settings.IControlSettings newSettings); + + event TitleChangedEventArgs TitleChanged; + event ConnectionClosedEventArgs ConnectionClosed; + event CopyToClipboardEventArgs CopyToClipboard; + event Windows.Foundation.TypedEventHandler PasteFromClipboard; + + String Title { get; }; + void CopySelectionToClipboard(Boolean trimTrailingWhitespace); + void Close(); + + void ScrollViewport(Int32 viewTop); + Int32 GetScrollOffset(); + event ScrollPositionChangedEventArgs ScrollPositionChanged; + } +} diff --git a/src/cascadia/TerminalControl/TerminalControl.def b/src/cascadia/TerminalControl/TerminalControl.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/src/cascadia/TerminalControl/TerminalControl.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/TerminalControl/TerminalControl.vcxproj b/src/cascadia/TerminalControl/TerminalControl.vcxproj new file mode 100644 index 000000000..60b8538b2 --- /dev/null +++ b/src/cascadia/TerminalControl/TerminalControl.vcxproj @@ -0,0 +1,69 @@ + + + + + + + DynamicLibrary + Console + + true + + + + {CA5CAD1A-44BD-4AC7-AC72-6CA5B3AB89ED} + TerminalControl + Microsoft.Terminal.TerminalControl + + + + + TermControl.idl + + + + + Create + + + TermControl.idl + + + + + + + + + + + + + + + + + + + + + + + + dwrite.lib;dxgi.lib;d2d1.lib;d3d11.lib;shcore.lib;winmm.lib;pathcch.lib;propsys.lib;uiautomationcore.lib;Shlwapi.lib;ntdll.lib;user32.lib;kernel32.lib;%(AdditionalDependencies) + + + $(OpenConsoleDir)src\types\inc;%(AdditionalIncludeDirectories) + + + + + true + + + + diff --git a/src/cascadia/TerminalControl/TerminalControl.vcxproj.filters b/src/cascadia/TerminalControl/TerminalControl.vcxproj.filters new file mode 100644 index 000000000..f2869cf5b --- /dev/null +++ b/src/cascadia/TerminalControl/TerminalControl.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + accd3aa8-1ba0-4223-9bbe-0c431709210b + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {926ab91d-31b4-48c3-b9a4-e681349f27f0} + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/packages.config b/src/cascadia/TerminalControl/packages.config new file mode 100644 index 000000000..4f7a6f98a --- /dev/null +++ b/src/cascadia/TerminalControl/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/TerminalControl/pch.cpp b/src/cascadia/TerminalControl/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/src/cascadia/TerminalControl/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalControl/pch.h b/src/cascadia/TerminalControl/pch.h new file mode 100644 index 000000000..7f48d39a5 --- /dev/null +++ b/src/cascadia/TerminalControl/pch.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + diff --git a/src/cascadia/TerminalCore/ITerminalApi.hpp b/src/cascadia/TerminalCore/ITerminalApi.hpp new file mode 100644 index 000000000..6297d6f3c --- /dev/null +++ b/src/cascadia/TerminalCore/ITerminalApi.hpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +namespace Microsoft::Terminal::Core +{ + class ITerminalApi + { + public: + virtual ~ITerminalApi() {} + virtual bool PrintString(std::wstring_view stringView) = 0; + virtual bool ExecuteChar(wchar_t wch) = 0; + + virtual bool SetTextToDefaults(bool foreground, bool background) = 0; + virtual bool SetTextForegroundIndex(BYTE colorIndex) = 0; + virtual bool SetTextBackgroundIndex(BYTE colorIndex) = 0; + virtual bool SetTextRgbColor(COLORREF color, bool foreground) = 0; + virtual bool BoldText(bool boldOn) = 0; + virtual bool UnderlineText(bool underlineOn) = 0; + virtual bool ReverseText(bool reversed) = 0; + + virtual bool SetCursorPosition(short x, short y) = 0; + virtual COORD GetCursorPosition() = 0; + + virtual bool EraseCharacters(const unsigned int numChars) = 0; + + virtual bool SetWindowTitle(std::wstring_view title) = 0; + + virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) = 0; + }; +} diff --git a/src/cascadia/TerminalCore/ITerminalInput.hpp b/src/cascadia/TerminalCore/ITerminalInput.hpp new file mode 100644 index 000000000..9d4a41cb9 --- /dev/null +++ b/src/cascadia/TerminalCore/ITerminalInput.hpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +namespace Microsoft::Terminal::Core +{ + class ITerminalInput + { + public: + virtual ~ITerminalInput() {} + + virtual bool SendKeyEvent(const WORD vkey, const bool ctrlPressed, const bool altPressed, const bool shiftPressed) = 0; + + // void SendMouseEvent(uint row, uint col, KeyModifiers modifiers); + [[nodiscard]] + virtual HRESULT UserResize(const COORD size) noexcept = 0; + virtual void UserScrollViewport(const int viewTop) = 0; + virtual int GetScrollOffset() = 0; + + }; +} diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp new file mode 100644 index 000000000..9ad7e0e8e --- /dev/null +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -0,0 +1,590 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Terminal.hpp" +#include "../../terminal/parser/OutputStateMachineEngine.hpp" +#include "TerminalDispatch.hpp" +#include "../../inc/unicode.hpp" +#include "../../inc/DefaultSettings.h" +#include "../../inc/argb.h" +#include "../../types/inc/utils.hpp" + +#include "winrt/Microsoft.Terminal.Settings.h" + +using namespace winrt::Microsoft::Terminal::Settings; +using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::VirtualTerminal; + +std::wstring _KeyEventsToText(std::deque>& inEventsToWrite) +{ + std::wstring wstr = L""; + for(auto& ev : inEventsToWrite) + { + if (ev->EventType() == InputEventType::KeyEvent) + { + auto& k = static_cast(*ev); + auto wch = k.GetCharData(); + wstr += wch; + } + } + return wstr; +} + +Terminal::Terminal() : + _mutableViewport{Viewport::Empty()}, + _title{ L"" }, + _colorTable{}, + _defaultFg{ RGB(255, 255, 255) }, + _defaultBg{ ARGB(0, 0, 0, 0) }, + _pfnWriteInput{ nullptr }, + _scrollOffset{ 0 }, + _snapOnInput{ true }, + _boxSelection{ false }, + _selectionActive{ false }, + _selectionAnchor{ 0, 0 }, + _endSelectionPosition { 0, 0 } +{ + _stateMachine = std::make_unique(new OutputStateMachineEngine(new TerminalDispatch(*this))); + + auto passAlongInput = [&](std::deque>& inEventsToWrite) + { + if(!_pfnWriteInput) return; + std::wstring wstr = _KeyEventsToText(inEventsToWrite); + _pfnWriteInput(wstr); + }; + + _terminalInput = std::make_unique(passAlongInput); + + _InitializeColorTable(); +} + +void Terminal::Create(COORD viewportSize, SHORT scrollbackLines, IRenderTarget& renderTarget) +{ + _mutableViewport = Viewport::FromDimensions({ 0,0 }, viewportSize); + _scrollbackLines = scrollbackLines; + COORD bufferSize { viewportSize.X, viewportSize.Y + scrollbackLines }; + TextAttribute attr{}; + UINT cursorSize = 12; + _buffer = std::make_unique(bufferSize, attr, cursorSize, renderTarget); +} + +// Method Description: +// - Initializes the Temrinal from the given set of settings. +// Arguments: +// - settings: the set of CoreSettings we need to use to initialize the terminal +// - renderTarget: A render target the terminal can use for paint invalidation. +void Terminal::CreateFromSettings(winrt::Microsoft::Terminal::Settings::ICoreSettings settings, + Microsoft::Console::Render::IRenderTarget& renderTarget) +{ + const COORD viewportSize{ static_cast(settings.InitialCols()), static_cast(settings.InitialRows()) }; + // TODO:MSFT:20642297 - Support infinite scrollback here, if HistorySize is -1 + Create(viewportSize, static_cast(settings.HistorySize()), renderTarget); + + UpdateSettings(settings); +} + +// Method Description: +// - Update our internal properties to match the new values in the provided +// CoreSettings object. +// Arguments: +// - settings: an ICoreSettings with new settings values for us to use. +void Terminal::UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSettings settings) +{ + _defaultFg = settings.DefaultForeground(); + _defaultBg = settings.DefaultBackground(); + + CursorType cursorShape = CursorType::VerticalBar; + switch (settings.CursorShape()) + { + case CursorStyle::Underscore: + cursorShape = CursorType::Underscore; + break; + case CursorStyle::FilledBox: + cursorShape = CursorType::FullBox; + break; + case CursorStyle::EmptyBox: + cursorShape = CursorType::EmptyBox; + break; + case CursorStyle::Vintage: + cursorShape = CursorType::Legacy; + break; + default: + case CursorStyle::Bar: + cursorShape = CursorType::VerticalBar; + break; + } + + _buffer->GetCursor().SetStyle(settings.CursorHeight(), + settings.CursorColor(), + cursorShape); + + for (int i = 0; i < 16; i++) + { + _colorTable[i] = settings.GetColorTableEntry(i); + } + + _snapOnInput = settings.SnapOnInput(); + + // TODO:MSFT:21327402 - if HistorySize has changed, resize the buffer so we + // have a smaller scrollback. We should do this carefully - if the new buffer + // size is smaller than where the mutable viewport currently is, we'll want + // to make sure to rotate the buffer contents upwards, so the mutable viewport + // remains at the bottom of the buffer. +} + +// Method Description: +// - Resize the terminal as the result of some user interaction. +// Arguments: +// - viewportSize: the new size of the viewport, in chars +// Return Value: +// - S_OK if we successfully resized the terminal, S_FALSE if there was +// nothing to do (the viewportSize is the same as our current size), or an +// appropriate HRESULT for failing to resize. +[[nodiscard]] +HRESULT Terminal::UserResize(const COORD viewportSize) noexcept +{ + const auto oldDimensions = _mutableViewport.Dimensions(); + if (viewportSize == oldDimensions) + { + return S_FALSE; + } + + const auto oldTop = _mutableViewport.Top(); + + const short newBufferHeight = viewportSize.Y + _scrollbackLines; + COORD bufferSize{ viewportSize.X, newBufferHeight }; + RETURN_IF_FAILED(_buffer->ResizeTraditional(bufferSize)); + + auto proposedTop = oldTop; + const auto newView = Viewport::FromDimensions({ 0, proposedTop }, viewportSize); + const auto proposedBottom = newView.BottomExclusive(); + // If the new bottom would be below the bottom of the buffer, then slide the + // top up so that we'll still fit within the buffer. + if (proposedBottom > bufferSize.Y) + { + proposedTop -= (proposedBottom - bufferSize.Y); + } + + _mutableViewport = Viewport::FromDimensions({ 0, proposedTop }, viewportSize); + _scrollOffset = 0; + _NotifyScrollEvent(); + + return S_OK; +} + +void Terminal::Write(std::wstring_view stringView) +{ + auto lock = LockForWriting(); + + _stateMachine->ProcessString(stringView.data(), stringView.size()); +} + +// Method Description: +// - Send this particular key event to the terminal. The terminal will translate +// the key and the modifiers pressed into the appropriate VT sequence for that +// key chord. If we do translate the key, we'll return true. In that case, the +// event should NOT br processed any further. If we return false, the event +// was NOT translated, and we should instead use the event to try and get the +// real character out of the event. +// Arguments: +// - vkey: The vkey of the key pressed. +// - ctrlPressed: true iff either ctrl key is pressed. +// - altPressed: true iff either alt key is pressed. +// - shiftPressed: true iff either shift key is pressed. +// Return Value: +// - true if we translated the key event, and it should not be processed any further. +// - false if we did not translate the key, and it should be processed into a character. +bool Terminal::SendKeyEvent(const WORD vkey, + const bool ctrlPressed, + const bool altPressed, + const bool shiftPressed) +{ + if (_snapOnInput && _scrollOffset != 0) + { + auto lock = LockForWriting(); + _scrollOffset = 0; + _NotifyScrollEvent(); + } + + DWORD modifiers = 0 + | (ctrlPressed? LEFT_CTRL_PRESSED : 0) + | (altPressed? LEFT_ALT_PRESSED : 0) + | (shiftPressed? SHIFT_PRESSED : 0) + ; + + // Alt key sequences _require_ the char to be in the keyevent. If alt is + // pressed, manually get the character that's being typed, and put it in the + // KeyEvent. + // DON'T manually handle Alt+Space - the system will use this to bring up + // the system menu for restore, min/maximimize, size, move, close + wchar_t ch = altPressed && vkey != VK_SPACE ? static_cast(LOWORD(MapVirtualKey(vkey, MAPVK_VK_TO_CHAR))) : UNICODE_NULL; + + // Manually handle Ctrl+H. Ctrl+H should be handled as Backspace. To do this + // correctly, the keyEvents's char needs to be set to Backspace. + // 0x48 is the VKEY for 'H', which isn't named + if (ctrlPressed && vkey == 0x48) + { + ch = UNICODE_BACKSPACE; + } + // Manually handle Ctrl+Space here. The terminalInput translator requires + // the char to be set to Space for space handling to work correctly. + if (ctrlPressed && vkey == VK_SPACE) + { + ch = UNICODE_SPACE; + } + + const bool manuallyHandled = ch != UNICODE_NULL; + + KeyEvent keyEv{ true, 0, vkey, 0, ch, modifiers}; + const bool translated = _terminalInput->HandleKey(&keyEv); + + return translated && manuallyHandled; +} + +// Method Description: +// - Aquire a read lock on the terminal. +// Return Value: +// - a shared_lock which can be used to unlock the terminal. The shared_lock +// will release this lock when it's destructed. +[[nodiscard]] +std::shared_lock Terminal::LockForReading() +{ + return std::shared_lock(_readWriteLock); +} + +// Method Description: +// - Aquire a write lock on the terminal. +// Return Value: +// - a unique_lock which can be used to unlock the terminal. The unique_lock +// will release this lock when it's destructed. +[[nodiscard]] +std::unique_lock Terminal::LockForWriting() +{ + return std::unique_lock(_readWriteLock); +} + + +Viewport Terminal::_GetMutableViewport() const noexcept +{ + return _mutableViewport; +} + +short Terminal::GetBufferHeight() const noexcept +{ + return _mutableViewport.BottomExclusive(); +} + +// _ViewStartIndex is also the length of the scrollback +int Terminal::_ViewStartIndex() const noexcept +{ + return _mutableViewport.Top(); +} + +// _VisibleStartIndex is the first visible line of the buffer +int Terminal::_VisibleStartIndex() const noexcept +{ + return std::max(0, _ViewStartIndex() - _scrollOffset); +} + +Viewport Terminal::_GetVisibleViewport() const noexcept +{ + const COORD origin{ 0, gsl::narrow(_VisibleStartIndex()) }; + return Viewport::FromDimensions(origin, + _mutableViewport.Dimensions()); +} + +// Writes a string of text to the buffer, then moves the cursor (and viewport) +// in accordance with the written text. +// This method is our proverbial `WriteCharsLegacy`, and great care should be made to +// keep it minimal and orderly, lest it become WriteCharsLegacy2ElectricBoogaloo +// TODO: MSFT 21006766 +// This needs to become stream logic on the buffer itself sooner rather than later +// because it's otherwise impossible to avoid the Electric Boogaloo-ness here. +// I had to make a bunch of hacks to get Japanese and emoji to work-ish. +void Terminal::_WriteBuffer(const std::wstring_view& stringView) +{ + auto& cursor = _buffer->GetCursor(); + const Viewport bufferSize = _buffer->GetSize(); + + for (size_t i = 0; i < stringView.size(); i++) + { + wchar_t wch = stringView[i]; + const COORD cursorPosBefore = cursor.GetPosition(); + COORD proposedCursorPosition = cursorPosBefore; + bool notifyScroll = false; + + if (wch == UNICODE_LINEFEED) + { + proposedCursorPosition.Y++; + } + else if (wch == UNICODE_CARRIAGERETURN) + { + proposedCursorPosition.X = 0; + } + else if (wch == UNICODE_BACKSPACE) + { + if (cursorPosBefore.X == 0) + { + proposedCursorPosition.X = bufferSize.Width() - 1; + proposedCursorPosition.Y--; + } + else + { + proposedCursorPosition.X--; + } + } + else + { + // TODO: MSFT 21006766 + // This is not great but I need it demoable. Fix by making a buffer stream writer. + if (wch >= 0xD800 && wch <= 0xDFFF) + { + OutputCellIterator it{ stringView.substr(i, 2) , _buffer->GetCurrentAttributes() }; + const auto end = _buffer->Write(it); + const auto cellDistance = end.GetCellDistance(it); + i += cellDistance - 1; + proposedCursorPosition.X += gsl::narrow(cellDistance); + } + else + { + OutputCellIterator it{ stringView.substr(i, 1) , _buffer->GetCurrentAttributes() }; + const auto end = _buffer->Write(it); + const auto cellDistance = end.GetCellDistance(it); + proposedCursorPosition.X += gsl::narrow(cellDistance); + } + } + + // If we're about to scroll past the bottom of the buffer, instead cycle the buffer. + const auto newRows = proposedCursorPosition.Y - bufferSize.Height() + 1; + if (newRows > 0) + { + for(auto dy = 0; dy < newRows; dy++) + { + _buffer->IncrementCircularBuffer(); + proposedCursorPosition.Y--; + } + notifyScroll = true; + } + + // This section is essentially equivalent to `AdjustCursorPosition` + // Update Cursor Position + cursor.SetPosition(proposedCursorPosition); + + const COORD cursorPosAfter = cursor.GetPosition(); + + // Move the viewport down if the cursor moved below the viewport. + if (cursorPosAfter.Y > _mutableViewport.BottomInclusive()) + { + const auto newViewTop = std::max(0, cursorPosAfter.Y - (_mutableViewport.Height() - 1)); + if (newViewTop != _mutableViewport.Top()) + { + _mutableViewport = Viewport::FromDimensions({0, gsl::narrow(newViewTop)}, _mutableViewport.Dimensions()); + notifyScroll = true; + } + } + + if (notifyScroll) + { + _buffer->GetRenderTarget().TriggerRedrawAll(); + _NotifyScrollEvent(); + } + } +} + +void Terminal::UserScrollViewport(const int viewTop) +{ + const auto clampedNewTop = std::max(0, viewTop); + const auto realTop = _ViewStartIndex(); + const auto newDelta = realTop - clampedNewTop; + // if viewTop > realTop, we want the offset to be 0. + + _scrollOffset = std::max(0, newDelta); + _buffer->GetRenderTarget().TriggerRedrawAll(); +} + +int Terminal::GetScrollOffset() +{ + return _VisibleStartIndex(); +} + +void Terminal::_NotifyScrollEvent() +{ + if (_pfnScrollPositionChanged) + { + const auto visible = _GetVisibleViewport(); + const auto top = visible.Top(); + const auto height = visible.Height(); + const auto bottom = this->GetBufferHeight(); + _pfnScrollPositionChanged(top, height, bottom); + } +} + +void Terminal::SetWriteInputCallback(std::function pfn) noexcept +{ + _pfnWriteInput = pfn; +} + +void Terminal::SetTitleChangedCallback(std::function pfn) noexcept +{ + _pfnTitleChanged = pfn; +} + +void Terminal::SetScrollPositionChangedCallback(std::function pfn) noexcept +{ + _pfnScrollPositionChanged = pfn; +} + +// Method Description: +// - Checks if selection is active +// Return Value: +// - bool representing if selection is active. Used to decide copy/paste on right click +const bool Terminal::IsSelectionActive() const noexcept +{ + return _selectionActive; +} + +// Method Description: +// - Record the position of the beginning of a selection +// Arguments: +// - position: the (x,y) coordinate on the visible viewport +void Terminal::SetSelectionAnchor(const COORD position) +{ + _selectionAnchor = position; + + // include _scrollOffset here to ensure this maps to the right spot of the original viewport + THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow(_scrollOffset), &_selectionAnchor.Y)); + + // copy value of ViewStartIndex to support scrolling + // and update on new buffer output (used in _GetSelectionRects()) + _selectionAnchor_YOffset = gsl::narrow(_ViewStartIndex()); + + _selectionActive = true; + SetEndSelectionPosition(position); +} + +// Method Description: +// - Record the position of the end of a selection +// Arguments: +// - position: the (x,y) coordinate on the visible viewport +void Terminal::SetEndSelectionPosition(const COORD position) +{ + _endSelectionPosition = position; + + // include _scrollOffset here to ensure this maps to the right spot of the original viewport + THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow(_scrollOffset), &_endSelectionPosition.Y)); + + // copy value of ViewStartIndex to support scrolling + // and update on new buffer output (used in _GetSelectionRects()) + _endSelectionPosition_YOffset = gsl::narrow(_ViewStartIndex()); +} + +void Terminal::_InitializeColorTable() +{ + gsl::span tableView = { &_colorTable[0], gsl::narrow(_colorTable.size()) }; + // First set up the basic 256 colors + ::Microsoft::Console::Utils::Initialize256ColorTable(tableView); + // Then use fill the first 16 values with the Campbell scheme + ::Microsoft::Console::Utils::InitializeCampbellColorTable(tableView); + // Then make sure all the values have an alpha of 255 + ::Microsoft::Console::Utils::SetColorTableAlpha(tableView, 0xff); +} + +// Method Description: +// - Helper to determine the selected region of the buffer. Used for rendering. +// Return Value: +// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin. +std::vector Terminal::_GetSelectionRects() const +{ + std::vector selectionArea; + + if (!_selectionActive) + { + return selectionArea; + } + + // Add anchor offset here to update properly on new buffer output + SHORT temp1, temp2; + THROW_IF_FAILED(ShortAdd(_selectionAnchor.Y, _selectionAnchor_YOffset, &temp1)); + THROW_IF_FAILED(ShortAdd(_endSelectionPosition.Y, _endSelectionPosition_YOffset, &temp2)); + + // create these new anchors for comparison and rendering + const COORD selectionAnchorWithOffset = { _selectionAnchor.X, temp1 }; + const COORD endSelectionPositionWithOffset = { _endSelectionPosition.X, temp2 }; + + // NOTE: (0,0) is top-left so vertical comparison is inverted + const COORD &higherCoord = (selectionAnchorWithOffset.Y <= endSelectionPositionWithOffset.Y) ? selectionAnchorWithOffset : endSelectionPositionWithOffset; + const COORD &lowerCoord = (selectionAnchorWithOffset.Y > endSelectionPositionWithOffset.Y) ? selectionAnchorWithOffset : endSelectionPositionWithOffset; + + selectionArea.reserve(lowerCoord.Y - higherCoord.Y + 1); + for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++) + { + SMALL_RECT selectionRow; + + selectionRow.Top = row; + selectionRow.Bottom = row; + + if (_boxSelection || higherCoord.Y == lowerCoord.Y) + { + selectionRow.Left = std::min(higherCoord.X, lowerCoord.X); + selectionRow.Right = std::max(higherCoord.X, lowerCoord.X); + } + else + { + selectionRow.Left = (row == higherCoord.Y) ? higherCoord.X : 0; + selectionRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : _buffer->GetSize().RightInclusive(); + } + + selectionArea.emplace_back(selectionRow); + } + return selectionArea; +} + +// Method Description: +// - enable/disable box selection (ALT + selection) +// Arguments: +// - isEnabled: new value for _boxSelection +void Terminal::SetBoxSelection(const bool isEnabled) noexcept +{ + _boxSelection = isEnabled; +} + +// Method Description: +// - clear selection data and disable rendering it +void Terminal::ClearSelection() noexcept +{ + _selectionActive = false; + _selectionAnchor = {0, 0}; + _endSelectionPosition = {0, 0}; + _selectionAnchor_YOffset = 0; + _endSelectionPosition_YOffset = 0; +} + +// Method Description: +// - get wstring text from highlighted portion of text buffer +// Arguments: +// - trimTrailingWhitespace: enable removing any whitespace from copied selection +// and get text to appear on separate lines. +// Return Value: +// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n +const std::wstring Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const +{ + std::function GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1); + std::function GetBackgroundColor = std::bind(&Terminal::GetBackgroundColor, this, std::placeholders::_1); + + auto data = _buffer->GetTextForClipboard(!_boxSelection, + trimTrailingWhitespace, + _GetSelectionRects(), + GetForegroundColor, + GetBackgroundColor); + + std::wstring result; + for (const auto& text : data.text) + { + result += text; + } + + return result; +} diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp new file mode 100644 index 000000000..37bc6cb45 --- /dev/null +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "../../buffer/out/textBuffer.hpp" +#include "../../renderer/inc/IRenderData.hpp" +#include "../../terminal/parser/StateMachine.hpp" +#include "../../terminal/input/terminalInput.hpp" + +#include "../../types/inc/Viewport.hpp" +#include "../../cascadia/terminalcore/ITerminalApi.hpp" +#include "../../cascadia/terminalcore/ITerminalInput.hpp" + +// You have to forward decl the ICoreSettings here, instead of including the header. +// If you include the header, there will be compilation errors with other +// headers that include Terminal.hpp +namespace winrt::Microsoft::Terminal::Settings +{ + struct ICoreSettings; +} + +namespace Microsoft::Terminal::Core +{ + class Terminal; +} + +class Microsoft::Terminal::Core::Terminal final : + public Microsoft::Terminal::Core::ITerminalApi, + public Microsoft::Terminal::Core::ITerminalInput, + public Microsoft::Console::Render::IRenderData +{ +public: + Terminal(); + virtual ~Terminal() {}; + + void Create(COORD viewportSize, + SHORT scrollbackLines, + Microsoft::Console::Render::IRenderTarget& renderTarget); + + void CreateFromSettings(winrt::Microsoft::Terminal::Settings::ICoreSettings settings, + Microsoft::Console::Render::IRenderTarget& renderTarget); + + void UpdateSettings(winrt::Microsoft::Terminal::Settings::ICoreSettings settings); + + // Write goes through the parser + void Write(std::wstring_view stringView); + + [[nodiscard]] + std::shared_lock LockForReading(); + [[nodiscard]] + std::unique_lock LockForWriting(); + + short GetBufferHeight() const noexcept; + + #pragma region ITerminalApi + // These methods are defined in TerminalApi.cpp + bool PrintString(std::wstring_view stringView) override; + bool ExecuteChar(wchar_t wch) override; + bool SetTextToDefaults(bool foreground, bool background) override; + bool SetTextForegroundIndex(BYTE colorIndex) override; + bool SetTextBackgroundIndex(BYTE colorIndex) override; + bool SetTextRgbColor(COLORREF color, bool foreground) override; + bool BoldText(bool boldOn) override; + bool UnderlineText(bool underlineOn) override; + bool ReverseText(bool reversed) override; + bool SetCursorPosition(short x, short y) override; + COORD GetCursorPosition() override; + bool EraseCharacters(const unsigned int numChars) override; + bool SetWindowTitle(std::wstring_view title) override; + bool SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) override; + #pragma endregion + + #pragma region ITerminalInput + // These methods are defined in Terminal.cpp + bool SendKeyEvent(const WORD vkey, + const bool ctrlPressed, + const bool altPressed, + const bool shiftPressed) override; + [[nodiscard]] + HRESULT UserResize(const COORD viewportSize) noexcept override; + void UserScrollViewport(const int viewTop) override; + int GetScrollOffset() override; + #pragma endregion + + #pragma region IRenderData + // These methods are defined in TerminalRenderData.cpp + Microsoft::Console::Types::Viewport GetViewport() noexcept override; + const TextBuffer& GetTextBuffer() noexcept override; + const FontInfo& GetFontInfo() noexcept override; + const TextAttribute GetDefaultBrushColors() noexcept override; + const COLORREF GetForegroundColor(const TextAttribute& attr) const noexcept override; + const COLORREF GetBackgroundColor(const TextAttribute& attr) const noexcept override; + COORD GetCursorPosition() const noexcept override; + bool IsCursorVisible() const noexcept override; + bool IsCursorOn() const noexcept override; + ULONG GetCursorHeight() const noexcept override; + ULONG GetCursorPixelWidth() const noexcept override; + CursorType GetCursorStyle() const noexcept override; + COLORREF GetCursorColor() const noexcept override; + bool IsCursorDoubleWidth() const noexcept override; + const std::vector GetOverlays() const noexcept override; + const bool IsGridLineDrawingAllowed() noexcept override; + std::vector GetSelectionRects() noexcept override; + const std::wstring GetConsoleTitle() const noexcept override; + void LockConsole() noexcept override; + void UnlockConsole() noexcept override; + #pragma endregion + + void SetWriteInputCallback(std::function pfn) noexcept; + void SetTitleChangedCallback(std::function pfn) noexcept; + void SetScrollPositionChangedCallback(std::function pfn) noexcept; + + #pragma region TextSelection + const bool IsSelectionActive() const noexcept; + void SetSelectionAnchor(const COORD position); + void SetEndSelectionPosition(const COORD position); + void SetBoxSelection(const bool isEnabled) noexcept; + void ClearSelection() noexcept; + + const std::wstring RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const; + #pragma endregion + + private: + std::function _pfnWriteInput; + std::function _pfnTitleChanged; + std::function _pfnScrollPositionChanged; + + std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine; + std::unique_ptr<::Microsoft::Console::VirtualTerminal::TerminalInput> _terminalInput; + + std::wstring _title; + + std::array _colorTable; + COLORREF _defaultFg; + COLORREF _defaultBg; + + bool _snapOnInput; + + // Text Selection + COORD _selectionAnchor; + COORD _endSelectionPosition; + bool _boxSelection; + bool _selectionActive; + SHORT _selectionAnchor_YOffset; + SHORT _endSelectionPosition_YOffset; + + std::shared_mutex _readWriteLock; + + // TODO: These members are not shared by an alt-buffer. They should be + // encapsulated, such that a Terminal can have both a main and alt buffer. + std::unique_ptr _buffer; + Microsoft::Console::Types::Viewport _mutableViewport; + SHORT _scrollbackLines; + + // _scrollOffset is the number of lines above the viewport that are currently visible + // If _scrollOffset is 0, then the visible region of the buffer is the viewport. + int _scrollOffset; + // TODO this might not be the value we want to store. + // We might want to store the height in the scrollback that's currenty visible. + // Think on this some more. + // For example: While looking at the scrollback, we probably want the visible region to "stick" + // to the region they scrolled to. If that were the case, then every time we move _mutableViewport, + // we'd also need to update _offset. + // However, if we just stored it as a _visibleTop, then that point would remain fixed - + // Though if _visibleTop == _mutableViewport.Top, then we'd need to make sure to update + // _visibleTop as well. + // Additionally, maybe some people want to scroll into the history, then have that scroll out from + // underneath them, while others would prefer to anchor it in place. + // Either way, we sohould make this behavior controlled by a setting. + + int _ViewStartIndex() const noexcept; + int _VisibleStartIndex() const noexcept; + + Microsoft::Console::Types::Viewport _GetMutableViewport() const noexcept; + Microsoft::Console::Types::Viewport _GetVisibleViewport() const noexcept; + + void _InitializeColorTable(); + + void _WriteBuffer(const std::wstring_view& stringView); + + void _NotifyScrollEvent(); + + std::vector _GetSelectionRects() const; +}; + diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp new file mode 100644 index 000000000..5c6555183 --- /dev/null +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Terminal.hpp" + +using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::VirtualTerminal; + +// Print puts the text in the buffer and moves the cursor +bool Terminal::PrintString(std::wstring_view stringView) +{ + _WriteBuffer(stringView); + return true; +} + +bool Terminal::ExecuteChar(wchar_t wch) +{ + std::wstring_view view{&wch, 1}; + _WriteBuffer(view); + return true; +} + +bool Terminal::SetTextToDefaults(bool foreground, bool background) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + if (foreground) + { + attrs.SetDefaultForeground(); + } + if (background) + { + attrs.SetDefaultBackground(); + } + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::SetTextForegroundIndex(BYTE colorIndex) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + attrs.SetIndexedAttributes({ colorIndex }, {}); + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::SetTextBackgroundIndex(BYTE colorIndex) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + attrs.SetIndexedAttributes({}, { colorIndex }); + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::SetTextRgbColor(COLORREF color, bool foreground) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + attrs.SetColor(color, foreground); + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::BoldText(bool boldOn) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + if (boldOn) + { + attrs.Embolden(); + } + else + { + attrs.Debolden(); + } + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::UnderlineText(bool underlineOn) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + WORD metaAttrs = attrs.GetMetaAttributes(); + + WI_UpdateFlag(metaAttrs, COMMON_LVB_UNDERSCORE, underlineOn); + + attrs.SetMetaAttributes(metaAttrs); + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::ReverseText(bool reversed) +{ + TextAttribute attrs = _buffer->GetCurrentAttributes(); + WORD metaAttrs = attrs.GetMetaAttributes(); + + WI_UpdateFlag(metaAttrs, COMMON_LVB_REVERSE_VIDEO, reversed); + + attrs.SetMetaAttributes(metaAttrs); + _buffer->SetCurrentAttributes(attrs); + return true; +} + +bool Terminal::SetCursorPosition(short x, short y) +{ + const auto viewport = _GetMutableViewport(); + const auto viewOrigin = viewport.Origin(); + const short absoluteX = viewOrigin.X + x; + const short absoluteY = viewOrigin.Y + y; + COORD newPos{absoluteX, absoluteY}; + viewport.Clamp(newPos); + _buffer->GetCursor().SetPosition(newPos); + + return true; +} + +COORD Terminal::GetCursorPosition() +{ + const auto absoluteCursorPos = _buffer->GetCursor().GetPosition(); + const auto viewport = _GetMutableViewport(); + const auto viewOrigin = viewport.Origin(); + const short relativeX = absoluteCursorPos.X - viewOrigin.X; + const short relativeY = absoluteCursorPos.Y - viewOrigin.Y; + COORD newPos{ relativeX, relativeY }; + + // TODO assert that the coord is > (0, 0) && <(view.W, view.H) + return newPos; +} + +bool Terminal::EraseCharacters(const unsigned int numChars) +{ + const auto absoluteCursorPos = _buffer->GetCursor().GetPosition(); + const auto viewport = _GetMutableViewport(); + const short distanceToRight = viewport.RightExclusive() - absoluteCursorPos.X; + const short fillLimit = std::min(static_cast(numChars), distanceToRight); + auto eraseIter = OutputCellIterator(L' ', _buffer->GetCurrentAttributes(), fillLimit); + _buffer->Write(eraseIter, absoluteCursorPos); + return true; +} + +bool Terminal::SetWindowTitle(std::wstring_view title) +{ + _title = title; + + if (_pfnTitleChanged) + { + _pfnTitleChanged(title); + } + + return true; +} + +// Method Description: +// - Updates the value in the colortable at index tableIndex to the new color +// dwColor. dwColor is a COLORREF, format 0x00BBGGRR. +// Arguments: +// - tableIndex: the index of the color table to update. +// - dwColor: the new COLORREF to use as that color table value. +// Return Value: +// - true iff we successfully updated the color table entry. +bool Terminal::SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) +{ + if (tableIndex > _colorTable.size()) + { + return false; + } + _colorTable.at(tableIndex) = dwColor; + + // Repaint everything - the colors might have changed + _buffer->GetRenderTarget().TriggerRedrawAll(); + return true; +} diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp new file mode 100644 index 000000000..05b8fc119 --- /dev/null +++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalDispatch.hpp" +using namespace ::Microsoft::Terminal::Core; +using namespace ::Microsoft::Console::VirtualTerminal; + +// NOTE: +// Functions related to Set Graphics Renditions (SGR) are in +// TerminalDispatchGraphics.cpp, not this file + +TerminalDispatch::TerminalDispatch(ITerminalApi& terminalApi) : + _terminalApi{ terminalApi } +{ + +} + +void TerminalDispatch::Execute(const wchar_t wchControl) +{ + _terminalApi.ExecuteChar(wchControl); +} + +void TerminalDispatch::Print(const wchar_t wchPrintable) +{ + _terminalApi.PrintString({ &wchPrintable, 1 }); +} + +void TerminalDispatch::PrintString(const wchar_t *const rgwch, const size_t cch) +{ + _terminalApi.PrintString({ rgwch, cch }); +} + +bool TerminalDispatch::CursorPosition(const unsigned int uiLine, + const unsigned int uiColumn) +{ + const auto columnInBufferSpace = uiColumn - 1; + const auto lineInBufferSpace = uiLine - 1; + short x = static_cast(uiColumn - 1); + short y = static_cast(uiLine - 1); + return _terminalApi.SetCursorPosition(x, y); +} + +bool TerminalDispatch::CursorForward(const unsigned int uiDistance) +{ + const auto cursorPos = _terminalApi.GetCursorPosition(); + const COORD newCursorPos { cursorPos.X + gsl::narrow(uiDistance), cursorPos.Y }; + return _terminalApi.SetCursorPosition(newCursorPos.X, newCursorPos.Y); +} + +bool TerminalDispatch::EraseCharacters(const unsigned int uiNumChars) +{ + return _terminalApi.EraseCharacters(uiNumChars); +} + +bool TerminalDispatch::SetWindowTitle(std::wstring_view title) +{ + return _terminalApi.SetWindowTitle(title); +} + +// Method Description: +// - Sets a single entry of the colortable to a new value +// Arguments: +// - tableIndex: The VT color table index +// - dwColor: The new RGB color value to use. +// Return Value: +// True if handled successfully. False othewise. +bool TerminalDispatch::SetColorTableEntry(const size_t tableIndex, + const DWORD dwColor) +{ + return _terminalApi.SetColorTableEntry(tableIndex, dwColor); +} diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp new file mode 100644 index 000000000..4a35ff8f4 --- /dev/null +++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "../../terminal/adapter/termDispatch.hpp" +#include "ITerminalApi.hpp" + +class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatch +{ +public: + TerminalDispatch(::Microsoft::Terminal::Core::ITerminalApi& terminalApi); + virtual ~TerminalDispatch(){}; + virtual void Execute(const wchar_t wchControl) override; + virtual void Print(const wchar_t wchPrintable) override; + virtual void PrintString(const wchar_t *const rgwch, const size_t cch) override; + + bool SetGraphicsRendition(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions) override; + + virtual bool CursorPosition(const unsigned int uiLine, + const unsigned int uiColumn) override; // CUP + + bool CursorForward(const unsigned int uiDistance) override; + + bool EraseCharacters(const unsigned int uiNumChars) override; + bool SetWindowTitle(std::wstring_view title) override; + + bool SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) override; + +private: + ::Microsoft::Terminal::Core::ITerminalApi& _terminalApi; + + static bool s_IsRgbColorOption(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions opt) noexcept; + static bool s_IsBoldColorOption(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions opt) noexcept; + static bool s_IsDefaultColorOption(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions opt) noexcept; + + bool _SetRgbColorsHelper(_In_reads_(cOptions) const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions, + _Out_ size_t* const pcOptionsConsumed); + bool _SetBoldColorHelper(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions option); + bool _SetDefaultColorHelper(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions option); + void _SetGraphicsOptionHelper(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::GraphicsOptions opt); + +}; diff --git a/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp new file mode 100644 index 000000000..55c004e33 --- /dev/null +++ b/src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalDispatch.hpp" +using namespace ::Microsoft::Terminal::Core; +using namespace ::Microsoft::Console::VirtualTerminal; + +const BYTE RED_ATTR = 0x01; +const BYTE GREEN_ATTR = 0x02; +const BYTE BLUE_ATTR = 0x04; +const BYTE BRIGHT_ATTR = 0x08; +const BYTE DARK_BLACK = 0; +const BYTE DARK_RED = RED_ATTR; +const BYTE DARK_GREEN = GREEN_ATTR; +const BYTE DARK_YELLOW = RED_ATTR | GREEN_ATTR; +const BYTE DARK_BLUE = BLUE_ATTR; +const BYTE DARK_MAGENTA = RED_ATTR | BLUE_ATTR; +const BYTE DARK_CYAN = GREEN_ATTR | BLUE_ATTR; +const BYTE DARK_WHITE = RED_ATTR | GREEN_ATTR | BLUE_ATTR; +const BYTE BRIGHT_BLACK = BRIGHT_ATTR; +const BYTE BRIGHT_RED = BRIGHT_ATTR | RED_ATTR; +const BYTE BRIGHT_GREEN = BRIGHT_ATTR | GREEN_ATTR; +const BYTE BRIGHT_YELLOW = BRIGHT_ATTR | RED_ATTR | GREEN_ATTR; +const BYTE BRIGHT_BLUE = BRIGHT_ATTR | BLUE_ATTR; +const BYTE BRIGHT_MAGENTA = BRIGHT_ATTR | RED_ATTR | BLUE_ATTR; +const BYTE BRIGHT_CYAN = BRIGHT_ATTR | GREEN_ATTR | BLUE_ATTR; +const BYTE BRIGHT_WHITE = BRIGHT_ATTR | RED_ATTR | GREEN_ATTR | BLUE_ATTR; + +// Routine Description: +// Returns true if the GraphicsOption represents an extended color option. +// These are followed by up to 4 more values which compose the entire option. +// Return Value: +// - true if the opt is the indicator for an extended color sequence, false otherwise. +bool TerminalDispatch::s_IsRgbColorOption(const DispatchTypes::GraphicsOptions opt) noexcept +{ + return opt == DispatchTypes::GraphicsOptions::ForegroundExtended || + opt == DispatchTypes::GraphicsOptions::BackgroundExtended; +} + +// Routine Description: +// Returns true if the GraphicsOption represents an extended color option. +// These are followed by up to 4 more values which compose the entire option. +// Return Value: +// - true if the opt is the indicator for an extended color sequence, false otherwise. +bool TerminalDispatch::s_IsBoldColorOption(const DispatchTypes::GraphicsOptions opt) noexcept +{ + return opt == DispatchTypes::GraphicsOptions::BoldBright || + opt == DispatchTypes::GraphicsOptions::UnBold; +} + +// Function Description: +// - checks if this graphics option should set either the console's FG or BG to +//the default attributes. +// Return Value: +// - true if the opt sets either/or attribute to the defaults, false otherwise. +bool TerminalDispatch::s_IsDefaultColorOption(const DispatchTypes::GraphicsOptions opt) noexcept +{ + return opt == DispatchTypes::GraphicsOptions::Off || + opt == DispatchTypes::GraphicsOptions::ForegroundDefault || + opt == DispatchTypes::GraphicsOptions::BackgroundDefault; +} + +// Routine Description: +// - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG) +// These options are followed by either a 2 (RGB) or 5 (xterm index) +// RGB sequences then take 3 MORE params to designate the R, G, B parts of the color +// Xterm index will use the param that follows to use a color from the preset 256 color xterm color table. +// Arguments: +// - rgOptions - An array of options that will be used to generate the RGB color +// - cOptions - The count of options +// - pcOptionsConsumed - a pointer to place the number of options we consumed parsing this option. +// Return Value: +// Returns true if we successfully parsed an extended color option from the options array. +// - This corresponds to the following number of options consumed (pcOptionsConsumed): +// 1 - false, not enough options to parse. +// 2 - false, not enough options to parse. +// 3 - true, parsed an xterm index to a color +// 5 - true, parsed an RGB color. +bool TerminalDispatch::_SetRgbColorsHelper(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions, + _Out_ size_t* const pcOptionsConsumed) +{ + COLORREF color = 0; + bool isForeground = false; + + bool fSuccess = false; + *pcOptionsConsumed = 1; + if (cOptions >= 2 && s_IsRgbColorOption(rgOptions[0])) + { + *pcOptionsConsumed = 2; + DispatchTypes::GraphicsOptions extendedOpt = rgOptions[0]; + DispatchTypes::GraphicsOptions typeOpt = rgOptions[1]; + + if (extendedOpt == DispatchTypes::GraphicsOptions::ForegroundExtended) + { + isForeground = true; + } + else if (extendedOpt == DispatchTypes::GraphicsOptions::BackgroundExtended) + { + isForeground = false; + } + + if (typeOpt == DispatchTypes::GraphicsOptions::RGBColor && cOptions >= 5) + { + *pcOptionsConsumed = 5; + // ensure that each value fits in a byte + unsigned int red = rgOptions[2] > 255? 255 : rgOptions[2]; + unsigned int green = rgOptions[3] > 255? 255 : rgOptions[3]; + unsigned int blue = rgOptions[4] > 255? 255 : rgOptions[4]; + + color = RGB(red, green, blue); + + fSuccess = _terminalApi.SetTextRgbColor(color, isForeground); + } + else if (typeOpt == DispatchTypes::GraphicsOptions::Xterm256Index && cOptions >= 3) + { + *pcOptionsConsumed = 3; + if (rgOptions[2] <= 255) // ensure that the provided index is on the table + { + unsigned int tableIndex = rgOptions[2]; + fSuccess = isForeground ? + _terminalApi.SetTextForegroundIndex((BYTE)tableIndex) : + _terminalApi.SetTextBackgroundIndex((BYTE)tableIndex); + } + } + } + return fSuccess; +} + +bool TerminalDispatch::_SetBoldColorHelper(const DispatchTypes::GraphicsOptions option) +{ + const bool bold = (option == DispatchTypes::GraphicsOptions::BoldBright); + return _terminalApi.BoldText(bold); +} + +bool TerminalDispatch::_SetDefaultColorHelper(const DispatchTypes::GraphicsOptions option) +{ + const bool fg = option == DispatchTypes::GraphicsOptions::Off || option == DispatchTypes::GraphicsOptions::ForegroundDefault; + const bool bg = option == DispatchTypes::GraphicsOptions::Off || option == DispatchTypes::GraphicsOptions::BackgroundDefault; + bool success = _terminalApi.SetTextToDefaults(fg, bg); + + if (success && fg && bg) + { + // If we're resetting both the FG & BG, also reset the meta attributes (underline) + // as well as the boldness + success = _terminalApi.UnderlineText(false) && + _terminalApi.ReverseText(false) && + _terminalApi.BoldText(false); + } + return success; +} + +// Routine Description: +// - Helper to apply the actual flags to each text attributes field. +// - Placed as a helper so it can be recursive/re-entrant for some of the convenience flag methods that perform similar/multiple operations in one command. +// Arguments: +// - opt - Graphics option sent to us by the parser/requestor. +// - pAttr - Pointer to the font attribute field to adjust +// Return Value: +// - +void TerminalDispatch::_SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt) +{ + switch (opt) + { + case DispatchTypes::GraphicsOptions::Off: + FAIL_FAST_MSG("GraphicsOptions::Off should be handled by _SetDefaultColorHelper"); + break; + // MSFT:16398982 - These two are now handled by _SetBoldColorHelper + // case DispatchTypes::GraphicsOptions::BoldBright: + // case DispatchTypes::GraphicsOptions::UnBold: + case DispatchTypes::GraphicsOptions::Negative: + _terminalApi.ReverseText(true); + break; + case DispatchTypes::GraphicsOptions::Underline: + _terminalApi.UnderlineText(true); + break; + case DispatchTypes::GraphicsOptions::Positive: + _terminalApi.ReverseText(false); + break; + case DispatchTypes::GraphicsOptions::NoUnderline: + _terminalApi.UnderlineText(false); + break; + case DispatchTypes::GraphicsOptions::ForegroundBlack: + _terminalApi.SetTextForegroundIndex(DARK_BLACK); + break; + case DispatchTypes::GraphicsOptions::ForegroundBlue: + _terminalApi.SetTextForegroundIndex(DARK_BLUE); + break; + case DispatchTypes::GraphicsOptions::ForegroundGreen: + _terminalApi.SetTextForegroundIndex(DARK_GREEN); + break; + case DispatchTypes::GraphicsOptions::ForegroundCyan: + _terminalApi.SetTextForegroundIndex(DARK_CYAN); + break; + case DispatchTypes::GraphicsOptions::ForegroundRed: + _terminalApi.SetTextForegroundIndex(DARK_RED); + break; + case DispatchTypes::GraphicsOptions::ForegroundMagenta: + _terminalApi.SetTextForegroundIndex(DARK_MAGENTA); + break; + case DispatchTypes::GraphicsOptions::ForegroundYellow: + _terminalApi.SetTextForegroundIndex(DARK_YELLOW); + break; + case DispatchTypes::GraphicsOptions::ForegroundWhite: + _terminalApi.SetTextForegroundIndex(DARK_WHITE); + break; + case DispatchTypes::GraphicsOptions::ForegroundDefault: + FAIL_FAST_MSG("GraphicsOptions::ForegroundDefault should be handled by _SetDefaultColorHelper"); + break; + case DispatchTypes::GraphicsOptions::BackgroundBlack: + _terminalApi.SetTextBackgroundIndex(DARK_BLACK); + break; + case DispatchTypes::GraphicsOptions::BackgroundBlue: + _terminalApi.SetTextBackgroundIndex(DARK_BLUE); + break; + case DispatchTypes::GraphicsOptions::BackgroundGreen: + _terminalApi.SetTextBackgroundIndex(DARK_GREEN); + break; + case DispatchTypes::GraphicsOptions::BackgroundCyan: + _terminalApi.SetTextBackgroundIndex(DARK_CYAN); + break; + case DispatchTypes::GraphicsOptions::BackgroundRed: + _terminalApi.SetTextBackgroundIndex(DARK_RED); + break; + case DispatchTypes::GraphicsOptions::BackgroundMagenta: + _terminalApi.SetTextBackgroundIndex(DARK_MAGENTA); + break; + case DispatchTypes::GraphicsOptions::BackgroundYellow: + _terminalApi.SetTextBackgroundIndex(DARK_YELLOW); + break; + case DispatchTypes::GraphicsOptions::BackgroundWhite: + _terminalApi.SetTextBackgroundIndex(DARK_WHITE); + break; + case DispatchTypes::GraphicsOptions::BackgroundDefault: + FAIL_FAST_MSG("GraphicsOptions::BackgroundDefault should be handled by _SetDefaultColorHelper"); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlack: + _terminalApi.SetTextForegroundIndex(BRIGHT_BLACK); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlue: + _terminalApi.SetTextForegroundIndex(BRIGHT_BLUE); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundGreen: + _terminalApi.SetTextForegroundIndex(BRIGHT_GREEN); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundCyan: + _terminalApi.SetTextForegroundIndex(BRIGHT_CYAN); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundRed: + _terminalApi.SetTextForegroundIndex(BRIGHT_RED); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundMagenta: + _terminalApi.SetTextForegroundIndex(BRIGHT_MAGENTA); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundYellow: + _terminalApi.SetTextForegroundIndex(BRIGHT_YELLOW); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundWhite: + _terminalApi.SetTextForegroundIndex(BRIGHT_WHITE); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlack: + _terminalApi.SetTextBackgroundIndex(BRIGHT_BLACK); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlue: + _terminalApi.SetTextBackgroundIndex(BRIGHT_BLUE); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundGreen: + _terminalApi.SetTextBackgroundIndex(BRIGHT_GREEN); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundCyan: + _terminalApi.SetTextBackgroundIndex(BRIGHT_CYAN); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundRed: + _terminalApi.SetTextBackgroundIndex(BRIGHT_RED); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundMagenta: + _terminalApi.SetTextBackgroundIndex(BRIGHT_MAGENTA); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundYellow: + _terminalApi.SetTextBackgroundIndex(BRIGHT_YELLOW); + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundWhite: + _terminalApi.SetTextBackgroundIndex(BRIGHT_WHITE); + break; + } +} + +bool TerminalDispatch::SetGraphicsRendition(const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions) +{ + bool fSuccess = false; + // Run through the graphics options and apply them + for (size_t i = 0; i < cOptions; i++) + { + DispatchTypes::GraphicsOptions opt = rgOptions[i]; + if (s_IsDefaultColorOption(opt)) + { + fSuccess = _SetDefaultColorHelper(opt); + } + else if (s_IsBoldColorOption(opt)) + { + fSuccess = _SetBoldColorHelper(rgOptions[i]); + } + else if (s_IsRgbColorOption(opt)) + { + size_t cOptionsConsumed = 0; + + // _SetRgbColorsHelper will call the appropriate ConApi function + fSuccess = _SetRgbColorsHelper(&(rgOptions[i]), cOptions-i, &cOptionsConsumed); + + i += (cOptionsConsumed - 1); // cOptionsConsumed includes the opt we're currently on. + } + else + { + _SetGraphicsOptionHelper(opt); + + // Make sure we un-bold + if (fSuccess && opt == DispatchTypes::GraphicsOptions::Off) + { + fSuccess = _SetBoldColorHelper(opt); + } + } + } + return fSuccess; +} diff --git a/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj b/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj new file mode 100644 index 000000000..161c77f69 --- /dev/null +++ b/src/cascadia/TerminalCore/lib/terminalcore-lib.vcxproj @@ -0,0 +1,56 @@ + + + + + + + + + + + {CA5CAD1A-ABCD-429C-B551-8562EC954746} + Win32Proj + TerminalCore + TerminalCore + 10.0.17763.0 + 10.0.17763.0 + Microsoft.Terminal.Core + + + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {48d21369-3d7b-4431-9967-24e81292cf62} + + + + + + pch.h + + $(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)src\cascadia\TerminalSettings\Generated Files";%(AdditionalIncludeDirectories); + + + + + + + + diff --git a/src/cascadia/TerminalCore/packages.config b/src/cascadia/TerminalCore/packages.config new file mode 100644 index 000000000..4f7a6f98a --- /dev/null +++ b/src/cascadia/TerminalCore/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/TerminalCore/pch.cpp b/src/cascadia/TerminalCore/pch.cpp new file mode 100644 index 000000000..398a99f66 --- /dev/null +++ b/src/cascadia/TerminalCore/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalCore/pch.h b/src/cascadia/TerminalCore/pch.h new file mode 100644 index 000000000..e03399daf --- /dev/null +++ b/src/cascadia/TerminalCore/pch.h @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include diff --git a/src/cascadia/TerminalCore/terminalcore-common.vcxproj b/src/cascadia/TerminalCore/terminalcore-common.vcxproj new file mode 100644 index 000000000..5215ba6a4 --- /dev/null +++ b/src/cascadia/TerminalCore/terminalcore-common.vcxproj @@ -0,0 +1,22 @@ + + + + + + + + + + + Create + + + + + + + + + + + diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp new file mode 100644 index 000000000..8b9b9c8ac --- /dev/null +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "Terminal.hpp" +#include +using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::Render; + +Viewport Terminal::GetViewport() noexcept +{ + return _GetVisibleViewport(); +} + +const TextBuffer& Terminal::GetTextBuffer() noexcept +{ + return *_buffer; +} + +const FontInfo& Terminal::GetFontInfo() noexcept +{ + // TODO: This font value is only used to check if the font is a raster font. + // Otherwise, the font is changed with the renderer via TriggerFontChange. + // The renderer never uses any of the other members from the value returned + // by this method. + // We could very likely replace this with just an IsRasterFont method + // (which would return false) + static const FontInfo _fakeFontInfo(DEFAULT_FONT_FACE.c_str(), TMPF_TRUETYPE, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false); + return _fakeFontInfo; +} + +const TextAttribute Terminal::GetDefaultBrushColors() noexcept +{ + return TextAttribute{}; +} + +const COLORREF Terminal::GetForegroundColor(const TextAttribute& attr) const noexcept +{ + return 0xff000000 | attr.CalculateRgbForeground({ &_colorTable[0], _colorTable.size() }, _defaultFg, _defaultBg); +} + +const COLORREF Terminal::GetBackgroundColor(const TextAttribute& attr) const noexcept +{ + const auto bgColor = attr.CalculateRgbBackground({ &_colorTable[0], _colorTable.size() }, _defaultFg, _defaultBg); + // We only care about alpha for the default BG (which enables acrylic) + // If the bg isn't the default bg color, then make it fully opaque. + if (!attr.BackgroundIsDefault()) + { + return 0xff000000 | bgColor; + } + return bgColor; +} + +COORD Terminal::GetCursorPosition() const noexcept +{ + const auto& cursor = _buffer->GetCursor(); + return cursor.GetPosition(); +} + +bool Terminal::IsCursorVisible() const noexcept +{ + const auto& cursor = _buffer->GetCursor(); + return cursor.IsVisible() && !cursor.IsPopupShown(); +} + +bool Terminal::IsCursorOn() const noexcept +{ + const auto& cursor = _buffer->GetCursor(); + return cursor.IsOn(); +} + +ULONG Terminal::GetCursorPixelWidth() const noexcept +{ + return 1; +} + +ULONG Terminal::GetCursorHeight() const noexcept +{ + return _buffer->GetCursor().GetSize(); +} + +CursorType Terminal::GetCursorStyle() const noexcept +{ + return _buffer->GetCursor().GetType(); +} + +COLORREF Terminal::GetCursorColor() const noexcept +{ + return _buffer->GetCursor().GetColor(); +} + +bool Terminal::IsCursorDoubleWidth() const noexcept +{ + return false; +} + +const std::vector Terminal::GetOverlays() const noexcept +{ + return {}; +} + +const bool Terminal::IsGridLineDrawingAllowed() noexcept +{ + return true; +} + +std::vector Terminal::GetSelectionRects() noexcept +{ + std::vector result; + + for (const auto& lineRect : _GetSelectionRects()) + { + result.emplace_back(Viewport::FromInclusive(lineRect)); + } + + return result; +} + +const std::wstring Terminal::GetConsoleTitle() const noexcept +{ + return _title; +} + +// Method Description: +// - Lock the terminal for reading the contents of the buffer. Ensures that the +// contents of the terminal won't be changed in the middle of a paint +// operation. +// Callers should make sure to also call Terminal::UnlockConsole once +// they're done with any querying they need to do. +void Terminal::LockConsole() noexcept +{ + _readWriteLock.lock_shared(); +} + +// Method Description: +// - Unlocks the terminal after a call to Terminal::LockConsole. +void Terminal::UnlockConsole() noexcept +{ + _readWriteLock.unlock_shared(); +} diff --git a/src/cascadia/TerminalSettings/IControlSettings.idl b/src/cascadia/TerminalSettings/IControlSettings.idl new file mode 100644 index 000000000..e552e3ac4 --- /dev/null +++ b/src/cascadia/TerminalSettings/IControlSettings.idl @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "IKeyBindings.idl"; +import "ICoreSettings.idl"; + +namespace Microsoft.Terminal.Settings +{ + enum ScrollbarState + { + Visible = 0, + Hidden + }; + + // Class Description: + // TerminalSettings encapsulates all settings that control the + // TermControl's behavior. In these settings there is both the entirety + // of the Core ITerminalSettings interface, and any additional settings + // for specifically the control. + interface IControlSettings requires Microsoft.Terminal.Settings.ICoreSettings + { + Boolean UseAcrylic; + Boolean CloseOnExit; + Double TintOpacity; + ScrollbarState ScrollState; + + String FontFace; + Int32 FontSize; + String Padding; + + IKeyBindings KeyBindings; + + String Commandline; + String StartingDirectory; + String EnvironmentVariables; + + }; +} diff --git a/src/cascadia/TerminalSettings/ICoreSettings.idl b/src/cascadia/TerminalSettings/ICoreSettings.idl new file mode 100644 index 000000000..ecf14cb3c --- /dev/null +++ b/src/cascadia/TerminalSettings/ICoreSettings.idl @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings +{ + enum CursorStyle + { + Vintage, + Bar, + Underscore, + FilledBox, + EmptyBox + }; + + interface ICoreSettings + { + UInt32 DefaultForeground; + UInt32 DefaultBackground; + UInt32 GetColorTableEntry(Int32 index); + void SetColorTableEntry(Int32 index, UInt32 value); + // TODO:MSFT:20642297 - define a sentinel for Infinite Scrollback + Int32 HistorySize; + Int32 InitialRows; + Int32 InitialCols; + Boolean SnapOnInput; + + UInt32 CursorColor; + CursorStyle CursorShape; + UInt32 CursorHeight; + }; + +} diff --git a/src/cascadia/TerminalSettings/IKeyBindings.idl b/src/cascadia/TerminalSettings/IKeyBindings.idl new file mode 100644 index 000000000..5c43e6dd3 --- /dev/null +++ b/src/cascadia/TerminalSettings/IKeyBindings.idl @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "KeyChord.idl"; + +namespace Microsoft.Terminal.Settings +{ + // [default_interface] + interface IKeyBindings + { + Boolean TryKeyChord(KeyChord kc); + } +} diff --git a/src/cascadia/TerminalSettings/KeyChord.cpp b/src/cascadia/TerminalSettings/KeyChord.cpp new file mode 100644 index 000000000..96e771acb --- /dev/null +++ b/src/cascadia/TerminalSettings/KeyChord.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "KeyChord.h" + +namespace winrt::Microsoft::Terminal::Settings::implementation +{ + KeyChord::KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey) : + _modifiers{ (ctrl ? Settings::KeyModifiers::Ctrl : Settings::KeyModifiers::None) | + (alt ? Settings::KeyModifiers::Alt : Settings::KeyModifiers::None) | + (shift ? Settings::KeyModifiers::Shift : Settings::KeyModifiers::None) }, + _vkey{ vkey } + { + } + + KeyChord::KeyChord(Settings::KeyModifiers const& modifiers, int32_t vkey) : + _modifiers{ modifiers }, + _vkey{ vkey } + { + } + + Settings::KeyModifiers KeyChord::Modifiers() + { + return _modifiers; + } + + void KeyChord::Modifiers(Settings::KeyModifiers const& value) + { + _modifiers = value; + } + + int32_t KeyChord::Vkey() + { + return _vkey; + } + + void KeyChord::Vkey(int32_t value) + { + _vkey = value; + } +} diff --git a/src/cascadia/TerminalSettings/KeyChord.h b/src/cascadia/TerminalSettings/KeyChord.h new file mode 100644 index 000000000..61456b257 --- /dev/null +++ b/src/cascadia/TerminalSettings/KeyChord.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "KeyChord.g.h" + +namespace winrt::Microsoft::Terminal::Settings::implementation +{ + struct KeyChord : KeyChordT + { + KeyChord() = default; + KeyChord(Settings::KeyModifiers const& modifiers, int32_t vkey); + KeyChord(bool ctrl, bool alt, bool shift, int32_t vkey); + + Settings::KeyModifiers Modifiers(); + void Modifiers(Settings::KeyModifiers const& value); + int32_t Vkey(); + void Vkey(int32_t value); + + private: + Settings::KeyModifiers _modifiers; + int32_t _vkey; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::factory_implementation +{ + struct KeyChord : KeyChordT + { + }; +} diff --git a/src/cascadia/TerminalSettings/KeyChord.idl b/src/cascadia/TerminalSettings/KeyChord.idl new file mode 100644 index 000000000..c02459144 --- /dev/null +++ b/src/cascadia/TerminalSettings/KeyChord.idl @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Settings +{ + [flags] + enum KeyModifiers + { + None = 0x0000, + Alt = 0x0001, + Ctrl = 0x0002, + Shift = 0x0004 + }; + + [default_interface] + runtimeclass KeyChord + { + KeyChord(); + KeyChord(KeyModifiers modifiers, Int32 vkey); + KeyChord(Boolean ctrl, Boolean alt, Boolean shift, Int32 vkey); + + KeyModifiers Modifiers; + Int32 Vkey; + } +} diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp new file mode 100644 index 000000000..d166ad923 --- /dev/null +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalSettings.h" +#include + +namespace winrt::Microsoft::Terminal::Settings::implementation +{ + TerminalSettings::TerminalSettings() : + _defaultForeground{ DEFAULT_FOREGROUND_WITH_ALPHA }, + _defaultBackground{ DEFAULT_BACKGROUND_WITH_ALPHA }, + _colorTable{}, + _historySize{ DEFAULT_HISTORY_SIZE }, + _initialRows{ 30 }, + _initialCols{ 80 }, + _snapOnInput{ true }, + _cursorColor{ DEFAULT_CURSOR_COLOR }, + _cursorShape{ CursorStyle::Vintage }, + _cursorHeight{ DEFAULT_CURSOR_HEIGHT }, + _useAcrylic{ false }, + _closeOnExit{ false }, + _tintOpacity{ 0.5 }, + _padding{ DEFAULT_PADDING }, + _fontFace{ DEFAULT_FONT_FACE }, + _fontSize{ DEFAULT_FONT_SIZE }, + _keyBindings{ nullptr }, + _scrollbarState{ ScrollbarState::Visible } + { + + } + + uint32_t TerminalSettings::DefaultForeground() + { + return _defaultForeground; + } + + void TerminalSettings::DefaultForeground(uint32_t value) + { + _defaultForeground = value; + } + + uint32_t TerminalSettings::DefaultBackground() + { + return _defaultBackground; + } + + void TerminalSettings::DefaultBackground(uint32_t value) + { + _defaultBackground = value; + } + + uint32_t TerminalSettings::GetColorTableEntry(int32_t index) const + { + return _colorTable[index]; + } + + void TerminalSettings::SetColorTableEntry(int32_t index, uint32_t value) + { + THROW_HR_IF(E_INVALIDARG, index > _colorTable.size()); + _colorTable[index] = value; + } + + int32_t TerminalSettings::HistorySize() + { + return _historySize; + } + + void TerminalSettings::HistorySize(int32_t value) + { + _historySize = value; + } + + int32_t TerminalSettings::InitialRows() + { + return _initialRows; + } + + void TerminalSettings::InitialRows(int32_t value) + { + _initialRows = value; + } + + int32_t TerminalSettings::InitialCols() + { + return _initialCols; + } + + void TerminalSettings::InitialCols(int32_t value) + { + _initialCols = value; + } + + bool TerminalSettings::SnapOnInput() + { + return _snapOnInput; + } + + void TerminalSettings::SnapOnInput(bool value) + { + _snapOnInput = value; + } + + uint32_t TerminalSettings::CursorColor() + { + return _cursorColor; + } + + void TerminalSettings::CursorColor(uint32_t value) + { + _cursorColor = value; + } + + Settings::CursorStyle TerminalSettings::CursorShape() const noexcept + { + return _cursorShape; + } + + void TerminalSettings::CursorShape(Settings::CursorStyle const& value) noexcept + { + _cursorShape = value; + } + + uint32_t TerminalSettings::CursorHeight() + { + return _cursorHeight; + } + + void TerminalSettings::CursorHeight(uint32_t value) + { + _cursorHeight = value; + } + + bool TerminalSettings::UseAcrylic() + { + return _useAcrylic; + } + + void TerminalSettings::UseAcrylic(bool value) + { + _useAcrylic = value; + } + + bool TerminalSettings::CloseOnExit() + { + return _closeOnExit; + } + + void TerminalSettings::CloseOnExit(bool value) + { + _closeOnExit = value; + } + + double TerminalSettings::TintOpacity() + { + return _tintOpacity; + } + + void TerminalSettings::TintOpacity(double value) + { + _tintOpacity = value; + } + + hstring TerminalSettings::Padding() + { + return _padding; + } + + void TerminalSettings::Padding(hstring value) + { + _padding = value; + } + + hstring TerminalSettings::FontFace() + { + return _fontFace; + } + + void TerminalSettings::FontFace(hstring const& value) + { + _fontFace = value; + } + + int32_t TerminalSettings::FontSize() + { + return _fontSize; + } + + void TerminalSettings::FontSize(int32_t value) + { + _fontSize = value; + } + + Settings::IKeyBindings TerminalSettings::KeyBindings() + { + return _keyBindings; + } + + void TerminalSettings::KeyBindings(Settings::IKeyBindings const& value) + { + _keyBindings = value; + } + + hstring TerminalSettings::Commandline() + { + return _commandline; + } + + void TerminalSettings::Commandline(hstring const& value) + { + _commandline = value; + } + + hstring TerminalSettings::StartingDirectory() + { + return _startingDir; + } + + void TerminalSettings::StartingDirectory(hstring const& value) + { + _startingDir = value; + } + + hstring TerminalSettings::EnvironmentVariables() + { + return _envVars; + } + + void TerminalSettings::EnvironmentVariables(hstring const& value) + { + _envVars = value; + } + + Settings::ScrollbarState TerminalSettings::ScrollState() const noexcept + { + return _scrollbarState; + } + + void TerminalSettings::ScrollState(Settings::ScrollbarState const& value) noexcept + { + _scrollbarState = value; + } + +} diff --git a/src/cascadia/TerminalSettings/TerminalSettings.def b/src/cascadia/TerminalSettings/TerminalSettings.def new file mode 100644 index 000000000..8c1a02932 --- /dev/null +++ b/src/cascadia/TerminalSettings/TerminalSettings.def @@ -0,0 +1,3 @@ +EXPORTS +DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE +DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE diff --git a/src/cascadia/TerminalSettings/TerminalSettings.idl b/src/cascadia/TerminalSettings/TerminalSettings.idl new file mode 100644 index 000000000..15d265b1a --- /dev/null +++ b/src/cascadia/TerminalSettings/TerminalSettings.idl @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ICoreSettings.idl"; +import "IControlSettings.idl"; + +namespace Microsoft.Terminal.Settings +{ + // Class Description: + // TerminalSettings encapsulates all settings that control the + // TermControl's behavior. In these settings there is both the entirety + // of the Core ICoreSettings properties and the IControlSettings + // properties. It's the Profile's responsibility to build this from + // settings it contains, in combination with the global settings. + // The TerminalControl will pull settings it requires from this object, + // and pass along the Core properties to the terminal core. + [default_interface] + runtimeclass TerminalSettings : ICoreSettings, + IControlSettings + { + TerminalSettings(); + }; + +} diff --git a/src/cascadia/TerminalSettings/TerminalSettings.vcxproj b/src/cascadia/TerminalSettings/TerminalSettings.vcxproj new file mode 100644 index 000000000..b40d1d8ae --- /dev/null +++ b/src/cascadia/TerminalSettings/TerminalSettings.vcxproj @@ -0,0 +1,61 @@ + + + + + + + DynamicLibrary + Console + + true + + + + {CA5CAD1A-d7ec-4107-b7c6-79cb77ae2907} + TerminalSettings + Microsoft.Terminal.Settings + + + + + KeyChord.idl + + + TerminalSettings.idl + + + + + Create + + + KeyChord.idl + + + TerminalSettings.idl + + + + + + + + + + + + + + + + + + true + + + + diff --git a/src/cascadia/TerminalSettings/packages.config b/src/cascadia/TerminalSettings/packages.config new file mode 100644 index 000000000..4f7a6f98a --- /dev/null +++ b/src/cascadia/TerminalSettings/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/cascadia/TerminalSettings/pch.cpp b/src/cascadia/TerminalSettings/pch.cpp new file mode 100644 index 000000000..3c27d44d5 --- /dev/null +++ b/src/cascadia/TerminalSettings/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/TerminalSettings/pch.h b/src/cascadia/TerminalSettings/pch.h new file mode 100644 index 000000000..8f45542e8 --- /dev/null +++ b/src/cascadia/TerminalSettings/pch.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// pch.h +// Header for platform projection include files +// + +#pragma once + +#define WIN32_LEAN_AND_MEAN + +#include +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif + +#include +#include + diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h new file mode 100644 index 000000000..847da3a76 --- /dev/null +++ b/src/cascadia/TerminalSettings/terminalsettings.h @@ -0,0 +1,110 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TerminalSettings.h + +Abstract: +- The implementation of the TerminalSettings winrt class. Provides both + terminal control settings and terminal core settings. +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once + +#include +#include "TerminalSettings.g.h" + +namespace winrt::Microsoft::Terminal::Settings::implementation +{ + struct TerminalSettings : TerminalSettingsT + { + TerminalSettings(); + + // --------------------------- Core Settings --------------------------- + // All of these settings are defined in ICoreSettings. + uint32_t DefaultForeground(); + void DefaultForeground(uint32_t value); + uint32_t DefaultBackground(); + void DefaultBackground(uint32_t value); + uint32_t GetColorTableEntry(int32_t index) const; + void SetColorTableEntry(int32_t index, uint32_t value); + int32_t HistorySize(); + void HistorySize(int32_t value); + int32_t InitialRows(); + void InitialRows(int32_t value); + int32_t InitialCols(); + void InitialCols(int32_t value); + bool SnapOnInput(); + void SnapOnInput(bool value); + uint32_t CursorColor(); + void CursorColor(uint32_t value); + CursorStyle CursorShape() const noexcept; + void CursorShape(winrt::Microsoft::Terminal::Settings::CursorStyle const& value) noexcept; + uint32_t CursorHeight(); + void CursorHeight(uint32_t value); + // ------------------------ End of Core Settings ----------------------- + + bool UseAcrylic(); + void UseAcrylic(bool value); + bool CloseOnExit(); + void CloseOnExit(bool value); + double TintOpacity(); + void TintOpacity(double value); + hstring Padding(); + void Padding(hstring value); + + hstring FontFace(); + void FontFace(hstring const& value); + int32_t FontSize(); + void FontSize(int32_t value); + + winrt::Microsoft::Terminal::Settings::IKeyBindings KeyBindings(); + void KeyBindings(winrt::Microsoft::Terminal::Settings::IKeyBindings const& value); + + hstring Commandline(); + void Commandline(hstring const& value); + + hstring StartingDirectory(); + void StartingDirectory(hstring const& value); + + hstring EnvironmentVariables(); + void EnvironmentVariables(hstring const& value); + + ScrollbarState ScrollState() const noexcept; + void ScrollState(winrt::Microsoft::Terminal::Settings::ScrollbarState const& value) noexcept; + + private: + uint32_t _defaultForeground; + uint32_t _defaultBackground; + std::array _colorTable; + int32_t _historySize; + int32_t _initialRows; + int32_t _initialCols; + bool _snapOnInput; + uint32_t _cursorColor; + Settings::CursorStyle _cursorShape; + uint32_t _cursorHeight; + + bool _useAcrylic; + bool _closeOnExit; + double _tintOpacity; + hstring _fontFace; + int32_t _fontSize; + hstring _padding; + hstring _commandline; + hstring _startingDir; + hstring _envVars; + Settings::IKeyBindings _keyBindings; + Settings::ScrollbarState _scrollbarState; + }; +} + +namespace winrt::Microsoft::Terminal::Settings::factory_implementation +{ + struct TerminalSettings : TerminalSettingsT + { + }; +} diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp new file mode 100644 index 000000000..f84471211 --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -0,0 +1,182 @@ +/* +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT license. +* +* This File was generated using the VisualTAEF C++ Project Wizard. +* Class Name: SelectionTest +*/ +#include "precomp.h" +#include + +#include "../cascadia/TerminalCore/Terminal.hpp" +#include "../renderer/inc/DummyRenderTarget.hpp" +#include "consoletaeftemplates.hpp" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace Microsoft::Terminal::Core; +using namespace Microsoft::Console::Render; + +namespace TerminalCoreUnitTests +{ + class SelectionTest + { + TEST_CLASS(SelectionTest); + + TEST_METHOD(SelectUnit) + { + Terminal term = Terminal(); + DummyRenderTarget emptyRT; + term.Create({ 100, 100 }, 0, emptyRT); + + // Simulate click at (x,y) = (5,10) + auto clickPos = COORD{ 5, 10 }; + term.SetSelectionAnchor(clickPos); + + // Simulate renderer calling TriggerSelection and acquiring selection area + auto selectionRects = term.GetSelectionRects(); + + // Validate selection area + VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(1)); + + auto selection = term.GetViewport().ConvertToOrigin(selectionRects.at(0)).ToInclusive(); + VerifyCompareTraits::AreEqual({5, 10, 10, 5}, selection); + } + + TEST_METHOD(SelectArea) + { + Terminal term = Terminal(); + DummyRenderTarget emptyRT; + term.Create({ 100, 100 }, 0, emptyRT); + + // Used for two things: + // - click y-pos + // - keep track of row we're verifying + SHORT rowValue = 10; + + // Simulate click at (x,y) = (5,10) + term.SetSelectionAnchor({ 5, rowValue }); + + // Simulate move to (x,y) = (15,20) + term.SetEndSelectionPosition({ 15, 20 }); + + // Simulate renderer calling TriggerSelection and acquiring selection area + auto selectionRects = term.GetSelectionRects(); + + // Validate selection area + VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(11)); + + auto viewport = term.GetViewport(); + SHORT rightBoundary = viewport.RightInclusive(); + for (auto selectionRect : selectionRects) + { + auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive(); + + if (rowValue == 10) + { + // Verify top line + VerifyCompareTraits::AreEqual({5, 10, rightBoundary, 10}, selection); + } + else if (rowValue == 20) + { + // Verify bottom line + VerifyCompareTraits::AreEqual({0, 20, 15, 20}, selection); + } + else + { + // Verify other lines (full) + VerifyCompareTraits::AreEqual({0, rowValue, rightBoundary, rowValue}, selection); + } + + rowValue++; + } + } + + TEST_METHOD(SelectBoxArea) + { + Terminal term = Terminal(); + DummyRenderTarget emptyRT; + term.Create({ 100, 100 }, 0, emptyRT); + + // Used for two things: + // - click y-pos + // - keep track of row we're verifying + SHORT rowValue = 10; + + // Simulate ALT + click at (x,y) = (5,10) + term.SetSelectionAnchor({ 5, rowValue }); + term.SetBoxSelection(true); + + // Simulate move to (x,y) = (15,20) + term.SetEndSelectionPosition({ 15, 20 }); + + // Simulate renderer calling TriggerSelection and acquiring selection area + auto selectionRects = term.GetSelectionRects(); + + // Validate selection area + VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(11)); + + auto viewport = term.GetViewport(); + for (auto selectionRect : selectionRects) + { + auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive(); + + // Verify all lines + VerifyCompareTraits::AreEqual({5, rowValue, 15, rowValue}, selection); + + rowValue++; + } + } + + TEST_METHOD(SelectAreaAfterScroll) + { + Terminal term = Terminal(); + DummyRenderTarget emptyRT; + SHORT scrollbackLines = 5; + term.Create({ 100, 100 }, scrollbackLines, emptyRT); + + // Used for two things: + // - click y-pos + // - keep track of row we're verifying + SHORT rowValue = 10; + + // Simulate click at (x,y) = (5,10) + term.SetSelectionAnchor({ 5, rowValue }); + + // Simulate move to (x,y) = (15,20) + term.SetEndSelectionPosition({ 15, 20 }); + + // Simulate renderer calling TriggerSelection and acquiring selection area + auto selectionRects = term.GetSelectionRects(); + + // Validate selection area + VERIFY_ARE_EQUAL(selectionRects.size(), static_cast(11)); + + auto viewport = term.GetViewport(); + SHORT rightBoundary = viewport.RightInclusive(); + for (auto selectionRect : selectionRects) + { + auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive(); + + if (rowValue == 10) + { + // Verify top line + VerifyCompareTraits::AreEqual({5, 10, rightBoundary, 10}, selection); + } + else if (rowValue == 20) + { + // Verify bottom line + VerifyCompareTraits::AreEqual({0, 20, 15, 20}, selection); + } + else + { + // Verify other lines (full) + VerifyCompareTraits::AreEqual({0, rowValue, rightBoundary, rowValue}, selection); + } + + rowValue++; + } + } + }; +} \ No newline at end of file diff --git a/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj new file mode 100644 index 000000000..d01c7223c --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/UnitTests.vcxproj @@ -0,0 +1,50 @@ + + + + + + + Create + + + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + {1cf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {ca5cad1a-abcd-429c-b551-8562ec954746} + + + + + + + {2C2BEEF4-9333-4D05-B12A-1905CBF112F9} + Win32Proj + TerminalCoreUnitTests + UnitTests_TerminalCore + Terminal.Core.Unit.Tests + + + + ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories) + precomp.h + + + WindowsApp.lib;%(AdditionalDependencies) + + + + + + + \ No newline at end of file diff --git a/src/cascadia/UnitTests_TerminalCore/precomp.cpp b/src/cascadia/UnitTests_TerminalCore/precomp.cpp new file mode 100644 index 000000000..6a6fa8e5a --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" \ No newline at end of file diff --git a/src/cascadia/UnitTests_TerminalCore/precomp.h b/src/cascadia/UnitTests_TerminalCore/precomp.h new file mode 100644 index 000000000..ddc9a4116 --- /dev/null +++ b/src/cascadia/UnitTests_TerminalCore/precomp.h @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). + +Author(s): +- Carlos Zamora (cazamor) April 2019 +--*/ + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#ifdef BUILDING_INSIDE_WINIDE +#define DbgRaiseAssertionFailure() __int2c() +#endif + +#include + +// Comment to build against the private SDK. +#define CON_BUILD_PUBLIC + +#ifdef CON_BUILD_PUBLIC +#define CON_USERPRIVAPI_INDIRECT +#define CON_DPIAPI_INDIRECT +#endif \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp new file mode 100644 index 000000000..2b95c764a --- /dev/null +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "AppHost.h" +#include "../types/inc/Viewport.hpp" + +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console::Types; + +// The tabs are 34.8px tall. This is their default height - we're not +// controlling the styling of the tabs at all currently. If we change the size +// of those, we'll need to change the size here, too. We can't get this size +// from the tab control until the control is added to a XAML element, and we +// can't create any XAML elements until we have a window, and we need to know +// this size before we can create a window, so unfortunately we're stuck +// hardcoding this. +const int NON_CLIENT_CONTENT_HEIGHT = static_cast(std::round(34.8)); + +AppHost::AppHost() noexcept : + _app{}, + _window{ nullptr } +{ + _useNonClientArea = _app.GetShowTabsInTitlebar(); + + if (_useNonClientArea) + { + _window = std::make_unique(); + auto pNcWindow = static_cast(_window.get()); + + pNcWindow->SetNonClientHeight(NON_CLIENT_CONTENT_HEIGHT); + } + else + { + _window = std::make_unique(); + } + + // Tell the window to callback to us when it's about to handle a WM_CREATE + auto pfn = std::bind(&AppHost::_HandleCreateWindow, + this, + std::placeholders::_1, + std::placeholders::_2); + _window->SetCreateCallback(pfn); + + _window->MakeWindow(); +} + +AppHost::~AppHost() +{ +} + +// Method Description: +// - Initializes the XAML island, creates the terminal app, and sets the +// island's content to that of the terminal app's content. Also registers some +// callbacks with TermApp. +// !!! IMPORTANT!!! +// This must be called *AFTER* WindowsXamlManager::InitializeForCurrentThread. +// If it isn't, then we won't be able to create the XAML island. +// Arguments: +// - +// Return Value: +// - +void AppHost::Initialize() +{ + _window->Initialize(); + _app.Create(); + + _app.TitleChanged({ this, &AppHost::AppTitleChanged }); + + AppTitleChanged(_app.GetTitle()); + + _window->SetRootContent(_app.GetRoot()); + if (_useNonClientArea) + { + auto pNcWindow = static_cast(_window.get()); + pNcWindow->SetNonClientContent(_app.GetTabs()); + } +} + +// Method Description: +// - Called when the app's title changes. Fires off a window message so we can +// update the window's title on the main thread. +// Arguments: +// - newTitle: the string to use as the new window title +// Return Value: +// - +void AppHost::AppTitleChanged(winrt::hstring newTitle) +{ + _window->UpdateTitle(newTitle.c_str()); +} + +// Method Description: +// - Resize the window we're about to create to the appropriate dimensions, as +// specified in the settings. This will be called during the handling of +// WM_CREATE. We'll load the settings for the app, then get the proposed size +// of the terminal from the app. Using that proposed size, we'll resize the +// window we're creating, so that it'll match the values in the settings. +// Arguments: +// - hwnd: The HWND of the window we're about to create. +// - proposedRect: The location and size of the window that we're about to +// create. We'll use this rect to determine which monitor the window is about +// to appear on. +// Return Value: +// - +void AppHost::_HandleCreateWindow(const HWND hwnd, const RECT proposedRect) +{ + // Find nearest montitor. + HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); + + // This API guarantees that dpix and dpiy will be equal, but neither is an + // optional parameter so give two UINTs. + UINT dpix = USER_DEFAULT_SCREEN_DPI; + UINT dpiy = USER_DEFAULT_SCREEN_DPI; + // If this fails, we'll use the default of 96. + GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); + + auto initialSize = _app.GetLaunchDimensions(dpix); + + const short _currentWidth = gsl::narrow(ceil(initialSize.X)); + const short _currentHeight = gsl::narrow(ceil(initialSize.Y)); + + // Create a RECT from our requested client size + auto nonClient = Viewport::FromDimensions({ _currentWidth, + _currentHeight }).ToRect(); + + // Get the size of a window we'd need to host that client rect. This will + // add the titlebar space. + if (_useNonClientArea) + { + // If we're in NC tabs mode, do the math ourselves. Get the margins + // we're using for the window - this will include the size of the + // titlebar content. + auto pNcWindow = static_cast(_window.get()); + const MARGINS margins = pNcWindow->GetFrameMargins(); + nonClient.left = 0; + nonClient.top = 0; + nonClient.right = margins.cxLeftWidth + nonClient.right + margins.cxRightWidth; + nonClient.bottom = margins.cyTopHeight + nonClient.bottom + margins.cyBottomHeight; + } + else + { + bool succeeded = AdjustWindowRectExForDpi(&nonClient, WS_OVERLAPPEDWINDOW, false, 0, dpix); + if (!succeeded) + { + // If we failed to get the correct window size for whatever reason, log + // the error and go on. We'll use whatever the control proposed as the + // size of our window, which will be at least close. + LOG_LAST_ERROR(); + nonClient = Viewport::FromDimensions({ _currentWidth, + _currentHeight }).ToRect(); + } + } + + + const auto adjustedHeight = nonClient.bottom - nonClient.top; + const auto adjustedWidth = nonClient.right - nonClient.left; + + const COORD origin{ gsl::narrow(proposedRect.left), + gsl::narrow(proposedRect.top) }; + const COORD dimensions{ gsl::narrow(adjustedWidth), + gsl::narrow(adjustedHeight) }; + + const auto newPos = Viewport::FromDimensions(origin, dimensions); + + bool succeeded = SetWindowPos(hwnd, nullptr, + newPos.Left(), + newPos.Top(), + newPos.Width(), + newPos.Height(), + SWP_NOACTIVATE | SWP_NOZORDER); + + // If we can't resize the window, that's really okay. We can just go on with + // the originally proposed window size. + LOG_LAST_ERROR_IF(!succeeded); + + +} diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h new file mode 100644 index 000000000..fcd8d0ca1 --- /dev/null +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" + +#include +#include + +#include "NonClientIslandWindow.h" + +class AppHost +{ +public: + AppHost() noexcept; + virtual ~AppHost(); + + void AppTitleChanged(winrt::hstring newTitle); + + void Initialize(); + +private: + bool _useNonClientArea; + + std::unique_ptr _window; + winrt::TerminalApp::App _app; + + void _HandleCreateWindow(const HWND hwnd, const RECT proposedRect); +}; diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h new file mode 100644 index 000000000..d7f327a56 --- /dev/null +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// Custom window messages +#define CM_UPDATE_TITLE (WM_USER) + +template +class BaseWindow +{ +public: + virtual ~BaseWindow() = 0; + static T* GetThisFromHandle(HWND const window) noexcept + { + return reinterpret_cast(GetWindowLongPtr(window, GWLP_USERDATA)); + } + + static LRESULT __stdcall WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept + { + WINRT_ASSERT(window); + + if (WM_NCCREATE == message) + { + auto cs = reinterpret_cast(lparam); + T* that = static_cast(cs->lpCreateParams); + WINRT_ASSERT(that); + WINRT_ASSERT(!that->_window); + that->_window = window; + SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(that)); + + EnableNonClientDpiScaling(window); + that->_currentDpi = GetDpiForWindow(window); + } + else if (T* that = GetThisFromHandle(window)) + { + return that->MessageHandler(message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); + } + + virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept + { + switch (message) { + case WM_DPICHANGED: + { + return HandleDpiChange(_window, wparam, lparam); + } + + case WM_DESTROY: + { + PostQuitMessage(0); + return 0; + } + + case WM_SIZE: + { + UINT width = LOWORD(lparam); + UINT height = HIWORD(lparam); + + switch (wparam) + { + case SIZE_MAXIMIZED: + [[fallthrough]]; + case SIZE_RESTORED: + if (_minimized) + { + _minimized = false; + OnRestore(); + } + + // We always need to fire the resize event, even when we're transitioning from minimized. + // We might be transitioning directly from minimized to maximized, and we'll need + // to trigger any size-related content changes. + OnResize(width, height); + break; + case SIZE_MINIMIZED: + if (!_minimized) + { + _minimized = true; + OnMinimize(); + } + break; + default: + // do nothing. + break; + } + } + case CM_UPDATE_TITLE: + { + + SetWindowTextW(_window, _title.c_str()); + break; + } + } + + return DefWindowProc(_window, message, wparam, lparam); + } + + // DPI Change handler. on WM_DPICHANGE resize the window + LRESULT HandleDpiChange(const HWND hWnd, const WPARAM wParam, const LPARAM lParam) + { + _inDpiChange = true; + const HWND hWndStatic = GetWindow(hWnd, GW_CHILD); + if (hWndStatic != nullptr) + { + const UINT uDpi = HIWORD(wParam); + + // Resize the window + auto lprcNewScale = reinterpret_cast(lParam); + + SetWindowPos(hWnd, nullptr, lprcNewScale->left, lprcNewScale->top, + lprcNewScale->right - lprcNewScale->left, lprcNewScale->bottom - lprcNewScale->top, + SWP_NOZORDER | SWP_NOACTIVATE); + + _currentDpi = uDpi; + NewScale(uDpi); + } + _inDpiChange = false; + return 0; + } + + virtual void NewScale(UINT dpi) = 0; + + virtual void OnResize(const UINT width, const UINT height) = 0; + virtual void OnMinimize() = 0; + virtual void OnRestore() = 0; + + RECT GetWindowRect() const + { + RECT rc = { 0 }; + ::GetWindowRect(_window, &rc); + return rc; + } + + HWND GetHandle() noexcept + { + return _window; + }; + + // Method Description: + // - Sends a message to our message loop to update the title of the window. + // Arguments: + // - newTitle: a string to use as the new title of the window. + // Return Value: + // - + void UpdateTitle(std::wstring_view newTitle) + { + _title = newTitle; + PostMessageW(_window, CM_UPDATE_TITLE, 0, reinterpret_cast(nullptr)); + }; + +protected: + using base_type = BaseWindow; + HWND _window = nullptr; + + unsigned int _currentDpi = 0; + bool _inDpiChange = false; + + std::wstring _title = L""; + + bool _minimized = false; +}; + +template +inline BaseWindow::~BaseWindow() { } diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp new file mode 100644 index 000000000..45f73d4cf --- /dev/null +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "IslandWindow.h" +#include "../types/inc/Viewport.hpp" + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console::Types; + +#define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" + +IslandWindow::IslandWindow() noexcept : + _currentWidth{ 0 }, + _currentHeight{ 0 }, + _interopWindowHandle{ nullptr }, + _scale{ nullptr }, + _rootGrid{ nullptr }, + _source{ nullptr }, + _pfnCreateCallback{ nullptr } +{ +} + +IslandWindow::~IslandWindow() +{ +} + +// Method Description: +// - Create the actual window that we'll use for the application. +// Arguments: +// - +// Return Value: +// - +void IslandWindow::MakeWindow() noexcept +{ + WNDCLASS wc{}; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hInstance = reinterpret_cast(&__ImageBase); + wc.lpszClassName = XAML_HOSTING_WINDOW_CLASS_NAME; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = WndProc; + RegisterClass(&wc); + WINRT_ASSERT(!_window); + + // Create the window with the default size here - During the creation of the + // window, the system will give us a chance to set it's size in WM_CREATE. + // WM_CREATE will be handled synchronously, before CreateWindow returns. + WINRT_VERIFY(CreateWindow(wc.lpszClassName, + L"Windows Terminal", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + nullptr, nullptr, wc.hInstance, this)); + + WINRT_ASSERT(_window); + +} + +// Method Description: +// - Set a callback to be called when we process a WM_CREATE message. This gives +// the AppHost a chance to resize the window to the propoer size. +// Arguments: +// - pfn: a function to be called during the handling of WM_CREATE. It takes two +// parameters: +// * HWND: the HWND of the window that's being created. +// * RECT: The position on the screen that the system has proposed for our +// window. +// Return Value: +// - +void IslandWindow::SetCreateCallback(std::function pfn) noexcept +{ + _pfnCreateCallback = pfn; +} + +// Method Description: +// - Handles a WM_CREATE message. Calls our create callback, if one's been set. +// Arguments: +// - wParam: unused +// - lParam: the lParam of a WM_CREATE, which is a pointer to a CREATESTRUCTW +// Return Value: +// - +void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexcept +{ + // Get proposed window rect from create structure + CREATESTRUCTW* pcs = reinterpret_cast(lParam); + RECT rc; + rc.left = pcs->x; + rc.top = pcs->y; + rc.right = rc.left + pcs->cx; + rc.bottom = rc.top + pcs->cy; + + if (_pfnCreateCallback) + { + _pfnCreateCallback(_window, rc); + } + + ShowWindow(_window, SW_SHOW); + UpdateWindow(_window); +} + +void IslandWindow::Initialize() +{ + const bool initialized = (_interopWindowHandle != nullptr); + + _source = DesktopWindowXamlSource{}; + + auto interop = _source.as(); + winrt::check_hresult(interop->AttachToWindow(_window)); + + // stash the child interop handle so we can resize it when the main hwnd is resized + interop->get_WindowHandle(&_interopWindowHandle); + + if (!initialized) + { + _InitXamlContent(); + } + + _source.Content(_rootGrid); + + // Do a quick resize to force the island to paint + OnSize(); +} + +void IslandWindow::_InitXamlContent() +{ + // setup a root grid that will be used to apply DPI scaling + winrt::Windows::UI::Xaml::Media::ScaleTransform dpiScaleTransform; + winrt::Windows::UI::Xaml::Controls::Grid dpiAdjustmentGrid; + + const auto dpi = GetDpiForWindow(_window); + const double scale = double(dpi) / double(USER_DEFAULT_SCREEN_DPI); + + _rootGrid = dpiAdjustmentGrid; + _scale = dpiScaleTransform; + + _scale.ScaleX(scale); + _scale.ScaleY(scale); +} + + +void IslandWindow::OnSize() +{ + // update the interop window size + SetWindowPos(_interopWindowHandle, 0, 0, 0, _currentWidth, _currentHeight, SWP_SHOWWINDOW); + _rootGrid.Width(_currentWidth); + _rootGrid.Height(_currentHeight); +} + +LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +{ + switch (message) { + + case WM_CREATE: + { + _HandleCreateWindow(wparam, lparam); + return 0; + } + case WM_SETFOCUS: + { + if (_interopWindowHandle != nullptr) + { + // send focus to the child window + SetFocus(_interopWindowHandle); + return 0; // eat the message + } + } + } + + // TODO: handle messages here... + return base_type::MessageHandler(message, wparam, lparam); +} + +// Method Description: +// - Called when the DPI of this window changes. Updates the XAML content sizing to match the client area of our window. +// Arguments: +// - dpi: new DPI to use. The default is 96, as defined by USER_DEFAULT_SCREEN_DPI. +// Return Value: +// - +void IslandWindow::NewScale(UINT dpi) +{ + const double scaleFactor = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + + if (_scale != nullptr) + { + _scale.ScaleX(scaleFactor); + _scale.ScaleY(scaleFactor); + } + + ApplyCorrection(scaleFactor); +} + +// Method Description: +// - This method updates the padding that exists off the edge of the window to +// make sure to keep the XAML content size the same as the actual window size. +// Arguments: +// - scaleFactor: the DPI scaling multiplier to use. for a dpi of 96, this would +// be 1, for 144, this would be 1.5. +// Return Value: +// - +void IslandWindow::ApplyCorrection(double scaleFactor) +{ + // Get the dimensions of the XAML content grid. + const auto realWidth = _rootGrid.Width(); + const auto realHeight = _rootGrid.Height(); + + // Scale those dimensions by our dpi scaling. This is how big the XAML + // content thinks it should be. + const auto dpiAwareWidth = realWidth * scaleFactor; + const auto dpiAwareHeight = realHeight * scaleFactor; + + // Get the difference between what xaml thinks and the actual client area + // of our window. + const auto deltaX = dpiAwareWidth - realWidth; + const auto deltaY = dpiAwareHeight - realHeight; + + // correct for the scaling we applied above + const auto dividedDeltaX = deltaX / scaleFactor; + const auto dividedDeltaY = deltaY / scaleFactor; + + const double rightCorrection = dividedDeltaX; + const double bottomCorrection = dividedDeltaY; + + // Apply padding to the root grid, so that it's content is the same size as + // our actual window size. + // Without this, XAML content will seem to spill off the side/bottom of the window + _rootGrid.Padding(Xaml::ThicknessHelper::FromLengths(0, 0, rightCorrection, bottomCorrection)); + +} + +// Method Description: +// - Called when the window has been resized (or maximized) +// Arguments: +// - width: the new width of the window _in pixels_ +// - height: the new height of the window _in pixels_ +void IslandWindow::OnResize(const UINT width, const UINT height) +{ + _currentWidth = width; + _currentHeight = height; + if (nullptr != _rootGrid) + { + OnSize(); + ApplyCorrection(_scale.ScaleX()); + } +} + +// Method Description: +// - Called when the window is minimized to the taskbar. +void IslandWindow::OnMinimize() +{ + // TODO MSFT#21315817 Stop rendering island content when the app is minimized. +} + +// Method Description: +// - Called when the window is restored from having been minimized. +void IslandWindow::OnRestore() +{ + // TODO MSFT#21315817 Stop rendering island content when the app is minimized. +} + +void IslandWindow::SetRootContent(winrt::Windows::UI::Xaml::UIElement content) +{ + _rootGrid.Children().Clear(); + ApplyCorrection(_scale.ScaleX()); + _rootGrid.Children().Append(content); +} diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h new file mode 100644 index 000000000..eeaf0910e --- /dev/null +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "BaseWindow.h" +#include +#include + +class IslandWindow : public BaseWindow +{ +public: + IslandWindow() noexcept; + virtual ~IslandWindow() override; + + void MakeWindow() noexcept; + + virtual void OnSize(); + + virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + void ApplyCorrection(double scaleFactor); + void NewScale(UINT dpi) override; + void OnResize(const UINT width, const UINT height) override; + void OnMinimize() override; + void OnRestore() override; + void SetRootContent(winrt::Windows::UI::Xaml::UIElement content); + + virtual void Initialize(); + + void SetCreateCallback(std::function pfn) noexcept; + +protected: + unsigned int _currentWidth; + unsigned int _currentHeight; + + HWND _interopWindowHandle; + + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; + + winrt::Windows::UI::Xaml::Media::ScaleTransform _scale; + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; + + std::function _pfnCreateCallback; + + void _InitXamlContent(); + void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; +}; diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp new file mode 100644 index 000000000..81ce8b305 --- /dev/null +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -0,0 +1,649 @@ +/******************************************************** +* * +* Copyright (C) Microsoft. All rights reserved. * +* * +********************************************************/ +#include "pch.h" +#include "NonClientIslandWindow.h" + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Composition; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Hosting; +using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console::Types; + +constexpr int RECT_WIDTH(const RECT* const pRect) +{ + return pRect->right - pRect->left; +} +constexpr int RECT_HEIGHT(const RECT* const pRect) +{ + return pRect->bottom - pRect->top; +} + +NonClientIslandWindow::NonClientIslandWindow() noexcept : + IslandWindow{ }, + _nonClientInteropWindowHandle{ nullptr }, + _nonClientRootGrid{ nullptr }, + _nonClientSource{ nullptr }, + _maximizedMargins{ 0 }, + _isMaximized{ false } +{ +} + +NonClientIslandWindow::~NonClientIslandWindow() +{ +} + +// Method Description: +// - Used to initialize the XAML island for the non-client area. Also calls our +// base IslandWindow's Initialize, which will initialize the client XAML +// Island. +void NonClientIslandWindow::Initialize() +{ + _nonClientSource = DesktopWindowXamlSource{}; + auto interop = _nonClientSource.as(); + winrt::check_hresult(interop->AttachToWindow(_window)); + + // stash the child interop handle so we can resize it when the main hwnd is resized + interop->get_WindowHandle(&_nonClientInteropWindowHandle); + + _nonClientRootGrid = winrt::Windows::UI::Xaml::Controls::Grid{}; + + _nonClientSource.Content(_nonClientRootGrid); + + // Call the IslandWindow Initialize to set up the client xaml island + IslandWindow::Initialize(); +} + +// Method Description: +// - Sets the content of the non-client area of our window to the given XAML element. +// Arguments: +// - content: a XAML element to use as the content of the titlebar. +// Return Value: +// - +void NonClientIslandWindow::SetNonClientContent(winrt::Windows::UI::Xaml::UIElement content) +{ + _nonClientRootGrid.Children().Clear(); + ApplyCorrection(_scale.ScaleX()); + _nonClientRootGrid.Children().Append(content); +} + + +// Method Description: +// - Set the height we expect to reserve for the non-client content. +// Arguments: +// - contentHeight: the size in pixels we should use for the non-client content. +void NonClientIslandWindow::SetNonClientHeight(const int contentHeight) noexcept +{ + _titlebarUnscaledContentHeight = contentHeight; +} + +// Method Description: +// - Gets the size of the content area of the titlebar (the non-client area). +// This can be padded either by the margins from maximization (when the window +// is maximized) or the normal window borders. +// Return Value: +// - A Viewport representing the area of the window which should be the titlebar +// content, in window coordinates. +Viewport NonClientIslandWindow::GetTitlebarContentArea() const noexcept +{ + const auto dpi = GetDpiForWindow(_window); + const double scale = static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI); + + const auto titlebarContentHeight = _titlebarUnscaledContentHeight * scale; + const auto titlebarMarginRight = _titlebarMarginRight; + + auto titlebarWidth = _currentWidth - (_windowMarginSides + titlebarMarginRight); + // Adjust for maximized margins + titlebarWidth -= (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth); + + const auto titlebarHeight = titlebarContentHeight - (_titlebarMarginTop + _titlebarMarginBottom); + + + COORD titlebarOrigin = { static_cast(_windowMarginSides), + static_cast(_titlebarMarginTop) }; + + if (_isMaximized) + { + titlebarOrigin.X = static_cast(_maximizedMargins.cxLeftWidth); + titlebarOrigin.Y = static_cast(_maximizedMargins.cyTopHeight); + } + + return Viewport::FromDimensions(titlebarOrigin, + { static_cast(titlebarWidth), static_cast(titlebarHeight) }); +} + +// Method Description: +// - Gets the size of the client content area of the window. +// This can be padded either by the margins from maximization (when the window +// is maximized) or the normal window borders. +// Arguments: +// - +// Return Value: +// - A Viewport representing the area of the window which should be the client +// content, in window coordinates. +Viewport NonClientIslandWindow::GetClientContentArea() const noexcept +{ + MARGINS margins = GetFrameMargins(); + + COORD clientOrigin = { static_cast(margins.cxLeftWidth), + static_cast(margins.cyTopHeight) }; + + auto clientWidth = _currentWidth; + auto clientHeight = _currentHeight; + + // If we're maximized, we don't want to use the frame as our margins, + // instead we want to use the margins from the maximization. If we included + // the left&right sides of the frame in this calculation while maximized, + // you' have a few pixels of the window border on the sides while maximized, + // which most apps do not have. + if (_isMaximized) + { + clientWidth -= (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth); + clientHeight -= (margins.cyTopHeight + _maximizedMargins.cyBottomHeight); + clientOrigin.X = static_cast(_maximizedMargins.cxLeftWidth); + } + else + { + // Remove the left and right width of the frame from the client area + clientWidth -= (margins.cxLeftWidth + margins.cxRightWidth); + clientHeight -= (margins.cyTopHeight + margins.cyBottomHeight); + } + + // The top maximization margin is already included in the GetFrameMargins + // calcualtion. + + return Viewport::FromDimensions(clientOrigin, + { static_cast(clientWidth), static_cast(clientHeight) }); +} + +// Method Description: +// - called when the size of the window changes for any reason. Updates the +// sizes of our child Xaml Islands to match our new sizing. +void NonClientIslandWindow::OnSize() +{ + auto clientArea = GetClientContentArea(); + auto titlebarArea = GetTitlebarContentArea(); + + // update the interop window size + SetWindowPos(_interopWindowHandle, 0, + clientArea.Left(), + clientArea.Top(), + clientArea.Width(), + clientArea.Height(), + SWP_SHOWWINDOW); + + _rootGrid.Width(clientArea.Width()); + _rootGrid.Height(clientArea.Height()); + + // update the interop window size + SetWindowPos(_nonClientInteropWindowHandle, 0, + titlebarArea.Left(), + titlebarArea.Top(), + titlebarArea.Width(), + titlebarArea.Height(), + SWP_SHOWWINDOW); +} + +// Method Description: +// Hit test the frame for resizing and moving. +// Method Description: +// - Hit test the frame for resizing and moving. +// Arguments: +// - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates. +// Return Value: +// - one of the values from +// https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value +// corresponding to the area of the window that was hit +// NOTE: +// Largely taken from code on: +// https://docs.microsoft.com/en-us/windows/desktop/dwm/customframe +LRESULT NonClientIslandWindow::HitTestNCA(POINT ptMouse) const noexcept +{ + // Get the window rectangle. + RECT rcWindow = BaseWindow::GetWindowRect(); + + MARGINS margins = GetFrameMargins(); + + // Get the frame rectangle, adjusted for the style without a caption. + RECT rcFrame = { 0 }; + auto expectedStyle = WS_OVERLAPPEDWINDOW; + WI_ClearAllFlags(expectedStyle, WS_CAPTION); + AdjustWindowRectEx(&rcFrame, expectedStyle, false, 0); + + // Determine if the hit test is for resizing. Default middle (1,1). + unsigned short uRow = 1; + unsigned short uCol = 1; + bool fOnResizeBorder = false; + + // Determine if the point is at the top or bottom of the window. + if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + margins.cyTopHeight) + { + fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top)); + uRow = 0; + } + else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - margins.cyBottomHeight) + { + uRow = 2; + } + + // Determine if the point is at the left or right of the window. + if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + margins.cxLeftWidth) + { + uCol = 0; // left side + } + else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - margins.cxRightWidth) + { + uCol = 2; // right side + } + + // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) + LRESULT hitTests[3][3] = + { + { HTTOPLEFT, fOnResizeBorder ? HTTOP : HTCAPTION, HTTOPRIGHT }, + { HTLEFT, HTNOWHERE, HTRIGHT }, + { HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT }, + }; + + return hitTests[uRow][uCol]; +} + +// Method Description: +// - Get the size of the borders we want to use. The sides and bottom will just +// be big enough for resizing, but the top will be as big as we need for the +// non-client content. +// Return Value: +// - A MARGINS struct containing the border dimensions we want. +MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept +{ + const auto titlebarView = GetTitlebarContentArea(); + + MARGINS margins{0}; + margins.cxLeftWidth = _windowMarginSides; + margins.cxRightWidth = _windowMarginSides; + margins.cyBottomHeight = _windowMarginBottom; + margins.cyTopHeight = titlebarView.BottomExclusive(); + + return margins; +} + +// Method Description: +// - Updates the borders of our window frame, using DwmExtendFrameIntoClientArea. +// Arguments: +// - +// Return Value: +// - the HRESULT returned by DwmExtendFrameIntoClientArea. +HRESULT NonClientIslandWindow::_UpdateFrameMargins() const noexcept +{ + // Get the size of the borders we want to use. The sides and bottom will + // just be big enough for resizing, but the top will be as big as we need + // for the non-client content. + MARGINS margins = GetFrameMargins(); + // Extend the frame into the client area. + return DwmExtendFrameIntoClientArea(_window, &margins); +} + +// Routine Description: +// - Gets the maximum possible window rectangle in pixels. Based on the monitor +// the window is on or the primary monitor if no window exists yet. +// Arguments: +// - prcSuggested - If we were given a suggested rectangle for where the window +// is going, we can pass it in here to find out the max size +// on that monitor. +// - If this value is zero and we had a valid window handle, +// we'll use that instead. Otherwise the value of 0 will make +// us use the primary monitor. +// - pDpiSuggested - The dpi that matches the suggested rect. We will attempt to +// compute this during the function, but if we fail for some +// reason, the original value passed in will be left untouched. +// Return Value: +// - RECT containing the left, right, top, and bottom positions from the desktop +// origin in pixels. Measures the outer edges of the potential window. +// NOTE: +// Heavily taken from WindowMetrics::GetMaxWindowRectInPixels in conhost. +RECT NonClientIslandWindow::GetMaxWindowRectInPixels(const RECT * const prcSuggested, + _Out_opt_ UINT * pDpiSuggested) +{ + // prepare rectangle + RECT rc = *prcSuggested; + + // use zero rect to compare. + RECT rcZero; + SetRectEmpty(&rcZero); + + // First get the monitor pointer from either the active window or the default location (0,0,0,0) + HMONITOR hMonitor = nullptr; + + // NOTE: We must use the nearest monitor because sometimes the system moves the window around into strange spots while performing snap and Win+D operations. + // Those operations won't work correctly if we use MONITOR_DEFAULTTOPRIMARY. + if (!EqualRect(&rc, &rcZero)) + { + // For invalid window handles or when we were passed a non-zero suggestion rectangle, get the monitor from the rect. + hMonitor = MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST); + } + else + { + // Otherwise, get the monitor from the window handle. + hMonitor = MonitorFromWindow(_window, MONITOR_DEFAULTTONEAREST); + } + + // If for whatever reason there is no monitor, we're going to give back whatever we got since we can't figure anything out. + // We won't adjust the DPI either. That's OK. DPI doesn't make much sense with no display. + if (nullptr == hMonitor) + { + return rc; + } + + // Now obtain the monitor pixel dimensions + MONITORINFO MonitorInfo = { 0 }; + MonitorInfo.cbSize = sizeof(MONITORINFO); + + GetMonitorInfoW(hMonitor, &MonitorInfo); + + // We have to make a correction to the work area. If we actually consume the entire work area (by maximizing the window) + // The window manager will render the borders off-screen. + // We need to pad the work rectangle with the border dimensions to represent the actual max outer edges of the window rect. + WINDOWINFO wi = { 0 }; + wi.cbSize = sizeof(WINDOWINFO); + GetWindowInfo(_window, &wi); + + // In non-full screen, we want to only use the work area (avoiding the task bar space) + rc = MonitorInfo.rcWork; + + if (pDpiSuggested != nullptr) + { + UINT monitorDpiX; + UINT monitorDpiY; + if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &monitorDpiX, &monitorDpiY))) + { + *pDpiSuggested = monitorDpiX; + } + else + { + *pDpiSuggested = GetDpiForWindow(_window); + } + } + + return rc; +} + +// Method Description: +// - Handle window messages from the message loop. +// Arguments: +// - message: A window message ID identifying the message. +// - wParam: The contents of this parameter depend on the value of the message parameter. +// - lParam: The contents of this parameter depend on the value of the message parameter. +// Return Value: +// - The return value is the result of the message processing and depends on the +// message sent. +LRESULT NonClientIslandWindow::MessageHandler(UINT const message, + WPARAM const wParam, + LPARAM const lParam) noexcept +{ + LRESULT lRet = 0; + + // First call DwmDefWindowProc. This might handle things like the + // min/max/close buttons for us. + const bool dwmHandledMessage = DwmDefWindowProc(_window, message, wParam, lParam, &lRet); + + switch (message) + { + case WM_ACTIVATE: + { + _HandleActivateWindow(); + break; + } + case WM_NCCALCSIZE: + { + if (wParam == false) + { + return 0; + } + // Handle the non-client size message. + if (wParam == TRUE && lParam) + { + // Calculate new NCCALCSIZE_PARAMS based on custom NCA inset. + NCCALCSIZE_PARAMS *pncsp = reinterpret_cast(lParam); + + pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0; + pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0; + pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0; + pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0; + + return 0; + } + break; + } + case WM_NCHITTEST: + { + if (dwmHandledMessage) + { + return lRet; + } + + // Handle hit testing in the NCA if not handled by DwmDefWindowProc. + if (lRet == 0) + { + lRet = HitTestNCA({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}); + + if (lRet != HTNOWHERE) + { + return lRet; + } + } + break; + } + case WM_WINDOWPOSCHANGING: + { + // Enforce maximum size here instead of WM_GETMINMAXINFO. If we return + // it in WM_GETMINMAXINFO, then it will be enforced when snapping across + // DPI boundaries (bad.) + LPWINDOWPOS lpwpos = reinterpret_cast(lParam); + if (lpwpos == nullptr) + { + break; + } + if (_HandleWindowPosChanging(lpwpos)) + { + return 0; + } + else + { + break; + } + } + + } + + return IslandWindow::MessageHandler(message, wParam, lParam); +} + +// Method Description: +// - Handle a WM_ACTIVATE message. Called during the creation of the window, and +// used as an opprotunity to get the dimensions of the caption buttons (the +// min, max, close buttons). We'll use these dimensions to help size the +// non-client area of the window. +void NonClientIslandWindow::_HandleActivateWindow() +{ + const auto dpi = GetDpiForWindow(_window); + + // Use DwmGetWindowAttribute to get the complete size of the caption buttons. + RECT captionSize{ 0 }; + THROW_IF_FAILED(DwmGetWindowAttribute(_window, DWMWA_CAPTION_BUTTON_BOUNDS, &captionSize, sizeof(RECT))); + + // Divide by 3 to get the width of a single button + // Multiply by 4 to reserve the space of one button as the "grab handle" + _titlebarMarginRight = MulDiv(RECT_WIDTH(&captionSize), 4, 3); + + // _titlebarUnscaledContentHeight is set with SetNonClientHeight by the app + // hosting us. + + _UpdateFrameMargins(); +} + +// Method Description: +// - Handle a WM_WINDOWPOSCHANGING message. When the window is changing, or the +// dpi is changing, this handler is triggered to give us a chance to adjust +// the window size and position manually. We use this handler during a maxiize +// to figure out by how much the window will overhang the edges of the +// monitor, and set up some padding to adjust for that. +// Arguments: +// - windowPos: A pointer to a proposed window location and size. Should we wish +// to manually position the window, we could change the values of this struct. +// Return Value: +// - true if we handled this message, false otherwise. If we return false, the +// message should instead be handled by DefWindowProc +// Note: +// Largely taken from the conhost WM_WINDOWPOSCHANGING handler. +bool NonClientIslandWindow::_HandleWindowPosChanging(WINDOWPOS* const windowPos) +{ + // We only need to apply restrictions if the size is changing. + if (WI_IsFlagSet(windowPos->flags, SWP_NOSIZE)) + { + return false; + } + + // Figure out the suggested dimensions + RECT rcSuggested; + rcSuggested.left = windowPos->x; + rcSuggested.top = windowPos->y; + rcSuggested.right = rcSuggested.left + windowPos->cx; + rcSuggested.bottom = rcSuggested.top + windowPos->cy; + SIZE szSuggested; + szSuggested.cx = RECT_WIDTH(&rcSuggested); + szSuggested.cy = RECT_HEIGHT(&rcSuggested); + + // Figure out the current dimensions for comparison. + RECT rcCurrent = GetWindowRect(); + + // Determine whether we're being resized by someone dragging the edge or + // completely moved around. + bool fIsEdgeResize = false; + { + // We can only be edge resizing if our existing rectangle wasn't empty. + // If it was empty, we're doing the initial create. + if (!IsRectEmpty(&rcCurrent)) + { + // If one or two sides are changing, we're being edge resized. + unsigned int cSidesChanging = 0; + if (rcCurrent.left != rcSuggested.left) + { + cSidesChanging++; + } + if (rcCurrent.right != rcSuggested.right) + { + cSidesChanging++; + } + if (rcCurrent.top != rcSuggested.top) + { + cSidesChanging++; + } + if (rcCurrent.bottom != rcSuggested.bottom) + { + cSidesChanging++; + } + + if (cSidesChanging == 1 || cSidesChanging == 2) + { + fIsEdgeResize = true; + } + } + } + + const auto windowStyle = GetWindowStyle(_window); + const auto isMaximized = WI_IsFlagSet(windowStyle, WS_MAXIMIZE); + + // If we're about to maximize the window, determine how much we're about to + // overhang by, and adjust for that. + // We need to do this because maximized windows will typically overhang the + // actual monitor bounds by roughly the size of the old "thick: window + // borders. For normal windows, this is fine, but because we're using + // DwmExtendFrameIntoClientArea, that means some of our client content will + // now overhang, and get cut off. + if (isMaximized) + { + // Find the related monitor, the maximum pixel size, + // and the dpi for the suggested rect. + UINT dpiOfMaximum; + RECT rcMaximum; + + if (fIsEdgeResize) + { + // If someone's dragging from the edge to resize in one direction, + // we want to make sure we never grow past the current monitor. + rcMaximum = GetMaxWindowRectInPixels(&rcCurrent, &dpiOfMaximum); + } + else + { + // In other circumstances, assume we're snapping around or some + // other jump (TS). Just do whatever we're told using the new + // suggestion as the restriction monitor. + rcMaximum = GetMaxWindowRectInPixels(&rcSuggested, &dpiOfMaximum); + } + + const auto suggestedWidth = szSuggested.cx; + const auto suggestedHeight = szSuggested.cy; + + const auto maxWidth = RECT_WIDTH(&rcMaximum); + const auto maxHeight = RECT_HEIGHT(&rcMaximum); + + // Only apply the maximum size restriction if the current DPI matches + // the DPI of the maximum rect. This keeps us from applying the wrong + // restriction if the monitor we're moving to has a different DPI but + // we've yet to get notified of that DPI change. If we do apply it, then + // we'll restrict the console window BEFORE its been resized for the DPI + // change, so we're likely to shrink the window too much or worse yet, + // keep it from moving entirely. We'll get a WM_DPICHANGED, resize the + // window, and then process the restriction in a few window messages. + if ( ((int)dpiOfMaximum == _currentDpi) && + ( (suggestedWidth > maxWidth) || + (suggestedHeight > maxHeight) ) ) + { + auto offset = 0; + // Determine which side of the window to use for the offset + // calculation. If the taskbar is on the left or top of the screen, + // then the x or y coordinate of the work rect might not be 0. + // Check both, and use whichever is 0. + if (rcMaximum.left == 0) + { + offset = windowPos->x; + } + else if (rcMaximum.top == 0) + { + offset = windowPos->y; + } + const auto offsetX = offset; + const auto offsetY = offset; + + _maximizedMargins.cxRightWidth = -offset; + _maximizedMargins.cxLeftWidth = -offset; + + _maximizedMargins.cyTopHeight = -offset; + _maximizedMargins.cyBottomHeight = -offset; + + _isMaximized = true; + _UpdateFrameMargins(); + } + } + else + { + // Clear our maximization state + _maximizedMargins = {0}; + + // Immediately after resoring down, don't update our frame margins. If + // you do this here, then a small gap will appear between the titlebar + // and the content, until the window is moved. However, we do need to + // keep this here _in general_ for dragging across DPI boundaries. + if (!_isMaximized) + { + _UpdateFrameMargins(); + } + + _isMaximized = false; + } + return true; +} diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h new file mode 100644 index 000000000..0457aad37 --- /dev/null +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -0,0 +1,72 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- NonClientIslandWindow.h + +Abstract: +- This class represents a window hosting two XAML Islands. One is in the client + area of the window, as it is in the base IslandWindow class. The second is in + the titlebar of the window, in the "non-client" area of the window. This + enables an app to place xaml content in the titlebar. +- Placing content in the frame is enabled with DwmExtendFrameIntoClientArea. See + https://docs.microsoft.com/en-us/windows/desktop/dwm/customframe + for information on how this is done. + +Author(s): + Mike Griese (migrie) April-2019 +--*/ + +#include "pch.h" +#include "IslandWindow.h" +#include "../../types/inc/Viewport.hpp" +#include +#include + +class NonClientIslandWindow : public IslandWindow +{ +public: + NonClientIslandWindow() noexcept; + virtual ~NonClientIslandWindow() override; + + virtual void OnSize() override; + + virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; + + void SetNonClientContent(winrt::Windows::UI::Xaml::UIElement content); + + virtual void Initialize() override; + + MARGINS GetFrameMargins() const noexcept; + + void SetNonClientHeight(const int contentHeight) noexcept; + +private: + + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _nonClientSource; + + HWND _nonClientInteropWindowHandle; + winrt::Windows::UI::Xaml::Controls::Grid _nonClientRootGrid; + + int _windowMarginBottom = 2; + int _windowMarginSides = 2; + int _titlebarMarginRight = 0; + int _titlebarMarginTop = 2; + int _titlebarMarginBottom = 0; + + int _titlebarUnscaledContentHeight = 0; + + ::Microsoft::Console::Types::Viewport GetTitlebarContentArea() const noexcept; + ::Microsoft::Console::Types::Viewport GetClientContentArea() const noexcept; + + MARGINS _maximizedMargins; + bool _isMaximized; + + LRESULT HitTestNCA(POINT ptMouse) const noexcept; + HRESULT _UpdateFrameMargins() const noexcept; + + void _HandleActivateWindow(); + bool _HandleWindowPosChanging(WINDOWPOS* const windowPos); + + RECT GetMaxWindowRectInPixels(const RECT * const prcSuggested, _Out_opt_ UINT * pDpiSuggested); +}; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.def b/src/cascadia/WindowsTerminal/WindowsTerminal.def new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.def @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.manifest b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest new file mode 100644 index 000000000..92c5544f4 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.manifest @@ -0,0 +1,129 @@ + + + + + + + + + + + PerMonitorV2 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.rc b/src/cascadia/WindowsTerminal/WindowsTerminal.rc new file mode 100644 index 000000000..4581915a6 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.rc @@ -0,0 +1,66 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj new file mode 100644 index 000000000..da62d2744 --- /dev/null +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -0,0 +1,122 @@ + + + + + Application + Windows + false + Windows Store + + + + 15.0 + {CA5CAD1A-1754-4A9D-93D7-857A9D17CB1B} + Win32Proj + WindowsTerminal + WindowsTerminal + WindowsTerminal + + + 10.0.17763.0 + + + + true + + + + + $(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\Console;$(OpenConsoleDir)\dep\Win32K;$(OpenConsoleDir)\dep\gsl\include;%(AdditionalIncludeDirectories); + + + + "$(OpenConsoleDir)src\cascadia\TerminalCore\lib\Generated Files";%(AdditionalIncludeDirectories); + + + dwmapi.lib;Shcore.lib;%(AdditionalDependencies) + + + + true + true + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + true + PreserveNewest + + + + + true + PreserveNewest + + + + + + + + + + + + + + + + + WindowsLocalDebugger + + + + true + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + diff --git a/src/cascadia/WindowsTerminal/main.cpp b/src/cascadia/WindowsTerminal/main.cpp new file mode 100644 index 000000000..3b6dff00d --- /dev/null +++ b/src/cascadia/WindowsTerminal/main.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "AppHost.h" + +using namespace winrt; +using namespace Windows::UI; +using namespace Windows::UI::Composition; +using namespace Windows::UI::Xaml::Hosting; +using namespace Windows::Foundation::Numerics; + +int __stdcall wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) +{ + // Make sure to call this so we get WM_POINTER messages. + EnableMouseInPointer(true); + + // Create the AppHost object, which will create both the window and the + // Terminal App. This MUST BE constructed before the Xaml manager as TermApp + // provides an implementation of Windows.UI.Xaml.Application. + AppHost host; + + // !!! LOAD BEARING !!! + // This is _magic_. Do the initial loading of our settings *BEFORE* we + // initialize our COM apartment type. This is because the Windows.Storage + // APIs require a MTA. However, other api's (notably the clipboard ones) + // require that the main thread is an STA. During startup, we don't yet have + // a dispatcher to background any async work, and we don't want to - we want + // to load the settings synchronously. Fortunately, WinRT will assume we're + // in a MTA until we explicitly call init_apartment. We can only call + // init_apartment _once_, so we'll do the settings loading first, in the + // implicit MTA, then set our apartment type to STA. The AppHost ctor will + // load the settings for us, as it constructs the window. + // This works because Kenny Kerr said it would, and he wrote cpp/winrt, so he knows. + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Initialize the Xaml Hosting Manager + auto manager = Windows::UI::Xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); + + // Initialize the xaml content. This must be called AFTER the + // WindowsXamlManager is initalized. + host.Initialize(); + + MSG message; + + while (GetMessage(&message, nullptr, 0, 0)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } + return 0; +} diff --git a/src/cascadia/WindowsTerminal/packages.config b/src/cascadia/WindowsTerminal/packages.config new file mode 100644 index 000000000..cbbd56478 --- /dev/null +++ b/src/cascadia/WindowsTerminal/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/src/cascadia/WindowsTerminal/pch.cpp b/src/cascadia/WindowsTerminal/pch.cpp new file mode 100644 index 000000000..398a99f66 --- /dev/null +++ b/src/cascadia/WindowsTerminal/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" diff --git a/src/cascadia/WindowsTerminal/pch.h b/src/cascadia/WindowsTerminal/pch.h new file mode 100644 index 000000000..a8016548d --- /dev/null +++ b/src/cascadia/WindowsTerminal/pch.h @@ -0,0 +1,50 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- pch.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +// Ignore checked iterators warning from VC compiler. +#define _SCL_SECURE_NO_WARNINGS + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +#define WIN32_LEAN_AND_MEAN +#include + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +#include +#include +#include +#include + +#include "../inc/LibraryIncludes.h" + +// This is inexplicable, but for whatever reason, cppwinrt conflicts with the +// SDK definition of this function, so the only fix is to undef it. +// from WinBase.h +// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime +#ifdef GetCurrentTime +#undef GetCurrentTime +#endif +// Needed just for XamlIslands to work at all: +#include +#include +#include +#include + +// Additional headers for various xaml features. We need: +// * Controls for grid +// * Media for ScaleTransform +#include +#include diff --git a/src/cascadia/WindowsTerminal/resource.h b/src/cascadia/WindowsTerminal/resource.h new file mode 100644 index 000000000..34124b61e --- /dev/null +++ b/src/cascadia/WindowsTerminal/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Cascadia.EXE.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 103 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/cascadia/inc/cppwinrt_utils.h b/src/cascadia/inc/cppwinrt_utils.h new file mode 100644 index 000000000..a0197a364 --- /dev/null +++ b/src/cascadia/inc/cppwinrt_utils.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/*++ +Module Name: +- cppwinrt_utils.h + +Abstract: +- This module is used for winrt event declarations/definitions + +Author(s): +- Carlos Zamora (CaZamor) 23-Apr-2019 + +Revision History: +- N/A +--*/ + +#pragma once + +// This is a helper macro to make declaring events easier. +// This will declare the event handler and the methods for adding and removing a +// handler callback from the event +#define DECLARE_EVENT(name, eventHandler, args) \ + public: \ + winrt::event_token name(args const& handler); \ + void name(winrt::event_token const& token) noexcept; \ + private: \ + winrt::event eventHandler; + +// This is a helper macro for defining the body of events. +// Winrt events need a method for adding a callback to the event and removing +// the callback. This macro will define them both for you, because they +// don't really vary from event to event. +#define DEFINE_EVENT(className, name, eventHandler, args) \ + winrt::event_token className::name(args const& handler) { return eventHandler.add(handler); } \ + void className::name(winrt::event_token const& token) noexcept { eventHandler.remove(token); } + +// This is a helper macro to make declaring events easier. +// This will declare the event handler and the methods for adding and removing a +// handler callback from the event. +// Use this if you have a Windows.Foundation.TypedEventHandler +#define DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(name, eventHandler, sender, args) \ + public: \ + winrt::event_token name(Windows::Foundation::TypedEventHandler const& handler); \ + void name(winrt::event_token const& token) noexcept; \ + private: \ + winrt::event> eventHandler; + +// This is a helper macro for defining the body of events. +// Winrt events need a method for adding a callback to the event and removing +// the callback. This macro will define them both for you, because they +// don't really vary from event to event. +// Use this if you have a Windows.Foundation.TypedEventHandler +#define DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(className, name, eventHandler, sender, args) \ + winrt::event_token className::name(Windows::Foundation::TypedEventHandler const& handler) { return eventHandler.add(handler); } \ + void className::name(winrt::event_token const& token) noexcept { eventHandler.remove(token); } \ No newline at end of file diff --git a/src/common.build.dll.props b/src/common.build.dll.props new file mode 100644 index 000000000..a24b8212d --- /dev/null +++ b/src/common.build.dll.props @@ -0,0 +1,12 @@ + + + + + DynamicLibrary + + + + _USRDLL;%(PreprocessorDefinitions) + + + diff --git a/src/common.build.exe.or.dll.props b/src/common.build.exe.or.dll.props new file mode 100644 index 000000000..04b586666 --- /dev/null +++ b/src/common.build.exe.or.dll.props @@ -0,0 +1,25 @@ + + + + + _WINDOWS;%(PreprocessorDefinitions) + + + $(OutDir)$(TargetName)FullPDB.pdb + dwrite.lib;dxgi.lib;d2d1.lib;d3d11.lib;shcore.lib;uxtheme.lib;dwmapi.lib;winmm.lib;pathcch.lib;propsys.lib;uiautomationcore.lib;Shlwapi.lib;ntdll.lib;%(AdditionalDependencies) + + gdi32.lib;advapi32.lib;shell32.lib;%(AdditionalDependencies) + + + diff --git a/src/common.build.exe.props b/src/common.build.exe.props new file mode 100644 index 000000000..c6b693df2 --- /dev/null +++ b/src/common.build.exe.props @@ -0,0 +1,7 @@ + + + + + Application + + diff --git a/src/common.build.lib.props b/src/common.build.lib.props new file mode 100644 index 000000000..8dbef91da --- /dev/null +++ b/src/common.build.lib.props @@ -0,0 +1,11 @@ + + + + StaticLibrary + + + + _LIB;%(PreprocessorDefinitions) + + + diff --git a/src/common.build.post.props b/src/common.build.post.props new file mode 100644 index 000000000..00a81388c --- /dev/null +++ b/src/common.build.post.props @@ -0,0 +1,28 @@ + + + + + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(ProjectName)\ + + + + + + + + + ProgramDatabase + + + + + + + diff --git a/src/common.build.pre.props b/src/common.build.pre.props new file mode 100644 index 000000000..1591634fe --- /dev/null +++ b/src/common.build.pre.props @@ -0,0 +1,127 @@ + + + + + + AuditMode + Win32 + + + Debug + Win32 + + + Release + Win32 + + + AuditMode + x64 + + + Debug + x64 + + + Release + x64 + + + AuditMode + ARM64 + + + Debug + ARM64 + + + Release + ARM64 + + + + 10.0.17763.0 + + + v141 + Unicode + false + + + true + + + false + true + + + + + Use + Level4 + 4189;4100;4242;4389;4244 + + true + + 4201;4312;4467 + EXTERNAL_BUILD;%(PreprocessorDefinitions) + true + precomp.h + $(IntDir)$(TargetName).pdb + ProgramDatabase + $(SolutionDir)\src\inc;$(SolutionDir)\dep;$(SolutionDir)\dep\Console;$(SolutionDir)\dep\Win32K;$(SolutionDir)\dep\gsl\include;$(SolutionDir)\dep\wil\include;%(AdditionalIncludeDirectories); + true + false + false + /std:c++17 %(AdditionalOptions) + + + EXTERNAL_BUILD;_UNICODE;UNICODE;%(PreprocessorDefinitions) + + + Windows + true + + + + + + Disabled + _DEBUG;DBG;%(PreprocessorDefinitions) + + + true + + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + false + + + true + true + + + + + + WIN32;%(PreprocessorDefinitions) + + + + + $(SolutionDir)\src\StaticAnalysis.ruleset + true + true + + + + true + + + diff --git a/src/common.build.tests.props b/src/common.build.tests.props new file mode 100644 index 000000000..9bd3921a7 --- /dev/null +++ b/src/common.build.tests.props @@ -0,0 +1,15 @@ + + + + + UNIT_TESTING;%(PreprocessorDefinitions) + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + diff --git a/src/cppwinrt.build.post.props b/src/cppwinrt.build.post.props new file mode 100644 index 000000000..2c8530f3f --- /dev/null +++ b/src/cppwinrt.build.post.props @@ -0,0 +1,76 @@ + + + + + + $(OpenConsoleDir)\bin\$(Platform)\$(Configuration)\$(ProjectName)\ + $(OpenConsoleDir)\bin\$(Platform)\$(Configuration)\$(ProjectName)\ + $(OpenConsoleDir)\obj\$(Platform)\$(Configuration)\$(ProjectName)\ + + + + + + + echo OutDir=$(OutDir) + (echo f | xcopy /y $(OutDir)$(ProjectName).dll $(OpenConsoleDir)$(Platform)\$(Configuration)\$(ProjectName).dll ) + (echo f | xcopy /y $(OutDir)$(ProjectName).pdb $(OpenConsoleDir)$(Platform)\$(Configuration)\$(ProjectName).pdb ) + + + + + echo OutDir=$(OutDir) + (echo f | xcopy /y $(OutDir)$(ProjectName).dll $(OpenConsoleDir)$(Configuration)\$(ProjectName).dll ) + (echo f | xcopy /y $(OutDir)$(ProjectName).pdb $(OpenConsoleDir)$(Configuration)\$(ProjectName).pdb ) + + + + + + + + + + if not exist $(OpenConsoleDir)$(Platform)\$(Configuration)\$(ProjectName) mkdir $(OpenConsoleDir)$(Platform)\$(Configuration)\$(ProjectName) + + + + + + + if not exist $(OpenConsoleDir)$(Configuration)\$(ProjectName) mkdir $(OpenConsoleDir)\$(Configuration)\$(ProjectName) + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + diff --git a/src/cppwinrt.build.pre.props b/src/cppwinrt.build.pre.props new file mode 100644 index 000000000..b2777e6e1 --- /dev/null +++ b/src/cppwinrt.build.pre.props @@ -0,0 +1,135 @@ + + + + + + + + + + + 10.0.17763.0 + 10.0.17763.0 + true + en-US + 14.0 + 10.0 + + + + + true + true + Windows Store + + + + + <_NoWinAPIFamilyApp>true + <_VC_Target_Library_Platform>Desktop + false + + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + + DynamicLibrary + v141 + Unicode + false + + + true + true + + + false + true + false + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /permissive- /bigobj /Zc:twoPhase- /std:c++17 + 28204 + _WINRT_DLL;%(PreprocessorDefinitions) + $(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories) + + + Console + true + $(ProjectName).def + + + + + + Disabled + + + + + + + true + + + true + true + + + + + + $(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\Console;$(OpenConsoleDir)\dep\gsl\include;$(OpenConsoleDir)\dep\wil\include;%(AdditionalIncludeDirectories) + + + WindowsApp.lib;%(AdditionalDependencies) + + + + + + diff --git a/src/dirs b/src/dirs new file mode 100644 index 000000000..ededb1c15 --- /dev/null +++ b/src/dirs @@ -0,0 +1,13 @@ +DIRS=\ + buffer \ + interactivity \ + host \ + propsheet \ + propslib \ + renderer \ + server \ + terminal \ + testlist \ + tools \ + tsf \ + types \ diff --git a/src/host/ApiRoutines.h b/src/host/ApiRoutines.h new file mode 100644 index 000000000..2aff07e01 --- /dev/null +++ b/src/host/ApiRoutines.h @@ -0,0 +1,442 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApiRoutines.h + +Abstract: +- This file defines the interface to respond to all API calls. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp, getset.cpp, directio.cpp, stream.cpp +--*/ + +#pragma once + +#include "..\server\IApiRoutines.h" + +class ApiRoutines : public IApiRoutines +{ +public: +#pragma region ObjectManagement + /*HRESULT CreateInitialObjects(_Out_ InputBuffer** const ppInputObject, + _Out_ SCREEN_INFORMATION** const ppOutputObject); + */ + +#pragma endregion + +#pragma region L1 + void GetConsoleInputCodePageImpl(ULONG& codepage) noexcept override; + + void GetConsoleOutputCodePageImpl(ULONG& codepage) noexcept override; + + void GetConsoleInputModeImpl(InputBuffer& context, + ULONG& mode) noexcept override; + + void GetConsoleOutputModeImpl(SCREEN_INFORMATION& context, + ULONG& mode) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleInputModeImpl(InputBuffer& context, + const ULONG mode) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleOutputModeImpl(SCREEN_INFORMATION& context, + const ULONG Mode) noexcept override; + + [[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 ReadConsoleAImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleWImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, + const std::string_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleWImpl(IConsoleOutputObject& context, + const std::wstring_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept override; + +#pragma region ThreadCreationInfo + [[nodiscard]] + HRESULT GetConsoleLangIdImpl(LANGID& langId) noexcept override; +#pragma endregion + +#pragma endregion + +#pragma region L2 + + [[nodiscard]] + HRESULT FillConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const WORD attribute, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept override; + + [[nodiscard]] + HRESULT FillConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const char character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept override; + + [[nodiscard]] + HRESULT FillConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const wchar_t character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept override; + + //// Process based. Restrict in protocol side? + //HRESULT GenerateConsoleCtrlEventImpl(const ULONG ProcessGroupFilter, + // const ULONG ControlEvent); + + void SetConsoleActiveScreenBufferImpl(SCREEN_INFORMATION& newContext) noexcept override; + + void FlushConsoleInputBuffer(InputBuffer& context) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleInputCodePageImpl(const ULONG codepage) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleOutputCodePageImpl(const ULONG codepage) noexcept override; + + void GetConsoleCursorInfoImpl(const SCREEN_INFORMATION& context, + ULONG& size, + bool& isVisible) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleCursorInfoImpl(SCREEN_INFORMATION& context, + const ULONG size, + const bool isVisible) noexcept override; + + //// driver will pare down for non-Ex method + void GetConsoleScreenBufferInfoExImpl(const SCREEN_INFORMATION& context, + CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleScreenBufferInfoExImpl(SCREEN_INFORMATION& context, + const CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleScreenBufferSizeImpl(SCREEN_INFORMATION& context, + const COORD size) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleCursorPositionImpl(SCREEN_INFORMATION& context, + const COORD position) noexcept override; + + void GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& context, + COORD& size) noexcept override; + + [[nodiscard]] + HRESULT ScrollConsoleScreenBufferAImpl(SCREEN_INFORMATION& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const char fillCharacter, + const WORD fillAttribute) noexcept override; + + [[nodiscard]] + HRESULT ScrollConsoleScreenBufferWImpl(SCREEN_INFORMATION& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const wchar_t fillCharacter, + const WORD fillAttribute) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleTextAttributeImpl(SCREEN_INFORMATION& context, + const WORD attribute) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleWindowInfoImpl(SCREEN_INFORMATION& context, + const bool isAbsolute, + const SMALL_RECT& windowRect) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleInputAImpl(InputBuffer& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleInputWImpl(InputBuffer& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& requestRectangle, + Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& requestRectangle, + Microsoft::Console::Types::Viewport& writtenRectangle) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const std::basic_string_view attrs, + const COORD target, + size_t& used) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const std::string_view text, + const COORD target, + size_t& used) noexcept override; + + [[nodiscard]] + HRESULT WriteConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const std::wstring_view text, + const COORD target, + size_t& used) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept override; + + [[nodiscard]] + HRESULT ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleOriginalTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleOriginalTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleTitleAImpl(const std::string_view title) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleTitleWImpl(const std::wstring_view title) noexcept override; + +#pragma endregion + +#pragma region L3 + void GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleFontSizeImpl(const SCREEN_INFORMATION& context, + const DWORD index, + COORD& size) noexcept override; + + //// driver will pare down for non-Ex method + [[nodiscard]] + HRESULT GetCurrentConsoleFontExImpl(const SCREEN_INFORMATION& context, + const bool isForMaximumWindowSize, + CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleDisplayModeImpl(SCREEN_INFORMATION& context, + const ULONG flags, + COORD& newSize) noexcept override; + + void GetConsoleDisplayModeImpl(ULONG& flags) noexcept override; + + [[nodiscard]] + HRESULT AddConsoleAliasAImpl(const std::string_view source, + const std::string_view target, + const std::string_view exeName) noexcept override; + + [[nodiscard]] + HRESULT AddConsoleAliasWImpl(const std::wstring_view source, + const std::wstring_view target, + const std::wstring_view exeName) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasAImpl(const std::string_view source, + gsl::span target, + size_t& written, + const std::string_view exeName) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasWImpl(const std::wstring_view source, + gsl::span target, + size_t& written, + const std::wstring_view exeName) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasesLengthAImpl(const std::string_view exeName, + size_t& bufferRequired) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasesLengthWImpl(const std::wstring_view exeName, + size_t& bufferRequired) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasExesLengthAImpl(size_t& bufferRequired) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasesAImpl(const std::string_view exeName, + gsl::span alias, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasesWImpl(const std::wstring_view exeName, + gsl::span alias, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasExesAImpl(gsl::span aliasExes, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleAliasExesWImpl(gsl::span aliasExes, + size_t& written) noexcept override; + +#pragma region CMDext Private API + + [[nodiscard]] + HRESULT ExpungeConsoleCommandHistoryAImpl(const std::string_view exeName) noexcept override; + + [[nodiscard]] + HRESULT ExpungeConsoleCommandHistoryWImpl(const std::wstring_view exeName) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleNumberOfCommandsAImpl(const std::string_view exeName, + const size_t numberOfCommands) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleNumberOfCommandsWImpl(const std::wstring_view exeName, + const size_t numberOfCommands) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleCommandHistoryLengthAImpl(const std::string_view exeName, + size_t& length) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleCommandHistoryLengthWImpl(const std::wstring_view exeName, + size_t& length) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleCommandHistoryAImpl(const std::string_view exeName, + gsl::span commandHistory, + size_t& written) noexcept override; + + [[nodiscard]] + HRESULT GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, + gsl::span commandHistory, + size_t& written) noexcept override; + +#pragma endregion + + void GetConsoleWindowImpl(HWND& hwnd) noexcept override; + + void GetConsoleSelectionInfoImpl(CONSOLE_SELECTION_INFO& consoleSelectionInfo) noexcept override; + + void GetConsoleHistoryInfoImpl(CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept override; + + [[nodiscard]] + HRESULT SetConsoleHistoryInfoImpl(const CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept override; + + [[nodiscard]] + HRESULT SetCurrentConsoleFontExImpl(IConsoleOutputObject& context, + const bool isForMaximumWindowSize, + const CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept override; + +#pragma endregion +}; diff --git a/src/host/CommandListPopup.cpp b/src/host/CommandListPopup.cpp new file mode 100644 index 000000000..5522de1c7 --- /dev/null +++ b/src/host/CommandListPopup.cpp @@ -0,0 +1,552 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "CommandListPopup.hpp" +#include "stream.h" +#include "_stream.h" +#include "cmdline.h" +#include "misc.h" +#include "_output.h" +#include "dbcs.h" +#include "../types/inc/GlyphWidth.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +static constexpr size_t COMMAND_NUMBER_SIZE = 8; // size of command number buffer + +// Routine Description: +// - Calculates what the proposed size of the popup should be, based on the commands in the history +// Arguments: +// - history - the history to look through to measure command sizes +// Return Value: +// - the proposed size of the popup with the history list taken into account +static COORD calculatePopupSize(const CommandHistory& history) +{ + // this is the historical size of the popup, so it is now used as a minimum + const COORD minSize = { 40, 10 }; + + // padding is for the command number listing before a command is printed to the window. + // ex: |10: echo blah + // ^^^^ <- these are the cells that are being accounted for by padding + const size_t padding = 4; + + // find the widest command history item and use it for the width + size_t width = minSize.X; + for (size_t i = 0; i < history.GetNumberOfCommands(); ++i) + { + const auto& historyItem = history.GetNth(gsl::narrow(i)); + width = std::max(width, historyItem.size() + padding); + } + if (width > SHRT_MAX) + { + width = SHRT_MAX; + } + + // calculate height, it can range up to 20 rows + short height = std::clamp(gsl::narrow(history.GetNumberOfCommands()), minSize.Y, 20i16); + + return { gsl::narrow(width), height }; +} + +CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) : + Popup(screenInfo, calculatePopupSize(history)), + _history{ history }, + _currentCommand{ std::min(history.LastDisplayed, static_cast(history.GetNumberOfCommands() - 1)) } +{ + FAIL_FAST_IF(_currentCommand < 0); + _setBottomIndex(); +} + +[[nodiscard]] +NTSTATUS CommandListPopup::_handlePopupKeys(COOKED_READ_DATA& cookedReadData, const wchar_t wch, const DWORD modifiers) noexcept +{ + try + { + short Index = 0; + const bool shiftPressed = WI_IsFlagSet(modifiers, SHIFT_PRESSED); + switch (wch) + { + case VK_F9: + { + const HRESULT hr = CommandLine::Instance().StartCommandNumberPopup(cookedReadData); + if (S_FALSE == hr) + { + // If we couldn't make the popup, break and go around to read another input character. + break; + } + else + { + return hr; + } + } + case VK_ESCAPE: + CommandLine::Instance().EndCurrentPopup(); + return CONSOLE_STATUS_WAIT_NO_BLOCK; + case VK_UP: + if (shiftPressed) + { + return _swapUp(cookedReadData); + } + else + { + _update(-1); + } + break; + case VK_DOWN: + if (shiftPressed) + { + return _swapDown(cookedReadData); + } + else + { + _update(1); + } + break; + case VK_END: + // Move waaay forward, UpdateCommandListPopup() can handle it. + _update((SHORT)(cookedReadData.History().GetNumberOfCommands())); + break; + case VK_HOME: + // Move waaay back, UpdateCommandListPopup() can handle it. + _update(-(SHORT)(cookedReadData.History().GetNumberOfCommands())); + break; + case VK_PRIOR: + _update(-(SHORT)Height()); + break; + case VK_NEXT: + _update((SHORT)Height()); + break; + case VK_DELETE: + return _deleteSelection(cookedReadData); + case VK_LEFT: + case VK_RIGHT: + Index = _currentCommand; + CommandLine::Instance().EndCurrentPopup(); + SetCurrentCommandLine(cookedReadData, (SHORT)Index); + return CONSOLE_STATUS_WAIT_NO_BLOCK; + default: + break; + } + } + CATCH_LOG(); + return STATUS_SUCCESS; +} + +void CommandListPopup::_setBottomIndex() +{ + if (_currentCommand < (SHORT)(_history.GetNumberOfCommands() - Height())) + { + _bottomIndex = std::max(_currentCommand, gsl::narrow(Height() - 1i16)); + } + else + { + _bottomIndex = (SHORT)(_history.GetNumberOfCommands() - 1); + } +} + +[[nodiscard]] +NTSTATUS CommandListPopup::_deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept +{ + try + { + auto& history = cookedReadData.History(); + history.Remove(static_cast(_currentCommand)); + _setBottomIndex(); + + if (history.GetNumberOfCommands() == 0) + { + // close the popup + return CONSOLE_STATUS_READ_COMPLETE; + } + else if (_currentCommand >= static_cast(history.GetNumberOfCommands())) + { + _currentCommand = static_cast(history.GetNumberOfCommands() - 1); + _bottomIndex = _currentCommand; + } + + _drawList(); + } + CATCH_LOG(); + return STATUS_SUCCESS; +} + +// Routine Description: +// - moves the selected history item up in the history list +// Arguments: +// - cookedReadData - the read wait object to operate upon +[[nodiscard]] +NTSTATUS CommandListPopup::_swapUp(COOKED_READ_DATA& cookedReadData) noexcept +{ + try + { + auto& history = cookedReadData.History(); + + if (history.GetNumberOfCommands() <= 1 || _currentCommand == 0) + { + return STATUS_SUCCESS; + } + history.Swap(_currentCommand, _currentCommand - 1); + _update(-1); + _drawList(); + } + CATCH_LOG(); + return STATUS_SUCCESS; +} + +// Routine Description: +// - moves the selected history item down in the history list +// Arguments: +// - cookedReadData - the read wait object to operate upon +[[nodiscard]] +NTSTATUS CommandListPopup::_swapDown(COOKED_READ_DATA& cookedReadData) noexcept +{ + try + { + auto& history = cookedReadData.History(); + + if (history.GetNumberOfCommands() <= 1 || _currentCommand == gsl::narrow(history.GetNumberOfCommands()) - 1i16) + { + return STATUS_SUCCESS; + } + history.Swap(_currentCommand, _currentCommand + 1); + _update(1); + _drawList(); + } + CATCH_LOG(); + return STATUS_SUCCESS; +} + +void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) +{ + short Index = 0; + NTSTATUS Status = STATUS_SUCCESS; + DWORD LineCount = 1; + Index = _currentCommand; + CommandLine::Instance().EndCurrentPopup(); + SetCurrentCommandLine(cookedReadData, (SHORT)Index); + cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status); + // complete read + if (cookedReadData.IsEchoInput()) + { + // check for alias + cookedReadData.ProcessAliases(LineCount); + } + + Status = STATUS_SUCCESS; + size_t NumBytes; + if (cookedReadData.BytesRead() > cookedReadData.UserBufferSize() || LineCount > 1) + { + if (LineCount > 1) + { + const wchar_t* Tmp; + for (Tmp = cookedReadData.BufferStartPtr(); *Tmp != UNICODE_LINEFEED; Tmp++) + { + FAIL_FAST_IF(!(Tmp < (cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()))); + } + NumBytes = (Tmp - cookedReadData.BufferStartPtr() + 1) * sizeof(*Tmp); + } + else + { + NumBytes = cookedReadData.UserBufferSize(); + } + + // Copy what we can fit into the user buffer + const size_t bytesWritten = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); + + // Store all of the remaining as pending until the next read operation. + cookedReadData.SavePendingInput(NumBytes / sizeof(wchar_t), LineCount > 1); + NumBytes = bytesWritten; + } + else + { + NumBytes = cookedReadData.BytesRead(); + NumBytes = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); + } + + cookedReadData.SetReportedByteCount(NumBytes); +} + +void CommandListPopup::_cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch) +{ + short Index = 0; + if (cookedReadData.History().FindMatchingCommand({ &wch, 1 }, + _currentCommand, + Index, + CommandHistory::MatchOptions::JustLooking)) + { + _update((SHORT)(Index - _currentCommand), true); + } +} + +// Routine Description: +// - This routine handles the command list popup. It returns when we're out of input or the user has selected a command line. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - CONSOLE_STATUS_READ_COMPLETE - user hit return +[[nodiscard]] +NTSTATUS CommandListPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept +{ + NTSTATUS Status = STATUS_SUCCESS; + + for (;;) + { + WCHAR wch = UNICODE_NULL; + bool popupKeys = false; + DWORD modifiers = 0; + + Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + if (popupKeys) + { + Status = _handlePopupKeys(cookedReadData, wch, modifiers); + if (Status != STATUS_SUCCESS) + { + return Status; + } + } + else if (wch == UNICODE_CARRIAGERETURN) + { + _handleReturn(cookedReadData); + return CONSOLE_STATUS_READ_COMPLETE; + } + else + { + // cycle through commands that start with the letter of the key pressed + _cycleSelectionToMatchingCommands(cookedReadData, wch); + } + } +} + +void CommandListPopup::_DrawContent() +{ + _drawList(); +} + +// Routine Description: +// - Draws a list of commands for the user to choose from +void CommandListPopup::_drawList() +{ + // draw empty popup + COORD WriteCoord; + WriteCoord.X = _region.Left + 1i16; + WriteCoord.Y = _region.Top + 1i16; + size_t lStringLength = Width(); + for (SHORT i = 0; i < Height(); ++i) + { + const OutputCellIterator spaces(UNICODE_SPACE, _attributes, lStringLength); + const auto result = _screenInfo.Write(spaces, WriteCoord); + lStringLength = result.GetCellDistance(spaces); + WriteCoord.Y += 1i16; + } + + auto& api = ServiceLocator::LocateGlobals().api; + + WriteCoord.Y = _region.Top + 1i16; + SHORT i = std::max(gsl::narrow(_bottomIndex - Height() + 1), 0i16); + for (; i <= _bottomIndex; i++) + { + CHAR CommandNumber[COMMAND_NUMBER_SIZE]; + // Write command number to screen. + if (0 != _itoa_s(i, CommandNumber, ARRAYSIZE(CommandNumber), 10)) + { + return; + } + + PCHAR CommandNumberPtr = CommandNumber; + + size_t CommandNumberLength; + if (FAILED(StringCchLengthA(CommandNumberPtr, ARRAYSIZE(CommandNumber), &CommandNumberLength))) + { + return; + } + __assume_bound(CommandNumberLength); + + if (CommandNumberLength + 1 >= ARRAYSIZE(CommandNumber)) + { + return; + } + + CommandNumber[CommandNumberLength] = ':'; + CommandNumber[CommandNumberLength + 1] = ' '; + CommandNumberLength += 2; + if (CommandNumberLength > static_cast(Width())) + { + CommandNumberLength = static_cast(Width()); + } + + WriteCoord.X = _region.Left + 1i16; + + LOG_IF_FAILED(api.WriteConsoleOutputCharacterAImpl(_screenInfo, + { CommandNumberPtr, CommandNumberLength }, + WriteCoord, + CommandNumberLength)); + + // write command to screen + auto command = _history.GetNth(i); + lStringLength = command.size(); + { + size_t lTmpStringLength = lStringLength; + LONG lPopupLength = static_cast(Width() - CommandNumberLength); + PCWCHAR lpStr = command.data(); + while (lTmpStringLength--) + { + if (IsGlyphFullWidth(*lpStr++)) + { + lPopupLength -= 2; + } + else + { + lPopupLength--; + } + + if (lPopupLength <= 0) + { + lStringLength -= lTmpStringLength; + if (lPopupLength < 0) + { + lStringLength--; + } + + break; + } + } + } + + WriteCoord.X = gsl::narrow(WriteCoord.X + CommandNumberLength); + size_t used; + LOG_IF_FAILED(api.WriteConsoleOutputCharacterWImpl(_screenInfo, + { command.data(), lStringLength }, + WriteCoord, + used)); + + // write attributes to screen + if (i == _currentCommand) + { + WriteCoord.X = _region.Left + 1i16; + // inverted attributes + lStringLength = Width(); + TextAttribute inverted = _attributes; + inverted.Invert(); + + const OutputCellIterator it(inverted, lStringLength); + const auto done = _screenInfo.Write(it, WriteCoord); + + lStringLength = done.GetCellDistance(it); + } + + WriteCoord.Y += 1; + } +} + +// Routine Description: +// - For popup lists, will adjust the position of the highlighted item and +// possibly scroll the list if necessary. +// Arguments: +// - originalDelta - The number of lines to move up or down +// - wrap - Down past the bottom or up past the top should wrap the command list +void CommandListPopup::_update(const SHORT originalDelta, const bool wrap) +{ + SHORT delta = originalDelta; + if (delta == 0) + { + return; + } + SHORT const Size = Height(); + + SHORT CurCmdNum = _currentCommand; + SHORT NewCmdNum = CurCmdNum + delta; + + if (wrap) + { + // Modulo the number of commands to "circle" around if we went off the end. + NewCmdNum %= _history.GetNumberOfCommands(); + } + else + { + if (NewCmdNum >= gsl::narrow(_history.GetNumberOfCommands())) + { + NewCmdNum = gsl::narrow(_history.GetNumberOfCommands()) - 1i16; + } + else if (NewCmdNum < 0) + { + NewCmdNum = 0; + } + } + delta = NewCmdNum - CurCmdNum; + + bool Scroll = false; + // determine amount to scroll, if any + if (NewCmdNum <= _bottomIndex - Size) + { + _bottomIndex += delta; + if (_bottomIndex < Size - 1i16) + { + _bottomIndex = Size - 1i16; + } + Scroll = true; + } + else if (NewCmdNum > _bottomIndex) + { + _bottomIndex += delta; + if (_bottomIndex >= gsl::narrow(_history.GetNumberOfCommands())) + { + _bottomIndex = gsl::narrow(_history.GetNumberOfCommands()) - 1i16; + } + Scroll = true; + } + + // write commands to popup + if (Scroll) + { + _currentCommand = NewCmdNum; + _drawList(); + } + else + { + _updateHighlight(_currentCommand, NewCmdNum); + _currentCommand = NewCmdNum; + } +} + +// Routine Description: +// - Adjusts the highligted line in a list of commands +// Arguments: +// - OldCurrentCommand - The previous command highlighted +// - NewCurrentCommand - The new command to be highlighted. +void CommandListPopup::_updateHighlight(const SHORT OldCurrentCommand, const SHORT NewCurrentCommand) +{ + SHORT TopIndex; + if (_bottomIndex < Height()) + { + TopIndex = 0; + } + else + { + TopIndex = _bottomIndex - Height() + 1i16; + } + COORD WriteCoord; + WriteCoord.X = _region.Left + 1i16; + size_t lStringLength = Width(); + + WriteCoord.Y = _region.Top + 1i16 + OldCurrentCommand - TopIndex; + + const OutputCellIterator it(_attributes, lStringLength); + const auto done = _screenInfo.Write(it, WriteCoord); + lStringLength = done.GetCellDistance(it); + + // highlight new command + WriteCoord.Y = _region.Top + 1i16 + NewCurrentCommand - TopIndex; + + // inverted attributes + TextAttribute inverted = _attributes; + inverted.Invert(); + const OutputCellIterator itAttr(inverted, lStringLength); + const auto doneAttr = _screenInfo.Write(itAttr, WriteCoord); + lStringLength = done.GetCellDistance(itAttr); +} diff --git a/src/host/CommandListPopup.hpp b/src/host/CommandListPopup.hpp new file mode 100644 index 000000000..07bd60e16 --- /dev/null +++ b/src/host/CommandListPopup.hpp @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CommandListPopup.hpp + +Abstract: +- Popup used for use command list input +- contains code pulled from popup.cpp and cmdline.cpp + +Author: +- Austin Diviness (AustDi) 18-Aug-2018 +--*/ + +#pragma once + +#include "popup.h" + + +class CommandListPopup : public Popup +{ +public: + CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history); + + [[nodiscard]] + NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; + +protected: + void _DrawContent() override; + +private: + void _drawList(); + void _update(const SHORT delta, const bool wrap = false); + void _updateHighlight(const SHORT oldCommand, const SHORT newCommand); + + void _handleReturn(COOKED_READ_DATA& cookedReadData); + void _cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch); + void _setBottomIndex(); + [[nodiscard]] + NTSTATUS _handlePopupKeys(COOKED_READ_DATA& cookedReadData, const wchar_t wch, const DWORD modifiers) noexcept; + [[nodiscard]] + NTSTATUS _deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept; + [[nodiscard]] + NTSTATUS _swapUp(COOKED_READ_DATA& cookedReadData) noexcept; + [[nodiscard]] + NTSTATUS _swapDown(COOKED_READ_DATA& cookedReadData) noexcept; + + SHORT _currentCommand; + SHORT _bottomIndex; // number of command displayed on last line of popup + const CommandHistory& _history; + +#ifdef UNIT_TESTING + friend class CommandListPopupTests; +#endif +}; diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp new file mode 100644 index 000000000..94c431943 --- /dev/null +++ b/src/host/CommandNumberPopup.cpp @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "CommandNumberPopup.hpp" + +#include "stream.h" +#include "_stream.h" +#include "cmdline.h" +#include "resource.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// 5 digit number for command history +static constexpr size_t COMMAND_NUMBER_LENGTH = 5; + +static constexpr size_t COMMAND_NUMBER_PROMPT_LENGTH = 22; + +CommandNumberPopup::CommandNumberPopup(SCREEN_INFORMATION& screenInfo) : + Popup(screenInfo, { COMMAND_NUMBER_PROMPT_LENGTH + COMMAND_NUMBER_LENGTH, 1 }) +{ + _userInput.reserve(COMMAND_NUMBER_LENGTH); +} + +// Routine Description: +// - handles numerical user input +// Arguments: +// - cookedReadData - read data to operate on +// - wch - digit to handle +void CommandNumberPopup::_handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept +{ + if (_userInput.size() < COMMAND_NUMBER_LENGTH) + { + size_t CharsToWrite = sizeof(wchar_t); + const TextAttribute realAttributes = cookedReadData.ScreenInfo().GetAttributes(); + cookedReadData.ScreenInfo().SetAttributes(_attributes); + size_t NumSpaces; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + _userInput.data(), + _userInput.data() + _userInput.size(), + &wch, + &CharsToWrite, + &NumSpaces, + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr)); + cookedReadData.ScreenInfo().SetAttributes(realAttributes); + try + { + _push(wch); + } + CATCH_LOG(); + } +} + +// Routine Description: +// - handles backspace user input. removes a digit from the user input +// Arguments: +// - cookedReadData - read data to operate on +void CommandNumberPopup::_handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept +{ + if (_userInput.size() > 0) + { + size_t CharsToWrite = sizeof(WCHAR); + const wchar_t backspace = UNICODE_BACKSPACE; + const TextAttribute realAttributes = cookedReadData.ScreenInfo().GetAttributes(); + cookedReadData.ScreenInfo().SetAttributes(_attributes); + size_t NumSpaces; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + _userInput.data(), + _userInput.data() + _userInput.size(), + &backspace, + &CharsToWrite, + &NumSpaces, + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr)); + cookedReadData.ScreenInfo().SetAttributes(realAttributes); + _pop(); + } +} + +// Routine Description: +// - handles escape user input. cancels the popup +// Arguments: +// - cookedReadData - read data to operate on +void CommandNumberPopup::_handleEscape(COOKED_READ_DATA& cookedReadData) noexcept +{ + CommandLine::Instance().EndAllPopups(); + + // Note that cookedReadData's OriginalCursorPosition is the position before ANY text was entered on the edit line. + // We want to use the position before the cursor was moved for this popup handler specifically, which may + // be *anywhere* in the edit line and will be synchronized with the pointers in the cookedReadData + // structure (BufPtr, etc.) + LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.BeforeDialogCursorPosition(), TRUE)); +} + +// Routine Description: +// - handles return user input. sets the prompt to the history item indicated +// Arguments: +// - cookedReadData - read data to operate on +void CommandNumberPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) noexcept +{ + const short commandNumber = gsl::narrow(std::min(static_cast(_parse()), + cookedReadData.History().GetNumberOfCommands() - 1)); + + CommandLine::Instance().EndAllPopups(); + SetCurrentCommandLine(cookedReadData, commandNumber); +} + +// Routine Description: +// - This routine handles the command number selection popup. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - CONSOLE_STATUS_READ_COMPLETE - user hit return +[[nodiscard]] +NTSTATUS CommandNumberPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept +{ + NTSTATUS Status = STATUS_SUCCESS; + WCHAR wch = UNICODE_NULL; + bool popupKeys = false; + DWORD modifiers = 0; + + for(;;) + { + Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + if (std::iswdigit(wch)) + { + _handleNumber(cookedReadData, wch); + } + else if (wch == UNICODE_BACKSPACE) + { + _handleBackspace(cookedReadData); + } + else if (wch == VK_ESCAPE) + { + _handleEscape(cookedReadData); + break; + } + else if (wch == UNICODE_CARRIAGERETURN) + { + _handleReturn(cookedReadData); + break; + } + } + return CONSOLE_STATUS_WAIT_NO_BLOCK; +} + +void CommandNumberPopup::_DrawContent() +{ + _DrawPrompt(ID_CONSOLE_MSGCMDLINEF9); +} + +// Routine Description: +// - adds single digit number to the popup's number buffer +// Arguments: +// - wch - char of the number to add. must be in the range [L'0', L'9'] +// Note: will throw if wch is out of range +void CommandNumberPopup::_push(const wchar_t wch) +{ + THROW_HR_IF(E_INVALIDARG, !std::iswdigit(wch)); + if (_userInput.size() < COMMAND_NUMBER_LENGTH) + { + _userInput += wch; + } +} + +// Routine Description: +// - removes the last number added to the number buffer +void CommandNumberPopup::_pop() noexcept +{ + if (!_userInput.empty()) + { + _userInput.pop_back(); + } +} + +// Routine Description: +// - get numerical value for the data stored in the number buffer +// Return Value: +// - parsed integer representing the string value found in the number buffer +int CommandNumberPopup::_parse() const noexcept +{ + try + { + return std::stoi(_userInput); + } + catch (...) + { + return 0; + } +} diff --git a/src/host/CommandNumberPopup.hpp b/src/host/CommandNumberPopup.hpp new file mode 100644 index 000000000..d92013e7e --- /dev/null +++ b/src/host/CommandNumberPopup.hpp @@ -0,0 +1,47 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CommandNumberPopup.hpp + +Abstract: +- Popup used for use command number input +- contains code pulled from popup.cpp and cmdline.cpp + +Author: +- Austin Diviness (AustDi) 18-Aug-2018 +--*/ + +#pragma once + +#include "popup.h" + + +class CommandNumberPopup final : public Popup +{ +public: + CommandNumberPopup(SCREEN_INFORMATION& screenInfo); + + [[nodiscard]] + NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; + +protected: + void _DrawContent() override; + +private: + std::wstring _userInput; + + void _handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept; + void _handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept; + void _handleEscape(COOKED_READ_DATA& cookedReadData) noexcept; + void _handleReturn(COOKED_READ_DATA& cookedReadData) noexcept; + + void _push(const wchar_t wch); + void _pop() noexcept; + int _parse() const noexcept; + +#ifdef UNIT_TESTING + friend class CommandNumberPopupTests; +#endif +}; diff --git a/src/host/ConsoleArguments.cpp b/src/host/ConsoleArguments.cpp new file mode 100644 index 000000000..5004c9912 --- /dev/null +++ b/src/host/ConsoleArguments.cpp @@ -0,0 +1,573 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "ConsoleArguments.hpp" +#include "../types/inc/utils.hpp" +#include +using namespace Microsoft::Console::Utils; + +const std::wstring ConsoleArguments::VT_MODE_ARG = L"--vtmode"; +const std::wstring ConsoleArguments::HEADLESS_ARG = L"--headless"; +const std::wstring ConsoleArguments::SERVER_HANDLE_ARG = L"--server"; +const std::wstring ConsoleArguments::SIGNAL_HANDLE_ARG = L"--signal"; +const std::wstring ConsoleArguments::HANDLE_PREFIX = L"0x"; +const std::wstring ConsoleArguments::CLIENT_COMMANDLINE_ARG = L"--"; +const std::wstring ConsoleArguments::FORCE_V1_ARG = L"-ForceV1"; +const std::wstring ConsoleArguments::FILEPATH_LEADER_PREFIX = L"\\??\\"; +const std::wstring ConsoleArguments::WIDTH_ARG = L"--width"; +const std::wstring ConsoleArguments::HEIGHT_ARG = L"--height"; +const std::wstring ConsoleArguments::INHERIT_CURSOR_ARG = L"--inheritcursor"; +const std::wstring ConsoleArguments::FEATURE_ARG = L"--feature"; +const std::wstring ConsoleArguments::FEATURE_PTY_ARG = L"pty"; + +ConsoleArguments::ConsoleArguments(const std::wstring& commandline, + const HANDLE hStdIn, + const HANDLE hStdOut) + : _commandline(commandline), + _vtInHandle(hStdIn), + _vtOutHandle(hStdOut), + _recievedEarlySizeChange{ false }, + _originalWidth{ -1 }, + _originalHeight{ -1 } +{ + _clientCommandline = L""; + _vtMode = L""; + _headless = false; + _createServerHandle = true; + _serverHandle = 0; + _signalHandle = 0; + _forceV1 = false; + _width = 0; + _height = 0; + _inheritCursor = false; +} + +ConsoleArguments::ConsoleArguments() : + ConsoleArguments(L"", nullptr, nullptr) +{ + +} + +ConsoleArguments& ConsoleArguments::operator=(const ConsoleArguments & other) +{ + if (this != &other) + { + _commandline = other._commandline; + _clientCommandline = other._clientCommandline; + _vtInHandle = other._vtInHandle; + _vtOutHandle = other._vtOutHandle; + _vtMode = other._vtMode; + _headless = other._headless; + _createServerHandle = other._createServerHandle; + _serverHandle = other._serverHandle; + _signalHandle = other._signalHandle; + _forceV1 = other._forceV1; + _width = other._width; + _height = other._height; + _inheritCursor = other._inheritCursor; + _recievedEarlySizeChange = other._recievedEarlySizeChange; + } + + return *this; +} + +// Routine Description: +// - Consumes the argument at the given index off of the vector. +// Arguments: +// - args - The vector full of args +// - index - The item to consume/remove from the vector. +// Return Value: +// - +void ConsoleArguments::s_ConsumeArg(_Inout_ std::vector& args, _In_ size_t& index) +{ + args.erase(args.begin() + index); +} + +// Routine Description: +// Given the commandline of tokens `args`, tries to find the argument at +// index+1, and places it's value into pSetting. +// If there aren't enough args, then returns E_INVALIDARG. +// If we found a value, then we take the elements at both index and index+1 out +// of args. We'll also decrement index, so that a caller who is using index +// as a loop index will autoincrement it to have it point at the correct +// next index. +// +// EX: for args=[--foo, bar, --baz] +// index=0 would place "bar" in pSetting, +// args is now [--baz], index is now -1, caller increments to 0 +// index=2 would return E_INVALIDARG, +// args is still [--foo, bar, --baz], index is still 2, caller increments to 3. +// Arguments: +// args: A collection of wstrings representing command-line arguments +// index: the index of the argument of which to get the value for. The value +// should be at (index+1). index will be decremented by one on success. +// pSetting: recieves the string at index+1 +// Return Value: +// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating +// failure. +[[nodiscard]] +HRESULT ConsoleArguments::s_GetArgumentValue(_Inout_ std::vector& args, _Inout_ size_t& index, _Out_opt_ std::wstring* const pSetting) +{ + bool hasNext = (index + 1) < args.size(); + if (hasNext) + { + s_ConsumeArg(args, index); + if (pSetting != nullptr) + { + *pSetting = args[index]; + } + s_ConsumeArg(args, index); + } + return (hasNext) ? S_OK : E_INVALIDARG; +} + +// Routine Description: +// Similar to s_GetArgumentValue. +// Attempts to get the next arg as a "feature" arg - this can be used for +// feature detection. +// If the next arg is not recognized, then we don't support that feature. +// Currently, the only supported feature arg is `pty`, to identify pty support. +// Arguments: +// args: A collection of wstrings representing command-line arguments +// index: the index of the argument of which to get the value for. The value +// should be at (index+1). index will be decremented by one on success. +// pSetting: recieves the string at index+1 +// Return Value: +// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating +// failure. +[[nodiscard]] +HRESULT ConsoleArguments::s_HandleFeatureValue(_Inout_ std::vector& args, _Inout_ size_t& index) +{ + HRESULT hr = E_INVALIDARG; + bool hasNext = (index + 1) < args.size(); + if (hasNext) + { + s_ConsumeArg(args, index); + std::wstring value = args[index]; + if (value == FEATURE_PTY_ARG) + { + hr = S_OK; + } + s_ConsumeArg(args, index); + } + return (hasNext) ? hr : E_INVALIDARG; +} + +// Method Description: +// Routine Description: +// Given the commandline of tokens `args`, tries to find the argument at +// index+1, and places it's value into pSetting. See above for examples. +// This implementation attempts to parse a short from the argument. +// Arguments: +// args: A collection of wstrings representing command-line arguments +// index: the index of the argument of which to get the value for. The value +// should be at (index+1). index will be decremented by one on success. +// pSetting: recieves the short at index+1 +// Return Value: +// S_OK if we parsed the short successfully, otherwise E_INVALIDARG indicating +// failure. This could be the case for non-numeric arguments, or for >SHORT_MAX args. +[[nodiscard]] +HRESULT ConsoleArguments::s_GetArgumentValue(_Inout_ std::vector& args, + _Inout_ size_t& index, + _Out_opt_ short* const pSetting) +{ + bool succeeded = (index + 1) < args.size(); + if (succeeded) + { + s_ConsumeArg(args, index); + if (pSetting != nullptr) + { + try + { + size_t pos = 0; + int value = std::stoi(args[index], &pos); + // If the entire string was a number, pos will be equal to the + // length of the string. Otherwise, a string like 8foo will + // be parsed as "8" + if (value > SHORT_MAX || pos != args[index].length()) + { + succeeded = false; + } + else + { + *pSetting = static_cast(value); + succeeded = true; + } + } + catch (...) + { + succeeded = false; + } + + } + s_ConsumeArg(args, index); + } + return (succeeded) ? S_OK : E_INVALIDARG; +} + +// Routine Description: +// - Parsing helper that will turn a string into a handle value if possible. +// Arguments: +// - handleAsText - The string representation of the handle that was passed in on the command line +// - handleAsVal - The location to store the value if we can appropriately convert it. +// Return Value: +// - S_OK if we could successfully parse the given text and store it in the handle value location. +// - E_INVALIDARG if we couldn't parse the text as a valid hex-encoded handle number OR +// if the handle value was already filled. +[[nodiscard]] +HRESULT ConsoleArguments::s_ParseHandleArg(const std::wstring& handleAsText, _Inout_ DWORD& handleAsVal) +{ + HRESULT hr = S_OK; + + // The handle should have a valid prefix. + if (handleAsText.substr(0, HANDLE_PREFIX.length()) != HANDLE_PREFIX) + { + hr = E_INVALIDARG; + } + else if (0 == handleAsVal) + { + handleAsVal = wcstoul(handleAsText.c_str(), nullptr /*endptr*/, 16 /*base*/); + + // If the handle didn't parse into a reasonable handle ID, invalid. + if (handleAsVal == 0) + { + hr = E_INVALIDARG; + } + } + else + { + // If we're trying to set the handle a second time, invalid. + hr = E_INVALIDARG; + } + + return hr; +} + +// Routine Description: +// Given the commandline of tokens `args`, creates a wstring containing all of +// the remaining args after index joined with spaces. If skipFirst==true, +// then we omit the argument at index from this finished string. skipFirst +// should only be true if the first arg is +// ConsoleArguments::CLIENT_COMMANDLINE_ARG. Removes all the args starting +// at index from the collection. +// The finished commandline is placed in _clientCommandline +// Arguments: +// args: A collection of wstrings representing command-line arguments +// index: the index of the argument of which to start the commandline from. +// skipFirst: if true, omit the arg at index (which should be "--") +// Return Value: +// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating +// failure. +[[nodiscard]] +HRESULT ConsoleArguments::_GetClientCommandline(_Inout_ std::vector& args, const size_t index, const bool skipFirst) +{ + auto start = args.begin()+index; + + // Erase the first token. + // Used to get rid of the explicit commandline token "--" + if (skipFirst) + { + // Make sure that the arg we're deleting is "--" + FAIL_FAST_IF(!(CLIENT_COMMANDLINE_ARG == start->c_str())); + args.erase(start); + } + + _clientCommandline = L""; + size_t j = 0; + for (j = index; j < args.size(); j++) + { + _clientCommandline += args[j]; + if (j+1 < args.size()) + { + _clientCommandline += L" "; + } + } + args.erase(args.begin()+index, args.begin()+j); + + return S_OK; +} + +// Routine Description: +// Attempts to parse the commandline that this ConsoleArguments was initialized +// with. Fills all of our members with values that were specified on the +// commandline. +// Arguments: +// +// Return Value: +// S_OK if we parsed our _commandline successfully, otherwise E_INVALIDARG +// indicating failure. +[[nodiscard]] +HRESULT ConsoleArguments::ParseCommandline() +{ + // If the commandline was empty, quick return. + if (_commandline.length() == 0) + { + return S_OK; + } + + std::vector args; + HRESULT hr = S_OK; + + // Make a mutable copy of the commandline for tokenizing + std::wstring copy = _commandline; + + // Tokenize the commandline + int argc = 0; + wil::unique_hlocal_ptr argv; + argv.reset(CommandLineToArgvW(copy.c_str(), &argc)); + RETURN_LAST_ERROR_IF(argv == nullptr); + + for (int i = 1; i < argc; ++i) + { + args.push_back(argv[i]); + } + + // Parse args out of the commandline. + // As we handle a token, remove it from the args. + // At the end of parsing, there should be nothing left. + for (size_t i = 0; i < args.size();) + { + hr = E_INVALIDARG; + + std::wstring arg = args[i]; + + if (arg.substr(0, HANDLE_PREFIX.length()) == HANDLE_PREFIX || + arg == SERVER_HANDLE_ARG) + { + // server handle token accepted two ways: + // --server 0x4 (new method) + // 0x4 (legacy method) + // If we see >1 of these, it's invalid. + std::wstring serverHandleVal = arg; + + if (arg == SERVER_HANDLE_ARG) + { + hr = s_GetArgumentValue(args, i, &serverHandleVal); + } + else + { + s_ConsumeArg(args, i); + hr = S_OK; + } + + if (SUCCEEDED(hr)) + { + hr = s_ParseHandleArg(serverHandleVal, _serverHandle); + if (SUCCEEDED(hr)) + { + _createServerHandle = false; + } + } + } + else if (arg == SIGNAL_HANDLE_ARG) + { + std::wstring signalHandleVal; + hr = s_GetArgumentValue(args, i, &signalHandleVal); + + if (SUCCEEDED(hr)) + { + hr = s_ParseHandleArg(signalHandleVal, _signalHandle); + } + } + else if (arg == FORCE_V1_ARG) + { + // -ForceV1 command line switch for NTVDM support + _forceV1 = true; + s_ConsumeArg(args, i); + hr = S_OK; + } + else if (arg.substr(0, FILEPATH_LEADER_PREFIX.length()) == FILEPATH_LEADER_PREFIX) + { + // beginning of command line -- includes file path + // skipped for historical reasons. + s_ConsumeArg(args, i); + hr = S_OK; + } + else if (arg == VT_MODE_ARG) + { + hr = s_GetArgumentValue(args, i, &_vtMode); + } + else if (arg == WIDTH_ARG) + { + hr = s_GetArgumentValue(args, i, &_width); + } + else if (arg == HEIGHT_ARG) + { + hr = s_GetArgumentValue(args, i, &_height); + } + else if (arg == FEATURE_ARG) + { + hr = s_HandleFeatureValue(args, i); + } + else if (arg == HEADLESS_ARG) + { + _headless = true; + s_ConsumeArg(args, i); + hr = S_OK; + } + else if (arg == INHERIT_CURSOR_ARG) + { + _inheritCursor = true; + s_ConsumeArg(args, i); + hr = S_OK; + } + else if (arg == CLIENT_COMMANDLINE_ARG) + { + // Everything after this is the explicit commandline + hr = _GetClientCommandline(args, i, true); + break; + } + // TODO: handle the rest of the possible params (MSFT:13271366, MSFT:13631640) + // TODO: handle invalid args + // eg "conhost --foo bar" should not make the clientCommandline "--foo bar" + else + { + // If we encounter something that doesn't match one of our other + // args, then it's the start of the commandline + hr = _GetClientCommandline(args, i, false); + break; + } + + if (FAILED(hr)) + { + break; + } + } + + // We should have consumed every token at this point. + // if not, it is some sort of parsing error. + // If we failed to parse an arg, then no need to assert. + if (SUCCEEDED(hr)) + { + FAIL_FAST_IF(!args.empty()); + } + + return hr; +} + +// Routine Description: +// - Returns true if we already have opened handles to use for the VT server +// streams. +// - If false, try next to see if we have pipe names to open instead. +// Arguments: +// - - uses internal state +// Return Value: +// - True or false (see description) +bool ConsoleArguments::HasVtHandles() const +{ + return IsValidHandle(_vtInHandle) && IsValidHandle(_vtOutHandle); +} + +// Routine Description: +// - Returns true if we were passed a seemingly valid signal handle on startup. +// Arguments: +// - - uses internal state +// Return Value: +// - True or false (see description) +bool ConsoleArguments::HasSignalHandle() const +{ + return IsValidHandle(GetSignalHandle()); +} + +// Routine Description: +// - Returns true if we already have at least one handle for conpty streams. +// Arguments: +// - - uses internal state +// Return Value: +// - True or false (see description) +bool ConsoleArguments::InConptyMode() const noexcept +{ + // If we only have a signal handle, then that's fine, they probably called + // CreatePseudoConsole with neither handle. + // If we only have one of the other handles, that's fine they're still + // invoking us by passing in pipes, so they know what they're doing. + return IsValidHandle(_vtInHandle) || IsValidHandle(_vtOutHandle) || HasSignalHandle(); +} + +bool ConsoleArguments::IsHeadless() const +{ + return _headless; +} + +bool ConsoleArguments::ShouldCreateServerHandle() const +{ + return _createServerHandle; +} + +HANDLE ConsoleArguments::GetServerHandle() const +{ + return ULongToHandle(_serverHandle); +} + +HANDLE ConsoleArguments::GetSignalHandle() const +{ + return ULongToHandle(_signalHandle); +} + +HANDLE ConsoleArguments::GetVtInHandle() const +{ + return _vtInHandle; +} + +HANDLE ConsoleArguments::GetVtOutHandle() const +{ + return _vtOutHandle; +} + +std::wstring ConsoleArguments::GetClientCommandline() const +{ + return _clientCommandline; +} + +std::wstring ConsoleArguments::GetVtMode() const +{ + return _vtMode; +} + +bool ConsoleArguments::GetForceV1() const +{ + return _forceV1; +} + +short ConsoleArguments::GetWidth() const +{ + return _width; +} + +short ConsoleArguments::GetHeight() const +{ + return _height; +} + +bool ConsoleArguments::GetInheritCursor() const +{ + return _inheritCursor; +} + +// Method Description: +// - Tell us to use a different size than the one parsed as the size of the +// console. This is called by the PtySignalInputThread when it recieves a +// resize before the first client has connected. Because there's no client, +// there's also no buffer yet, so it has nothing to resize. +// However, we shouldn't just discard that first resize message. Instead, +// store it in here, so we can use the value when the first client does connect. +// Arguments: +// - dimensions: the new size in characters of the conpty buffer & viewport. +// Return Value: +// - +void ConsoleArguments::SetExpectedSize(COORD dimensions) noexcept +{ + _width = dimensions.X; + _height = dimensions.Y; + // Stash away the original values we parsed when this is called. + // This is to help debugging - if the signal thread DOES change these values, + // we can still recover what was given to us on the commandline. + if (!_recievedEarlySizeChange) + { + _originalWidth = _width; + _originalHeight = _height; + // Mark that we've changed size from what our commandline values were + _recievedEarlySizeChange = true; + } +} diff --git a/src/host/ConsoleArguments.hpp b/src/host/ConsoleArguments.hpp new file mode 100644 index 000000000..16f2e2fb1 --- /dev/null +++ b/src/host/ConsoleArguments.hpp @@ -0,0 +1,259 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleArguments.hpp + +Abstract: +- Encapsulates the commandline arguments to the console host. + +Author(s): +- Mike Griese (migrie) 07-Sept-2017 +--*/ + +#pragma once + +#ifdef UNIT_TESTING +#include "WexTestClass.h" +#endif + +class ConsoleArguments +{ +public: + ConsoleArguments(const std::wstring& commandline, + const HANDLE hStdIn, + const HANDLE hStdOut); + + ConsoleArguments(); + + ConsoleArguments& operator=(const ConsoleArguments& other); + + [[nodiscard]] + HRESULT ParseCommandline(); + + bool IsUsingVtPipe() const; + bool HasVtHandles() const; + bool InConptyMode() const noexcept; + bool IsHeadless() const; + bool ShouldCreateServerHandle() const; + + HANDLE GetServerHandle() const; + HANDLE GetVtInHandle() const; + HANDLE GetVtOutHandle() const; + + bool HasSignalHandle() const; + HANDLE GetSignalHandle() const; + + std::wstring GetClientCommandline() const; + std::wstring GetVtMode() const; + bool GetForceV1() const; + + short GetWidth() const; + short GetHeight() const; + bool GetInheritCursor() const; + + void SetExpectedSize(COORD dimensions) noexcept; + + static const std::wstring VT_MODE_ARG; + static const std::wstring HEADLESS_ARG; + static const std::wstring SERVER_HANDLE_ARG; + static const std::wstring SIGNAL_HANDLE_ARG; + static const std::wstring HANDLE_PREFIX; + static const std::wstring CLIENT_COMMANDLINE_ARG; + static const std::wstring FORCE_V1_ARG; + static const std::wstring FILEPATH_LEADER_PREFIX; + static const std::wstring WIDTH_ARG; + static const std::wstring HEIGHT_ARG; + static const std::wstring INHERIT_CURSOR_ARG; + static const std::wstring FEATURE_ARG; + static const std::wstring FEATURE_PTY_ARG; + +private: +#ifdef UNIT_TESTING + // This accessor used to create a copy of this class for unit testing comparison ease. + ConsoleArguments(const std::wstring commandline, + const std::wstring clientCommandline, + const HANDLE vtInHandle, + const HANDLE vtOutHandle, + const std::wstring vtMode, + const short width, + const short height, + const bool forceV1, + const bool headless, + const bool createServerHandle, + const DWORD serverHandle, + const DWORD signalHandle, + const bool inheritCursor) : + _commandline(commandline), + _clientCommandline(clientCommandline), + _vtInHandle(vtInHandle), + _vtOutHandle(vtOutHandle), + _vtMode(vtMode), + _width(width), + _height(height), + _forceV1(forceV1), + _headless(headless), + _createServerHandle(createServerHandle), + _serverHandle(serverHandle), + _signalHandle(signalHandle), + _inheritCursor(inheritCursor), + _recievedEarlySizeChange{ false }, + _originalWidth{ -1 }, + _originalHeight{ -1 } + { + + } +#endif + + std::wstring _commandline; + + std::wstring _clientCommandline; + + HANDLE _vtInHandle; + + HANDLE _vtOutHandle; + + std::wstring _vtMode; + + bool _forceV1; + bool _headless; + + short _width; + short _height; + + bool _createServerHandle; + DWORD _serverHandle; + DWORD _signalHandle; + bool _inheritCursor; + + bool _recievedEarlySizeChange; + short _originalWidth; + short _originalHeight; + + [[nodiscard]] + HRESULT _GetClientCommandline(_Inout_ std::vector& args, + const size_t index, + const bool skipFirst); + + static void s_ConsumeArg(_Inout_ std::vector& args, + _In_ size_t& index); + [[nodiscard]] + static HRESULT s_GetArgumentValue(_Inout_ std::vector& args, + _Inout_ size_t& index, + _Out_opt_ std::wstring* const pSetting); + [[nodiscard]] + static HRESULT s_GetArgumentValue(_Inout_ std::vector& args, + _Inout_ size_t& index, + _Out_opt_ short* const pSetting); + [[nodiscard]] + static HRESULT s_HandleFeatureValue(_Inout_ std::vector& args, + _Inout_ size_t& index); + + [[nodiscard]] + static HRESULT s_ParseHandleArg(const std::wstring& handleAsText, + _Inout_ DWORD& handleAsVal); + + +#ifdef UNIT_TESTING + friend class ConsoleArgumentsTests; +#endif +}; + +#ifdef UNIT_TESTING +namespace WEX { + namespace TestExecution { + template<> + class VerifyOutputTraits < ConsoleArguments > + { + public: + static WEX::Common::NoThrowString ToString(const ConsoleArguments& ci) + { + return WEX::Common::NoThrowString().Format(L"\r\nClient Command Line: '%ws',\r\n" + L"Use VT Handles: '%ws',\r\n" + L"VT In Handle: '0x%x',\r\n" + L"VT Out Handle: '0x%x',\r\n" + L"Vt Mode: '%ws',\r\n" + L"WidthxHeight: '%dx%d',\r\n" + L"ForceV1: '%ws',\r\n" + L"Headless: '%ws',\r\n" + L"Create Server Handle: '%ws',\r\n" + L"Server Handle: '0x%x'\r\n" + L"Use Signal Handle: '%ws'\r\n" + L"Signal Handle: '0x%x'\r\n", + L"Inherit Cursor: '%ws'\r\n", + ci.GetClientCommandline().c_str(), + s_ToBoolString(ci.HasVtHandles()), + ci.GetVtInHandle(), + ci.GetVtOutHandle(), + ci.GetVtMode().c_str(), + ci.GetWidth(), + ci.GetHeight(), + s_ToBoolString(ci.GetForceV1()), + s_ToBoolString(ci.IsHeadless()), + s_ToBoolString(ci.ShouldCreateServerHandle()), + ci.GetServerHandle(), + s_ToBoolString(ci.HasSignalHandle()), + ci.GetSignalHandle(), + s_ToBoolString(ci.GetInheritCursor())); + } + + private: + static PCWSTR s_ToBoolString(const bool val) + { + return val ? L"true" : L"false"; + } + }; + + template<> + class VerifyCompareTraits < ConsoleArguments, ConsoleArguments> + { + public: + static bool AreEqual(const ConsoleArguments& expected, const ConsoleArguments& actual) + { + return + expected.GetClientCommandline() == actual.GetClientCommandline() && + expected.HasVtHandles() == actual.HasVtHandles() && + expected.GetVtInHandle() == actual.GetVtInHandle() && + expected.GetVtOutHandle() == actual.GetVtOutHandle() && + expected.GetVtMode() == actual.GetVtMode() && + expected.GetWidth() == actual.GetWidth() && + expected.GetHeight() == actual.GetHeight() && + expected.GetForceV1() == actual.GetForceV1() && + expected.IsHeadless() == actual.IsHeadless() && + expected.ShouldCreateServerHandle() == actual.ShouldCreateServerHandle() && + expected.GetServerHandle() == actual.GetServerHandle() && + expected.HasSignalHandle() == actual.HasSignalHandle() && + expected.GetSignalHandle() == actual.GetSignalHandle() && + expected.GetInheritCursor() == actual.GetInheritCursor(); + } + + static bool AreSame(const ConsoleArguments& expected, const ConsoleArguments& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const ConsoleArguments&, const ConsoleArguments&) = delete; + + static bool IsGreaterThan(const ConsoleArguments&, const ConsoleArguments&) = delete; + + static bool IsNull(const ConsoleArguments& object) + { + return + object.GetClientCommandline().empty() && + (object.GetVtInHandle() == 0 || object.GetVtInHandle() == INVALID_HANDLE_VALUE) && + (object.GetVtOutHandle() == 0 || object.GetVtOutHandle() == INVALID_HANDLE_VALUE) && + object.GetVtMode().empty() && + !object.GetForceV1() && + (object.GetWidth() == 0) && + (object.GetHeight() == 0) && + !object.IsHeadless() && + !object.ShouldCreateServerHandle() && + object.GetServerHandle() == 0 && + (object.GetSignalHandle() == 0 || object.GetSignalHandle() == INVALID_HANDLE_VALUE) && + !object.GetInheritCursor(); + } + }; + } +} +#endif diff --git a/src/host/CopyFromCharPopup.cpp b/src/host/CopyFromCharPopup.cpp new file mode 100644 index 000000000..d267238a3 --- /dev/null +++ b/src/host/CopyFromCharPopup.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "CopyFromCharPopup.hpp" + +#include "_stream.h" +#include "resource.h" + +static constexpr size_t COPY_FROM_CHAR_PROMPT_LENGTH = 28; + +CopyFromCharPopup::CopyFromCharPopup(SCREEN_INFORMATION& screenInfo) : + Popup(screenInfo, { COPY_FROM_CHAR_PROMPT_LENGTH + 2, 1 }) +{ +} + +// Routine Description: +// - This routine handles the delete from cursor to char char popup. It returns when we're out of input or the user has entered a char. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - CONSOLE_STATUS_READ_COMPLETE - user hit return +[[nodiscard]] +NTSTATUS CopyFromCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept +{ + // get user input + WCHAR Char = UNICODE_NULL; + bool PopupKeys = false; + DWORD modifiers = 0; + NTSTATUS Status = _getUserInput(cookedReadData, PopupKeys, modifiers, Char); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + CommandLine::Instance().EndCurrentPopup(); + + if (PopupKeys && Char == VK_ESCAPE) + { + return CONSOLE_STATUS_WAIT_NO_BLOCK; + } + + const auto span = cookedReadData.SpanAtPointer(); + const auto foundLocation = std::find(std::next(span.begin()), span.end(), Char); + if (foundLocation == span.end()) + { + // char not found, delete everything to the right of the cursor + CommandLine::Instance().DeletePromptAfterCursor(cookedReadData); + } + else + { + // char was found, delete everything between the cursor and it + const auto difference = std::distance(span.begin(), foundLocation); + for (unsigned int i = 0; i < gsl::narrow(difference); ++i) + { + CommandLine::Instance().DeleteFromRightOfCursor(cookedReadData); + } + } + return CONSOLE_STATUS_WAIT_NO_BLOCK; +} + +void CopyFromCharPopup::_DrawContent() +{ + _DrawPrompt(ID_CONSOLE_MSGCMDLINEF4); +} diff --git a/src/host/CopyFromCharPopup.hpp b/src/host/CopyFromCharPopup.hpp new file mode 100644 index 000000000..9de8f9838 --- /dev/null +++ b/src/host/CopyFromCharPopup.hpp @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CopyFromCharPopup.hpp + +Abstract: +- Popup used for use copying from char input +- contains code pulled from popup.cpp and cmdline.cpp + +Author: +- Austin Diviness (AustDi) 18-Aug-2018 +--*/ + +#pragma once + +#include "popup.h" + +class CopyFromCharPopup final : public Popup +{ +public: + CopyFromCharPopup(SCREEN_INFORMATION& screenInfo); + + [[nodiscard]] + NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; + +protected: + void _DrawContent() override; +}; diff --git a/src/host/CopyToCharPopup.cpp b/src/host/CopyToCharPopup.cpp new file mode 100644 index 000000000..6b6baeb5f --- /dev/null +++ b/src/host/CopyToCharPopup.cpp @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "CopyToCharPopup.hpp" + +#include "stream.h" +#include "_stream.h" +#include "resource.h" + +static constexpr size_t COPY_TO_CHAR_PROMPT_LENGTH = 26; + +CopyToCharPopup::CopyToCharPopup(SCREEN_INFORMATION& screenInfo) : + Popup(screenInfo, { COPY_TO_CHAR_PROMPT_LENGTH + 2, 1 }) +{ +} + +// Routine Description: +// - copies text from the previous command into the current prompt line, up to but not including the first +// instance of wch after the current cookedReadData's cursor position. if wch is not found, nothing is copied. +// Arguments: +// - cookedReadData - the read data to operate on +// - LastCommand - the most recent command run +// - wch - the wchar to copy up to +void CopyToCharPopup::_copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch) +{ + // make sure that there it is possible to copy any found text over + if (cookedReadData.InsertionPoint() >= LastCommand.size()) + { + return; + } + + const auto searchStart = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint() + 1); + auto location = std::find(searchStart, LastCommand.cend(), wch); + + // didn't find wch so copy nothing + if (location == LastCommand.cend()) + { + return; + } + + const auto startIt = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint()); + const auto endIt = location; + + cookedReadData.Write({ &*startIt, gsl::narrow(std::distance(startIt, endIt)) }); +} + +// Routine Description: +// - This routine handles the delete char popup. It returns when we're out of input or the user has entered a char. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - CONSOLE_STATUS_READ_COMPLETE - user hit return +[[nodiscard]] +NTSTATUS CopyToCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept +{ + wchar_t wch = UNICODE_NULL; + bool popupKey = false; + DWORD modifiers = 0; + NTSTATUS Status = _getUserInput(cookedReadData, popupKey, modifiers, wch); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + CommandLine::Instance().EndCurrentPopup(); + + if (popupKey && wch == VK_ESCAPE) + { + return CONSOLE_STATUS_WAIT_NO_BLOCK; + } + + // copy up to specified char + const auto lastCommand = cookedReadData.History().GetLastCommand(); + if (!lastCommand.empty()) + { + _copyToChar(cookedReadData, lastCommand, wch); + } + + return CONSOLE_STATUS_WAIT_NO_BLOCK; +} + +void CopyToCharPopup::_DrawContent() +{ + _DrawPrompt(ID_CONSOLE_MSGCMDLINEF2); +} diff --git a/src/host/CopyToCharPopup.hpp b/src/host/CopyToCharPopup.hpp new file mode 100644 index 000000000..a961bc119 --- /dev/null +++ b/src/host/CopyToCharPopup.hpp @@ -0,0 +1,32 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CopyToCharPopup.hpp + +Abstract: +- Popup used for use copying to char input +- contains code pulled from popup.cpp and cmdline.cpp + +Author: +- Austin Diviness (AustDi) 18-Aug-2018 +--*/ + +#pragma once + +#include "popup.h" + +class CopyToCharPopup final : public Popup +{ +public: + CopyToCharPopup(SCREEN_INFORMATION& screenInfo); + + [[nodiscard]] + NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; +protected: + void _DrawContent() override; + +private: + void _copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch); +}; diff --git a/src/host/CursorBlinker.cpp b/src/host/CursorBlinker.cpp new file mode 100644 index 000000000..c6ae247cc --- /dev/null +++ b/src/host/CursorBlinker.cpp @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "../host/scrolling.hpp" +#include "../interactivity/inc/ServiceLocator.hpp" +#pragma hdrstop +using namespace Microsoft::Console; + +CursorBlinker::CursorBlinker() : + _hCaretBlinkTimer(INVALID_HANDLE_VALUE), + _hCaretBlinkTimerQueue(THROW_LAST_ERROR_IF_NULL(CreateTimerQueue())), + _uCaretBlinkTime(INFINITE) // default to no blink +{ +} + +CursorBlinker::~CursorBlinker() +{ + if (_hCaretBlinkTimerQueue) + { + DeleteTimerQueueEx(_hCaretBlinkTimerQueue, INVALID_HANDLE_VALUE); + } +} + +void CursorBlinker::UpdateSystemMetrics() +{ + // This can be -1 in a TS session + _uCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); +} + +void CursorBlinker::SettingsChanged() +{ + DWORD const dwCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); + + if (dwCaretBlinkTime != _uCaretBlinkTime) + { + KillCaretTimer(); + _uCaretBlinkTime = dwCaretBlinkTime; + SetCaretTimer(); + } +} + +void CursorBlinker::FocusEnd() +{ + KillCaretTimer(); +} + +void CursorBlinker::FocusStart() +{ + SetCaretTimer(); +} + +// Routine Description: +// - This routine is called when the timer in the console with the focus goes off. It blinks the cursor. +// Arguments: +// - ScreenInfo - reference to screen info structure. +// Return Value: +// - +void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) +{ + Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor(); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto* const _pAccessibilityNotifier = ServiceLocator::LocateAccessibilityNotifier(); + + if (!WI_IsFlagSet(gci.Flags, CONSOLE_HAS_FOCUS)) + { + goto DoScroll; + } + + // Update the cursor pos in USER so accessibility will work. + if (cursor.HasMoved()) + { + const auto position = cursor.GetPosition(); + const auto viewport = ScreenInfo.GetViewport(); + const auto fontSize = ScreenInfo.GetScreenFontSize(); + cursor.SetHasMoved(false); + + RECT rc; + rc.left = (position.X - viewport.Left()) * fontSize.X; + rc.top = (position.Y - viewport.Top()) * fontSize.Y; + rc.right = rc.left + fontSize.X; + rc.bottom = rc.top + fontSize.Y; + + _pAccessibilityNotifier->NotifyConsoleCaretEvent(rc); + + // Send accessibility information + { + IAccessibilityNotifier::ConsoleCaretEventFlags flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretInvisible; + + // Flags is expected to be 2, 1, or 0. 2 in selecting (whether or not visible), 1 if just visible, 0 if invisible/noselect. + if (WI_IsFlagSet(gci.Flags, CONSOLE_SELECTING)) + { + flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection; + } + else if (cursor.IsVisible()) + { + flags = IAccessibilityNotifier::ConsoleCaretEventFlags::CaretVisible; + } + + _pAccessibilityNotifier->NotifyConsoleCaretEvent(flags, MAKELONG(position.X, position.Y)); + } + } + + // If the DelayCursor flag has been set, wait one more tick before toggle. + // This is used to guarantee the cursor is on for a finite period of time + // after a move and off for a finite period of time after a WriteString. + if (cursor.GetDelay()) + { + cursor.SetDelay(false); + goto DoScroll; + } + + // Don't blink the cursor for remote sessions. + if ((!ServiceLocator::LocateSystemConfigurationProvider()->IsCaretBlinkingEnabled() || + _uCaretBlinkTime == -1 || + (!cursor.IsBlinkingAllowed())) && + cursor.IsOn()) + { + goto DoScroll; + } + + // Blink only if the cursor isn't turned off via the API + if (cursor.IsVisible()) + { + cursor.SetIsOn(!cursor.IsOn()); + } + +DoScroll: + Scrolling::s_ScrollIfNecessary(ScreenInfo); +} + +void CALLBACK CursorTimerRoutineWrapper(_In_ PVOID /* lpParam */, _In_ BOOL /* TimerOrWaitFired */) +{ + // Suppose the following sequence of events takes place: + // + // 1. The user resizes the console; + // 2. The console acquires the console lock; + // 3. The current SCREEN_INFORMATION instance is deleted; + // 4. This causes the current Cursor instance to be deleted, too; + // 5. The Cursor's destructor is called; + // => Somewhere between 1 and 5, the timer fires: + // Timer queue timer callbacks execute asynchronously with respect to + // the UI thread under which the numbered steps are taking place. + // Because the callback touches console state, it needs to acquire the + // console lock. But what if the timer callback fires at just the right + // time such that 2 has already acquired the lock? + // 6. The Cursor's destructor deletes the timer queue and thereby destroys + // the timer queue timer used for blinking. However, because this + // timer's callback modifies console state, it is prudent to not + // continue the destruction if the callback has already started but has + // not yet finished. Therefore, the destructor waits for the callback to + // finish executing. + // => Meanwhile, the callback just happens to be stuck waiting for the + // console lock acquired in step 2. Since the destructor is waiting on + // the callback to complete, and the callback is waiting on the lock, + // which will only be released way after the Cursor instance is deleted, + // the console has now deadlocked. + // + // As a solution, skip the blink if the console lock is already being held. + // Note that critical sections to not have a waitable synchronization + // object unless there readily is contention on it. As a result, if we + // wanted to wait until the lock became available under the condition of + // not being destroyed, things get too complicated. + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + if (gci.TryLockConsole() != false) + { + // Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + gci.GetCursorBlinker().TimerRoutine(gci.GetActiveOutputBuffer()); + + // This was originally just UnlockConsole, not CONSOLE_INFORMATION::UnlockConsole + // Is there a reason it would need to be the global version? + gci.UnlockConsole(); + } +} + +// Routine Description: +// - If guCaretBlinkTime is -1, we don't want to blink the caret. However, we +// need to make sure it gets drawn, so we'll set a short timer. When that +// goes off, we'll hit CursorTimerRoutine, and it'll do the right thing if +// guCaretBlinkTime is -1. +void CursorBlinker::SetCaretTimer() +{ + static const DWORD dwDefTimeout = 0x212; + + KillCaretTimer(); + + if (_hCaretBlinkTimer == INVALID_HANDLE_VALUE) + { + bool bRet = true; + DWORD dwEffectivePeriod = _uCaretBlinkTime == -1 ? dwDefTimeout : _uCaretBlinkTime; + + bRet = CreateTimerQueueTimer(&_hCaretBlinkTimer, + _hCaretBlinkTimerQueue, + (WAITORTIMERCALLBACKFUNC)CursorTimerRoutineWrapper, + this, + dwEffectivePeriod, + dwEffectivePeriod, + 0); + + LOG_LAST_ERROR_IF(!bRet); + } +} + +void CursorBlinker::KillCaretTimer() +{ + if (_hCaretBlinkTimer != INVALID_HANDLE_VALUE) + { + bool bRet = true; + + bRet = DeleteTimerQueueTimer(_hCaretBlinkTimerQueue, + _hCaretBlinkTimer, + NULL); + + // According to https://msdn.microsoft.com/en-us/library/windows/desktop/ms682569(v=vs.85).aspx + // A failure to delete the timer with the LastError being ERROR_IO_PENDING means that the timer is + // currently in use and will get cleaned up when released. Delete should not be called again. + // We treat that case as a success. + if (bRet == false && GetLastError() != ERROR_IO_PENDING) + { + LOG_LAST_ERROR(); + } + else + { + _hCaretBlinkTimer = INVALID_HANDLE_VALUE; + } + } +} diff --git a/src/host/CursorBlinker.hpp b/src/host/CursorBlinker.hpp new file mode 100644 index 000000000..6f8ca4a88 --- /dev/null +++ b/src/host/CursorBlinker.hpp @@ -0,0 +1,39 @@ +/* +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- CursorBlinker.hpp +Abstract: +- Encapsulates all of the behavior needed to blink the cursor, and update the + blink rate to account for different system settings. + +Author(s): +- Mike Griese (migrie) Nov 2018 +*/ + +namespace Microsoft::Console +{ + class CursorBlinker final + { + public: + CursorBlinker(); + ~CursorBlinker(); + + void FocusStart(); + void FocusEnd(); + + void UpdateSystemMetrics(); + void SettingsChanged(); + void TimerRoutine(SCREEN_INFORMATION& ScreenInfo); + + private: + // These use Timer Queues: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms687003(v=vs.85).aspx + HANDLE _hCaretBlinkTimer; // timer used to periodically blink the cursor + HANDLE _hCaretBlinkTimerQueue; // timer queue where the blink timer lives + UINT _uCaretBlinkTime; + void SetCaretTimer(); + void KillCaretTimer(); + }; +} diff --git a/src/host/IIoProvider.hpp b/src/host/IIoProvider.hpp new file mode 100644 index 000000000..db0fbf5d7 --- /dev/null +++ b/src/host/IIoProvider.hpp @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IIoProvider.hpp + +Abstract: +- Provides an abstraction for aquiring the active input and output objects of + the console. + +Author(s): +- Mike Griese (migrie) 11 Oct 2017 +--*/ +#pragma once + +class SCREEN_INFORMATION; +class InputBuffer; + +namespace Microsoft::Console +{ + class IIoProvider + { + public: + virtual SCREEN_INFORMATION& GetActiveOutputBuffer() = 0; + virtual const SCREEN_INFORMATION& GetActiveOutputBuffer() const = 0; + virtual InputBuffer* const GetActiveInputBuffer() const = 0; + }; +} diff --git a/src/host/PtySignalInputThread.cpp b/src/host/PtySignalInputThread.cpp new file mode 100644 index 000000000..52dc0e36b --- /dev/null +++ b/src/host/PtySignalInputThread.cpp @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "PtySignalInputThread.hpp" + +#include "output.h" +#include "handle.h" +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "..\terminal\adapter\DispatchCommon.hpp" + +#define PTY_SIGNAL_RESIZE_WINDOW 8u + +struct PTY_SIGNAL_RESIZE +{ + unsigned short sx; + unsigned short sy; +}; + +using namespace Microsoft::Console; +using namespace Microsoft::Console::Interactivity; + +// Constructor Description: +// - Creates the PTY Signal Input Thread. +// Arguments: +// - hPipe - a handle to the file representing the read end of the VT pipe. +PtySignalInputThread::PtySignalInputThread(_In_ wil::unique_hfile hPipe) : + _hFile{ std::move(hPipe) }, + _hThread{}, + _pConApi{ std::make_unique(ServiceLocator::LocateGlobals().getConsoleInformation()) }, + _dwThreadId{ 0 }, + _consoleConnected{ false } +{ + THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE); + THROW_IF_NULL_ALLOC(_pConApi.get()); +} + +PtySignalInputThread::~PtySignalInputThread() +{ + // Manually terminate our thread during unittesting. Otherwise, the test + // will finish, but TAEF will not manually kill the test. +#ifdef UNIT_TESTING + TerminateThread(_hThread.get(), 0); +#endif +} + +// Function Description: +// - Static function used for initializing an instance's ThreadProc. +// Arguments: +// - lpParameter - A pointer to the PtySignalInputTHread instance that should be called. +// Return Value: +// - The return value of the underlying instance's _InputThread +DWORD PtySignalInputThread::StaticThreadProc(_In_ LPVOID lpParameter) +{ + PtySignalInputThread* const pInstance = reinterpret_cast(lpParameter); + return pInstance->_InputThread(); +} + +// Method Description: +// - Tell us that there's a client attached to the console, so we can actually +// do something with the messages we recieve now. Before this is set, there +// is no guarantee that a client has attached, so most parts of the console +// (in and screen buffers) haven't yet been initialized. +// Arguments: +// - +// Return Value: +// - +void PtySignalInputThread::ConnectConsole() noexcept +{ + _consoleConnected = true; +} + +// Method Description: +// - The ThreadProc for the PTY Signal Input Thread. +// Return Value: +// - S_OK if the thread runs to completion. +// - Otherwise it may cause an application termination another route and never return. +[[nodiscard]] +HRESULT PtySignalInputThread::_InputThread() +{ + unsigned short signalId; + while (_GetData(&signalId, sizeof(signalId))) + { + switch (signalId) + { + case PTY_SIGNAL_RESIZE_WINDOW: + { + PTY_SIGNAL_RESIZE resizeMsg = { 0 }; + _GetData(&resizeMsg, sizeof(resizeMsg)); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + // If the client app hasn't yet connected, stash the new size in the launchArgs. + // We'll later use the value in launchArgs to set up the console buffer + if (!_consoleConnected) + { + short sColumns = 0; + short sRows = 0; + if (SUCCEEDED(UShortToShort(resizeMsg.sx, &sColumns)) && + SUCCEEDED(UShortToShort(resizeMsg.sy, &sRows)) && + (sColumns > 0 && sRows > 0)) + { + ServiceLocator::LocateGlobals().launchArgs.SetExpectedSize({sColumns, sRows}); + } + break; + } + else + { + if (DispatchCommon::s_ResizeWindow(*_pConApi, resizeMsg.sx, resizeMsg.sy)) + { + DispatchCommon::s_SuppressResizeRepaint(*_pConApi); + } + } + + break; + } + default: + { + THROW_HR(E_UNEXPECTED); + } + } + } + return S_OK; +} + +// Method Description: +// - Retrieves bytes from the file stream and exits or throws errors should the pipe state +// be compromised. +// Arguments: +// - pBuffer - Buffer to fill with data. +// - cbBuffer - Count of bytes in the given buffer. +// Return Value: +// - True if data was retrieved successfully. False otherwise. +bool PtySignalInputThread::_GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, + const DWORD cbBuffer) +{ + DWORD dwRead = 0; + // If we failed to read because the terminal broke our pipe (usually due + // to dying itself), close gracefully with ERROR_BROKEN_PIPE. + // Otherwise throw an exception. ERROR_BROKEN_PIPE is the only case that + // we want to gracefully close in. + if (FALSE == ReadFile(_hFile.get(), pBuffer, cbBuffer, &dwRead, nullptr)) + { + DWORD lastError = GetLastError(); + if (lastError == ERROR_BROKEN_PIPE) + { + _Shutdown(); + return false; + } + else + { + THROW_WIN32(lastError); + } + } + else if (dwRead != cbBuffer) + { + _Shutdown(); + return false; + } + + return true; +} + +// Method Description: +// - Starts the PTY Signal input thread. +[[nodiscard]] +HRESULT PtySignalInputThread::Start() noexcept +{ + RETURN_LAST_ERROR_IF(!_hFile); + + HANDLE hThread = nullptr; + // 0 is the right value, https://blogs.msdn.microsoft.com/oldnewthing/20040223-00/?p=40503 + DWORD dwThreadId = 0; + + hThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)PtySignalInputThread::StaticThreadProc, + this, + 0, + &dwThreadId); + + RETURN_LAST_ERROR_IF(hThread == INVALID_HANDLE_VALUE); + _hThread.reset(hThread); + _dwThreadId = dwThreadId; + + return S_OK; +} + +// Method Description: +// - Perform a shutdown of the console. This happens when the signal pipe is +// broken, which means either the parent terminal process has died, or they +// called ClosePseudoConsole. +// CloseConsoleProcessState is not enough by itself - it will disconnect +// clients as if the X was pressed, but then we need to actually still die, +// so then call RundownAndExit. +// Arguments: +// - +// Return Value: +// - +void PtySignalInputThread::_Shutdown() +{ + // Trigger process shutdown. + CloseConsoleProcessState(); + + // If we haven't terminated by now, that's because there's a client that's still attached. + // Force the handling of the control events by the attached clients. + // As of MSFT:19419231, CloseConsoleProcessState will make sure this + // happens if this method is called outside of lock, but if we're + // currently locked, we want to make sure ctrl events are handled + // _before_ we RundownAndExit. + ProcessCtrlEvents(); + + // Make sure we terminate. + ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE); +} diff --git a/src/host/PtySignalInputThread.hpp b/src/host/PtySignalInputThread.hpp new file mode 100644 index 000000000..8e1ac659f --- /dev/null +++ b/src/host/PtySignalInputThread.hpp @@ -0,0 +1,48 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PtySignalInputThread.hpp + +Abstract: +- Defines methods that wrap the thread that will wait for Pty Signals + if a Pty server (VT server) is running. + +Author(s): +- Mike Griese (migrie) 15 Aug 2017 +- Michael Niksa (miniksa) 19 Jan 2018 +--*/ +#pragma once + +namespace Microsoft::Console +{ + class PtySignalInputThread final + { + public: + PtySignalInputThread(_In_ wil::unique_hfile hPipe); + ~PtySignalInputThread(); + + [[nodiscard]] + HRESULT Start() noexcept; + static DWORD StaticThreadProc(_In_ LPVOID lpParameter); + + // Prevent copying and assignment. + PtySignalInputThread(const PtySignalInputThread&) = delete; + PtySignalInputThread& operator=(const PtySignalInputThread&) = delete; + + void ConnectConsole() noexcept; + + private: + [[nodiscard]] + HRESULT _InputThread(); + bool _GetData(_Out_writes_bytes_(cbBuffer) void* const pBuffer, const DWORD cbBuffer); + void _Shutdown(); + + wil::unique_hfile _hFile; + wil::unique_handle _hThread; + DWORD _dwThreadId; + bool _consoleConnected; + std::unique_ptr _pConApi; + }; +} diff --git a/src/host/ScreenBufferRenderTarget.cpp b/src/host/ScreenBufferRenderTarget.cpp new file mode 100644 index 000000000..f1a24255d --- /dev/null +++ b/src/host/ScreenBufferRenderTarget.cpp @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "ScreenBufferRenderTarget.hpp" +#include "../interactivity/inc/ServiceLocator.hpp" + +ScreenBufferRenderTarget::ScreenBufferRenderTarget(SCREEN_INFORMATION& owner) : + _owner{ owner } +{ +} + +void ScreenBufferRenderTarget::TriggerRedraw(const Microsoft::Console::Types::Viewport& region) +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerRedraw(region); + } +} + +void ScreenBufferRenderTarget::TriggerRedraw(const COORD* const pcoord) +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerRedraw(pcoord); + } +} + +void ScreenBufferRenderTarget::TriggerRedrawCursor(const COORD* const pcoord) +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerRedrawCursor(pcoord); + } +} + +void ScreenBufferRenderTarget::TriggerRedrawAll() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerRedrawAll(); + } +} + +void ScreenBufferRenderTarget::TriggerTeardown() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerTeardown(); + } +} + +void ScreenBufferRenderTarget::TriggerSelection() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerSelection(); + } +} + +void ScreenBufferRenderTarget::TriggerScroll() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerScroll(); + } +} + +void ScreenBufferRenderTarget::TriggerScroll(const COORD* const pcoordDelta) +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerScroll(pcoordDelta); + } +} + +void ScreenBufferRenderTarget::TriggerCircling() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerCircling(); + } +} + +void ScreenBufferRenderTarget::TriggerTitleChange() +{ + auto* pRenderer = ServiceLocator::LocateGlobals().pRender; + const auto* pActive = &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + if (pRenderer != nullptr && pActive == &_owner) + { + pRenderer->TriggerTitleChange(); + } +} diff --git a/src/host/ScreenBufferRenderTarget.hpp b/src/host/ScreenBufferRenderTarget.hpp new file mode 100644 index 000000000..434df4c37 --- /dev/null +++ b/src/host/ScreenBufferRenderTarget.hpp @@ -0,0 +1,46 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ScreenBufferRenderTarget.hpp + +Abstract: +Provides an encapsulation for all of the RenderTarget methods for the SCreenBuffer. +Unfortunately, these cannot be defined directly on the SCREEN_INFORMATION due to +MSFT 9358743. +Adding an interface to SCREEN_INFORMATION makes the ConsoleObjectHeader no +longer the first part of the SCREEN_INFORMATION. +The Screen buffer will pass this object to other objects that need to trigger +redrawing the buffer contents. + +Author(s): +- Mike Griese (migrie) Nov 2018 +--*/ + +#pragma once +#include "../renderer/inc/IRenderTarget.hpp" + +// fwdecl +class SCREEN_INFORMATION; + +class ScreenBufferRenderTarget final : public Microsoft::Console::Render::IRenderTarget +{ +public: + ScreenBufferRenderTarget(SCREEN_INFORMATION& owner); + + void TriggerRedraw(const Microsoft::Console::Types::Viewport& region) override; + void TriggerRedraw(const COORD* const pcoord) override; + void TriggerRedrawCursor(const COORD* const pcoord) override; + void TriggerRedrawAll() override; + void TriggerTeardown() override; + void TriggerSelection() override; + void TriggerScroll() override; + void TriggerScroll(const COORD* const pcoordDelta) override; + void TriggerCircling() override; + void TriggerTitleChange() override; + +private: + SCREEN_INFORMATION& _owner; + +}; diff --git a/src/host/VtInputThread.cpp b/src/host/VtInputThread.cpp new file mode 100644 index 000000000..448bd57ce --- /dev/null +++ b/src/host/VtInputThread.cpp @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "VtInputThread.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "input.h" +#include "../terminal/parser/InputStateMachineEngine.hpp" +#include "outputStream.hpp" // For ConhostInternalGetSet +#include "../terminal/adapter/InteractDispatch.hpp" +#include "../types/inc/convert.hpp" +#include "server.h" +#include "output.h" +#include "handle.h" + +using namespace Microsoft::Console; + +// Constructor Description: +// - Creates the VT Input Thread. +// Arguments: +// - hPipe - a handle to the file representing the read end of the VT pipe. +// - inheritCursor - a bool indicating if the state machine should expect a +// cursor positioning sequence. See MSFT:15681311. +VtInputThread::VtInputThread(_In_ wil::unique_hfile hPipe, + const bool inheritCursor) : + _hFile{ std::move(hPipe) }, + _hThread{}, + _utf8Parser{ CP_UTF8 }, + _dwThreadId{ 0 }, + _exitRequested{ false }, + _exitResult{ S_OK } +{ + THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + auto pGetSet = std::make_unique(gci); + THROW_IF_NULL_ALLOC(pGetSet.get()); + + auto engine = std::make_unique(new InteractDispatch(pGetSet.release()), inheritCursor); + THROW_IF_NULL_ALLOC(engine.get()); + + _pInputStateMachine = std::make_unique(engine.release()); + THROW_IF_NULL_ALLOC(_pInputStateMachine.get()); +} + +// Method Description: +// - Processes a buffer of input characters. The characters should be utf-8 +// encoded, and will get converted to wchar_t's to be processed by the +// input state machine. +// Arguments: +// - charBuffer - the UTF-8 characters recieved. +// - cch - number of UTF-8 characters in charBuffer +// Return Value: +// - S_OK on success, otherwise an appropriate failure. +[[nodiscard]] +HRESULT VtInputThread::_HandleRunInput(_In_reads_(cch) const byte* const charBuffer, const int cch) +{ + // Make sure to call the GLOBAL Lock/Unlock, not the gci's lock/unlock. + // Only the global unlock attempts to dispatch ctrl events. If you use the + // gci's unlock, when you press C-c, it won't be dispatched until the + // next console API call. For something like `powershell sleep 60`, + // that won't happen for 60s + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + std::unique_ptr pwsSequence; + unsigned int cchConsumed; + unsigned int cchSequence; + auto hr = _utf8Parser.Parse(charBuffer, cch, cchConsumed, pwsSequence, cchSequence); + // If we hit a parsing error, eat it. It's bad utf-8, we can't do anything with it. + if (FAILED(hr)) + { + return S_FALSE; + } + _pInputStateMachine->ProcessString(pwsSequence.get(), cchSequence); + } + CATCH_RETURN(); + + return S_OK; +} + +// Function Description: +// - Static function used for initializing an instance's ThreadProc. +// Arguments: +// - lpParameter - A pointer to the VtInputThread instance that should be called. +// Return Value: +// - The return value of the underlying instance's _InputThread +DWORD VtInputThread::StaticVtInputThreadProc(_In_ LPVOID lpParameter) +{ + VtInputThread* const pInstance = reinterpret_cast(lpParameter); + return pInstance->_InputThread(); +} + +// Method Description: +// - Do a single ReadFile from our pipe, and try and handle it. If handling +// failed, throw or log, depending on what the caller wants. +// Arguments: +// - throwOnFail: If true, throw an exception if there was an error processing +// the input recieved. Otherwise, log the error. +// Return Value: +// - +void VtInputThread::DoReadInput(const bool throwOnFail) +{ + byte buffer[256]; + DWORD dwRead = 0; + bool fSuccess = !!ReadFile(_hFile.get(), buffer, ARRAYSIZE(buffer), &dwRead, nullptr); + + // If we failed to read because the terminal broke our pipe (usually due + // to dying itself), close gracefully with ERROR_BROKEN_PIPE. + // Otherwise throw an exception. ERROR_BROKEN_PIPE is the only case that + // we want to gracefully close in. + if (!fSuccess) + { + _exitRequested = true; + _exitResult = HRESULT_FROM_WIN32(GetLastError()); + return; + } + + HRESULT hr = _HandleRunInput(buffer, dwRead); + if (FAILED(hr)) + { + if (throwOnFail) + { + _exitResult = hr; + _exitRequested = true; + } + else + { + LOG_IF_FAILED(hr); + } + } +} + +// Method Description: +// - The ThreadProc for the VT Input Thread. Reads input from the pipe, and +// passes it to _HandleRunInput to be processed by the +// InputStateMachineEngine. +// Return Value: +// - Any error from reading the pipe or writing to the input buffer that might +// have caused us to exit. +DWORD VtInputThread::_InputThread() +{ + while (!_exitRequested) + { + DoReadInput(true); + } + ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo()->CloseInput(); + + return _exitResult; +} + +// Method Description: +// - Starts the VT input thread. +[[nodiscard]] +HRESULT VtInputThread::Start() +{ + RETURN_HR_IF(E_HANDLE, !_hFile); + + HANDLE hThread = nullptr; + // 0 is the right value, https://blogs.msdn.microsoft.com/oldnewthing/20040223-00/?p=40503 + DWORD dwThreadId = 0; + + hThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)VtInputThread::StaticVtInputThreadProc, + this, + 0, + &dwThreadId); + + RETURN_LAST_ERROR_IF(hThread == INVALID_HANDLE_VALUE); + _hThread.reset(hThread); + _dwThreadId = dwThreadId; + + return S_OK; +} diff --git a/src/host/VtInputThread.hpp b/src/host/VtInputThread.hpp new file mode 100644 index 000000000..4c8640eba --- /dev/null +++ b/src/host/VtInputThread.hpp @@ -0,0 +1,47 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- VtInputThread.hpp + +Abstract: +- Defines methods that wrap the thread that reads VT input from a pipe and + feeds it into the console's input buffer. + +Author(s): +- Mike Griese (migrie) 15 Aug 2017 +--*/ +#pragma once + +#include "..\terminal\parser\StateMachine.hpp" +#include "utf8ToWideCharParser.hpp" + +namespace Microsoft::Console +{ + class VtInputThread + { + public: + VtInputThread(_In_ wil::unique_hfile hPipe, const bool inheritCursor); + + [[nodiscard]] + HRESULT Start(); + static DWORD StaticVtInputThreadProc(_In_ LPVOID lpParameter); + void DoReadInput(const bool throwOnFail); + + private: + [[nodiscard]] + HRESULT _HandleRunInput(_In_reads_(cch) const byte* const charBuffer, const int cch); + DWORD _InputThread(); + + wil::unique_hfile _hFile; + wil::unique_handle _hThread; + DWORD _dwThreadId; + + bool _exitRequested; + HRESULT _exitResult; + + std::unique_ptr _pInputStateMachine; + Utf8ToWideCharParser _utf8Parser; + }; +} diff --git a/src/host/VtIo.cpp b/src/host/VtIo.cpp new file mode 100644 index 000000000..b1f872dc7 --- /dev/null +++ b/src/host/VtIo.cpp @@ -0,0 +1,403 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "VtIo.hpp" +#include "../interactivity/inc/ServiceLocator.hpp" + +#include "../renderer/vt/XtermEngine.hpp" +#include "../renderer/vt/Xterm256Engine.hpp" +#include "../renderer/vt/WinTelnetEngine.hpp" + +#include "../renderer/base/renderer.hpp" +#include "../types/inc/utils.hpp" +#include "input.h" // ProcessCtrlEvents +#include "output.h" // CloseConsoleProcessState + +using namespace Microsoft::Console; +using namespace Microsoft::Console::VirtualTerminal; +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::Utils; + +VtIo::VtIo() : + _initialized(false), + _objectsCreated(false), + _lookingForCursorPosition(false), + _IoMode(VtIoMode::INVALID) +{ +} + +// Routine Description: +// Tries to get the VtIoMode from the given string. If it's not one of the +// *_STRING constants in VtIoMode.hpp, then it returns E_INVALIDARG. +// Arguments: +// VtIoMode: A string containing the console's requested VT mode. This can be +// any of the strings in VtIoModes.hpp +// pIoMode: recieves the VtIoMode that the string prepresents if it's a valid +// IO mode string +// Return Value: +// S_OK if we parsed the string successfully, otherwise E_INVALIDARG indicating failure. +[[nodiscard]] +HRESULT VtIo::ParseIoMode(const std::wstring& VtMode, _Out_ VtIoMode& ioMode) +{ + ioMode = VtIoMode::INVALID; + + if (VtMode == XTERM_256_STRING) + { + ioMode = VtIoMode::XTERM_256; + } + else if (VtMode == XTERM_STRING) + { + ioMode = VtIoMode::XTERM; + } + else if (VtMode == WIN_TELNET_STRING) + { + ioMode = VtIoMode::WIN_TELNET; + } + else if (VtMode == XTERM_ASCII_STRING) + { + ioMode = VtIoMode::XTERM_ASCII; + } + else if (VtMode == DEFAULT_STRING) + { + ioMode = VtIoMode::XTERM_256; + } + else + { + return E_INVALIDARG; + } + return S_OK; +} + +[[nodiscard]] +HRESULT VtIo::Initialize(const ConsoleArguments * const pArgs) +{ + _lookingForCursorPosition = pArgs->GetInheritCursor(); + + // If we were already given VT handles, set up the VT IO engine to use those. + if (pArgs->InConptyMode()) + { + return _Initialize(pArgs->GetVtInHandle(), pArgs->GetVtOutHandle(), pArgs->GetVtMode(), pArgs->GetSignalHandle()); + } + // Didn't need to initialize if we didn't have VT stuff. It's still OK, but report we did nothing. + else + { + return S_FALSE; + } +} + +// Routine Description: +// Tries to initialize this VtIo instance from the given pipe handles and +// VtIoMode. The pipes should have been created already (by the caller of +// conhost), in non-overlapped mode. +// The VtIoMode string can be the empty string as a default value. +// Arguments: +// InHandle: a valid file handle. The console will +// read VT sequences from this pipe to generate INPUT_RECORDs and other +// input events. +// OutHandle: a valid file handle. The console +// will be "rendered" to this pipe using VT sequences +// VtIoMode: A string containing the console's requested VT mode. This can be +// any of the strings in VtIoModes.hpp +// SignalHandle: an optional file handle that will be used to send signals into the console. +// This represents the ability to send signals to a *nix tty/pty. +// Return Value: +// S_OK if we initialized successfully, otherwise an appropriate HRESULT +// indicating failure. +[[nodiscard]] +HRESULT VtIo::_Initialize(const HANDLE InHandle, const HANDLE OutHandle, const std::wstring& VtMode, const HANDLE SignalHandle) +{ + FAIL_FAST_IF_MSG(_initialized, "Someone attempted to double-_Initialize VtIo"); + + RETURN_IF_FAILED(ParseIoMode(VtMode, _IoMode)); + + _hInput.reset(InHandle); + _hOutput.reset(OutHandle); + _hSignal.reset(SignalHandle); + + // The only way we're initialized is if the args said we're in conpty mode. + // If the args say so, then at least one of in, out, or signal was specified + _initialized = true; + return S_OK; +} + +// Method Description: +// - Create the VtRenderer and the VtInputThread for this console. +// MUST BE DONE AFTER CONSOLE IS INITIALIZED, to make sure we've gotten the +// buffer size from the attached client application. +// Arguments: +// - +// Return Value: +// S_OK if we initialized successfully, +// S_FALSE if VtIo hasn't been initialized (or we're not in conpty mode) +// otherwise an appropriate HRESULT indicating failure. +[[nodiscard]] +HRESULT VtIo::CreateIoHandlers() noexcept +{ + if (!_initialized) + { + return S_FALSE; + } + + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + if (IsValidHandle(_hInput.get())) + { + _pVtInputThread = std::make_unique(std::move(_hInput), _lookingForCursorPosition); + } + + if (IsValidHandle(_hOutput.get())) + { + Viewport initialViewport = Viewport::FromDimensions({0, 0}, + gci.GetWindowSize().X, + gci.GetWindowSize().Y); + switch (_IoMode) + { + case VtIoMode::XTERM_256: + _pVtRenderEngine = std::make_unique(std::move(_hOutput), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize())); + break; + case VtIoMode::XTERM: + _pVtRenderEngine = std::make_unique(std::move(_hOutput), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize()), + false); + break; + case VtIoMode::XTERM_ASCII: + _pVtRenderEngine = std::make_unique(std::move(_hOutput), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize()), + true); + break; + case VtIoMode::WIN_TELNET: + _pVtRenderEngine = std::make_unique(std::move(_hOutput), + gci, + initialViewport, + gci.GetColorTable(), + static_cast(gci.GetColorTableSize())); + break; + default: + return E_FAIL; + } + if (_pVtRenderEngine) + { + _pVtRenderEngine->SetTerminalOwner(this); + } + } + } + CATCH_RETURN(); + + _objectsCreated = true; + return S_OK; +} + +bool VtIo::IsUsingVt() const +{ + return _objectsCreated; +} + +// Routine Description: +// Potentially starts this VtIo's input thread and render engine. +// If the VtIo hasn't yet been given pipes, then this function will +// silently do nothing. It's the responsibility of the caller to make sure +// that the pipes are initialized first with VtIo::Initialize +// Arguments: +// +// Return Value: +// S_OK if we started successfully or had nothing to start, otherwise an +// appropriate HRESULT indicating failure. +[[nodiscard]] +HRESULT VtIo::StartIfNeeded() +{ + // If we haven't been set up, do nothing (because there's nothing to start) + if (!_objectsCreated) + { + return S_FALSE; + } + Globals& g = ServiceLocator::LocateGlobals(); + + if (_pVtRenderEngine) + { + try + { + g.pRender->AddRenderEngine(_pVtRenderEngine.get()); + g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(_pVtRenderEngine.get()); + } + CATCH_RETURN(); + } + + // MSFT: 15813316 + // If the terminal application wants us to inherit the cursor position, + // we're going to emit a VT sequence to ask for the cursor position, then + // read input until we get a response. Terminals who request this behavior + // but don't respond will hang. + // If we get a response, the InteractDispatch will call SetCursorPosition, + // which will call to our VtIo::SetCursorPosition method. + // We need both handles for this initialization to work. If we don't have + // both, we'll skip it. They either aren't going to be reading output + // (so they can't get the DSR) or they can't write the response to us. + if (_lookingForCursorPosition && _pVtRenderEngine && _pVtInputThread) + { + LOG_IF_FAILED(_pVtRenderEngine->RequestCursor()); + while(_lookingForCursorPosition) + { + _pVtInputThread->DoReadInput(false); + } + } + + if (_pVtInputThread) + { + LOG_IF_FAILED(_pVtInputThread->Start()); + } + + if (_pPtySignalInputThread) + { + // Let the signal thread know that the console is connected + _pPtySignalInputThread->ConnectConsole(); + } + + return S_OK; +} + +// Method Description: +// - Create and start the signal thread. The signal thread can be created +// independent of the i/o threads, and doesn't require a client first +// attaching to the console. We need to create it first and foremost, +// because it's possible that a terminal application could +// CreatePseudoConsole, then ClosePseudoConsole without ever attaching a +// client. Should that happen, we still need to exit. +// Arguments: +// - +// Return Value: +// - S_FALSE if we're not in VtIo mode, +// S_OK if we succeeded, +// otherwise an appropriate HRESULT indicating failure. +[[nodiscard]] +HRESULT VtIo::CreateAndStartSignalThread() noexcept +{ + if (!_initialized) + { + return S_FALSE; + } + + // If we were passed a signal handle, try to open it and make a signal reading thread. + if (IsValidHandle(_hSignal.get())) + { + try + { + _pPtySignalInputThread = std::make_unique(std::move(_hSignal)); + + // Start it if it was successfully created. + RETURN_IF_FAILED(_pPtySignalInputThread->Start()); + } + CATCH_RETURN(); + } + + return S_OK; +} + +// Method Description: +// - Prevent the renderer from emitting output on the next resize. This prevents +// the host from echoing a resize to the terminal that requested it. +// Arguments: +// - +// Return Value: +// - S_OK if the renderer successfully suppressed the next repaint, otherwise an +// appropriate HRESULT indicating failure. +[[nodiscard]] +HRESULT VtIo::SuppressResizeRepaint() +{ + HRESULT hr = S_OK; + if (_pVtRenderEngine) + { + hr = _pVtRenderEngine->SuppressResizeRepaint(); + } + return hr; +} + +// Method Description: +// - Attempts to set the initial cursor position, if we're looking for it. +// If we're not trying to inherit the cursor, does nothing. +// Arguments: +// - coordCursor: The initial position of the cursor. +// Return Value: +// - S_OK if we successfully inherited the cursor or did nothing, else an +// appropriate HRESULT +[[nodiscard]] +HRESULT VtIo::SetCursorPosition(const COORD coordCursor) +{ + HRESULT hr = S_OK; + if (_lookingForCursorPosition) + { + if (_pVtRenderEngine) + { + hr = _pVtRenderEngine->InheritCursor(coordCursor); + } + + _lookingForCursorPosition = false; + } + return hr; +} + +void VtIo::CloseInput() +{ + // This will release the lock when it goes out of scope + std::lock_guard lk(_shutdownLock); + _pVtInputThread = nullptr; + _ShutdownIfNeeded(); +} + +void VtIo::CloseOutput() +{ + // This will release the lock when it goes out of scope + std::lock_guard lk(_shutdownLock); + + Globals& g = ServiceLocator::LocateGlobals(); + // DON'T RemoveRenderEngine, as that requires the engine list lock, and this + // is usually being triggered on a paint operation, when the lock is already + // owned by the paint. + // Instead we're releasing the Engine here. A pointer to it has already been + // given to the Renderer, so we don't want the unique_ptr to delete it. The + // Renderer will own it's lifetime now. + _pVtRenderEngine.release(); + + g.getConsoleInformation().GetActiveOutputBuffer().SetTerminalConnection(nullptr); + + _ShutdownIfNeeded(); +} + + +void VtIo::_ShutdownIfNeeded() +{ + // The callers should have both accquired the _shutdownLock at this point - + // we dont want a race on who is actually responsible for closing it. + if (_objectsCreated && _pVtInputThread == nullptr && _pVtRenderEngine == nullptr) + { + // At this point, we no longer have a renderer or inthread. So we've + // effectively been disconnected from the terminal. + + // If we have any remaining attached processes, this will prepare us to send a ctrl+close to them + // if we don't, this will cause us to rundown and exit. + CloseConsoleProcessState(); + + // If we haven't terminated by now, that's because there's a client that's still attached. + // Force the handling of the control events by the attached clients. + // As of MSFT:19419231, CloseConsoleProcessState will make sure this + // happens if this method is called outside of lock, but if we're + // currently locked, we want to make sure ctrl events are handled + // _before_ we RundownAndExit. + ProcessCtrlEvents(); + + // Make sure we terminate. + ServiceLocator::RundownAndExit(ERROR_BROKEN_PIPE); + } +} diff --git a/src/host/VtIo.hpp b/src/host/VtIo.hpp new file mode 100644 index 000000000..988e891d5 --- /dev/null +++ b/src/host/VtIo.hpp @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "..\inc\VtIoModes.hpp" +#include "..\inc\ITerminalOwner.hpp" +#include "..\renderer\vt\vtrenderer.hpp" +#include "VtInputThread.hpp" +#include "PtySignalInputThread.hpp" + +class ConsoleArguments; + +namespace Microsoft::Console::VirtualTerminal +{ + class VtIo : public Microsoft::Console::ITerminalOwner + { + public: + VtIo(); + virtual ~VtIo() override = default; + + [[nodiscard]] + HRESULT Initialize(const ConsoleArguments* const pArgs); + + + [[nodiscard]] + HRESULT CreateAndStartSignalThread() noexcept; + [[nodiscard]] + HRESULT CreateIoHandlers() noexcept; + + bool IsUsingVt() const; + + [[nodiscard]] + HRESULT StartIfNeeded(); + + [[nodiscard]] + static HRESULT ParseIoMode(const std::wstring& VtMode, _Out_ VtIoMode& ioMode); + + [[nodiscard]] + HRESULT SuppressResizeRepaint(); + [[nodiscard]] + HRESULT SetCursorPosition(const COORD coordCursor); + + void CloseInput() override; + void CloseOutput() override; + + private: + // After CreateIoHandlers is called, these will be invalid. + wil::unique_hfile _hInput; + wil::unique_hfile _hOutput; + // After CreateAndStartSignalThread is called, this will be invalid. + wil::unique_hfile _hSignal; + VtIoMode _IoMode; + + bool _initialized; + bool _objectsCreated; + + bool _lookingForCursorPosition; + std::mutex _shutdownLock; + + std::unique_ptr _pVtRenderEngine; + std::unique_ptr _pVtInputThread; + std::unique_ptr _pPtySignalInputThread; + + [[nodiscard]] + HRESULT _Initialize(const HANDLE InHandle, const HANDLE OutHandle, const std::wstring& VtMode); + [[nodiscard]] + HRESULT _Initialize(const HANDLE InHandle, const HANDLE OutHandle, const std::wstring& VtMode, _In_opt_ HANDLE SignalHandle); + + void _ShutdownIfNeeded(); + + #ifdef UNIT_TESTING + friend class VtIoTests; + #endif + }; +} diff --git a/src/host/_output.cpp b/src/host/_output.cpp new file mode 100644 index 000000000..ac988fc79 --- /dev/null +++ b/src/host/_output.cpp @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "_output.h" + +#include "dbcs.h" +#include "handle.h" +#include "misc.h" + +#include "../buffer/out/CharRow.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/Viewport.hpp" +#include "../types/inc/convert.hpp" +#include "../types/inc/Utf16Parser.hpp" + +#include +#include + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - This routine writes a screen buffer region to the screen. +// Arguments: +// - screenInfo - reference to screen buffer information. +// - srRegion - Region to write in screen buffer coordinates. Region is inclusive +// Return Value: +// - +void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region) +{ + DBGOUTPUT(("WriteToScreen\n")); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // update to screen, if we're not iconic. + if (!screenInfo.IsActiveScreenBuffer() || WI_IsFlagSet(gci.Flags, CONSOLE_IS_ICONIC)) + { + return; + } + + // clip region to fit within the viewport + const auto clippedRegion = screenInfo.GetViewport().Clamp(region); + if (!clippedRegion.IsValid()) + { + return; + } + + if (screenInfo.IsActiveScreenBuffer()) + { + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender->TriggerRedraw(region); + } + } + + WriteConvRegionToScreen(screenInfo, region); +} + +// Routine Description: +// - writes text attributes to the screen +// Arguments: +// - OutContext - the screen info to write to +// - attrs - the attrs to write to the screen +// - target - the starting coordinate in the screen +// - used - number of elements written +// Return Value: +// - S_OK, E_INVALIDARG or similar HRESULT error. +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const std::basic_string_view attrs, + const COORD target, + size_t& used) noexcept +{ + // Set used to 0 from the beginning in case we exit early. + used = 0; + + if (attrs.empty()) + { + return S_OK; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + auto& screenInfo = OutContext.GetActiveBuffer(); + const auto bufferSize = screenInfo.GetBufferSize(); + if (!bufferSize.IsInBounds(target)) + { + return E_INVALIDARG; + } + + const OutputCellIterator it(attrs, true); + const auto done = screenInfo.Write(it, target); + + used = done.GetCellDistance(it); + + return S_OK; +} + +// Routine Description: +// - writes text to the screen +// Arguments: +// - screenInfo - the screen info to write to +// - chars - the text to write to the screen +// - target - the starting coordinate in the screen +// - used - number of elements written +// Return Value: +// - S_OK, E_INVALIDARG or similar HRESULT error. +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const std::wstring_view chars, + const COORD target, + size_t& used) noexcept +{ + // Set used to 0 from the beginning in case we exit early. + used = 0; + + if (chars.empty()) + { + return S_OK; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + auto& screenInfo = OutContext.GetActiveBuffer(); + const auto bufferSize = screenInfo.GetBufferSize(); + if (!bufferSize.IsInBounds(target)) + { + return E_INVALIDARG; + } + + try + { + OutputCellIterator it(chars); + const auto finished = screenInfo.Write(it, target); + used = finished.GetInputDistance(it); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - writes text to the screen +// Arguments: +// - screenInfo - the screen info to write to +// - chars - the text to write to the screen +// - target - the starting coordinate in the screen +// - used - number of elements written +// Return Value: +// - S_OK, E_INVALIDARG or similar HRESULT error. +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const std::string_view chars, + const COORD target, + size_t& used) noexcept +{ + // Set used to 0 from the beginning in case we exit early. + used = 0; + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto codepage = gci.OutputCP; + try + { + // convert to wide chars so we can call the W version of this function + const auto wideChars = ConvertToW(codepage, chars); + + size_t wideCharsWritten = 0; + RETURN_IF_FAILED(WriteConsoleOutputCharacterWImpl(OutContext, wideChars, target, wideCharsWritten)); + + // Create a view over the wide chars and reduce it to the amount actually written (do in two steps to enforce bounds) + std::wstring_view writtenView(wideChars); + writtenView = writtenView.substr(0, wideCharsWritten); + + // Look over written wide chars to find equilalent count of ascii chars so we can properly report back + // how many elements were actually written + used = GetALengthFromW(codepage, writtenView); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - fills the screen buffer with the specified text attribute +// Arguments: +// - OutContext - reference to screen buffer information. +// - attribute - the text attribute to use to fill +// - lengthToWrite - the number of elements to write +// - startingCoordinate - Screen buffer coordinate to begin writing to. +// - cellsModified - the number of elements written +// Return Value: +// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.) +[[nodiscard]] +HRESULT ApiRoutines::FillConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const WORD attribute, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept +{ + // Set modified cells to 0 from the beginning. + cellsModified = 0; + + if (lengthToWrite == 0) + { + return S_OK; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + auto& screenBuffer = OutContext.GetActiveBuffer(); + const auto bufferSize = screenBuffer.GetBufferSize(); + if (!bufferSize.IsInBounds(startingCoordinate)) + { + return S_OK; + } + + try + { + TextAttribute useThisAttr(attribute); + + // Here we're being a little clever - + // Because RGB color can't roundtrip the API, certain VT sequences will forget the RGB color + // because their first call to GetScreenBufferInfo returned a legacy attr. + // If they're calling this with the default attrs, they likely wanted to use the RGB default attrs. + // This could create a scenario where someone emitted RGB with VT, + // THEN used the API to FillConsoleOutput with the default attrs, and DIDN'T want the RGB color + // they had set. + if (screenBuffer.InVTMode()) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto bufferLegacy = gci.GenerateLegacyAttributes(screenBuffer.GetAttributes()); + if (bufferLegacy == attribute) + { + useThisAttr = TextAttribute(screenBuffer.GetAttributes()); + } + + } + + const OutputCellIterator it(useThisAttr, lengthToWrite); + const auto done = screenBuffer.Write(it, startingCoordinate); + + cellsModified = done.GetCellDistance(it); + + // Notify accessibility + auto endingCoordinate = startingCoordinate; + bufferSize.MoveInBounds(cellsModified, endingCoordinate); + screenBuffer.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - fills the screen buffer with the specified wchar +// Arguments: +// - OutContext - reference to screen buffer information. +// - character - wchar to fill with +// - lengthToWrite - the number of elements to write +// - startingCoordinate - Screen buffer coordinate to begin writing to. +// - cellsModified - the number of elements written +// Return Value: +// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.) +[[nodiscard]] +HRESULT ApiRoutines::FillConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const wchar_t character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept +{ + // Set modified cells to 0 from the beginning. + cellsModified = 0; + + if (lengthToWrite == 0) + { + return S_OK; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // TODO: does this even need to be here or will it exit quickly? + auto& screenInfo = OutContext.GetActiveBuffer(); + const auto bufferSize = screenInfo.GetBufferSize(); + if (!bufferSize.IsInBounds(startingCoordinate)) + { + return S_OK; + } + + try + { + const OutputCellIterator it(character, lengthToWrite); + const auto done = screenInfo.Write(it, startingCoordinate); + cellsModified = done.GetInputDistance(it); + + // Notify accessibility + auto endingCoordinate = startingCoordinate; + bufferSize.MoveInBounds(cellsModified, endingCoordinate); + screenInfo.NotifyAccessibilityEventing(startingCoordinate.X, startingCoordinate.Y, endingCoordinate.X, endingCoordinate.Y); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - fills the screen buffer with the specified char +// Arguments: +// - OutContext - reference to screen buffer information. +// - character - ascii character to fill with +// - lengthToWrite - the number of elements to write +// - startingCoordinate - Screen buffer coordinate to begin writing to. +// - cellsModified - the number of elements written +// Return Value: +// - S_OK or suitable HRESULT code from failure to write (memory issues, invalid arg, etc.) +[[nodiscard]] +HRESULT ApiRoutines::FillConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const char character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept +{ + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // convert to wide chars and call W version + const auto wchs = ConvertToW(gci.OutputCP, { &character, 1 }); + + LOG_HR_IF(E_UNEXPECTED, wchs.size() > 1); + + return FillConsoleOutputCharacterWImpl(OutContext, wchs.at(0), lengthToWrite, startingCoordinate, cellsModified); +} diff --git a/src/host/_output.h b/src/host/_output.h new file mode 100644 index 000000000..eb5641830 --- /dev/null +++ b/src/host/_output.h @@ -0,0 +1,25 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- _output.h + +Abstract: +- These methods provide processing of the text buffer into the final screen rendering state +- For all languages (CJK and Western) +- Most GDI work is processed in these classes + +Author(s): +- KazuM Jun.11.1997 + +Revision History: +- Remove FE/Non-FE separation in preparation for refactoring. (MiNiksa, 2014) +--*/ + +#pragma once + +#include "screenInfo.hpp" +#include "../types/inc/viewport.hpp" + +void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Microsoft::Console::Types::Viewport& region); diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp new file mode 100644 index 000000000..c1c77431c --- /dev/null +++ b/src/host/_stream.cpp @@ -0,0 +1,1376 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiRoutines.h" + +#include "_stream.h" +#include "stream.h" +#include "writeData.hpp" + +#include "_output.h" +#include "output.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "utf8ToWidecharParser.hpp" + +#include "../types/inc/convert.hpp" +#include "../types/inc/GlyphWidth.hpp" +#include "../types/inc/Viewport.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop +using namespace Microsoft::Console::Types; + +// Used by WriteCharsLegacy. +#define IS_GLYPH_CHAR(wch) (((wch) < L' ') || ((wch) == 0x007F)) + +// Routine Description: +// - This routine updates the cursor position. Its input is the non-special +// cased new location of the cursor. For example, if the cursor were being +// moved one space backwards from the left edge of the screen, the X +// coordinate would be -1. This routine would set the X coordinate to +// the right edge of the screen and decrement the Y coordinate by one. +// Arguments: +// - screenInfo - reference to screen buffer information structure. +// - 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_ COORD coordCursor, + const BOOL fKeepCursorVisible, + _Inout_opt_ PSHORT psScrollY) +{ + const COORD bufferSize = screenInfo.GetBufferSize().Dimensions(); + if (coordCursor.X < 0) + { + if (coordCursor.Y > 0) + { + coordCursor.X = (SHORT)(bufferSize.X + coordCursor.X); + coordCursor.Y = (SHORT)(coordCursor.Y - 1); + } + else + { + coordCursor.X = 0; + } + } + else if (coordCursor.X >= bufferSize.X) + { + // at end of line. if wrap mode, wrap cursor. otherwise leave it where it is. + if (screenInfo.OutputMode & ENABLE_WRAP_AT_EOL_OUTPUT) + { + coordCursor.Y += coordCursor.X / bufferSize.X; + coordCursor.X = coordCursor.X % bufferSize.X; + } + else + { + coordCursor.X = screenInfo.GetTextBuffer().GetCursor().GetPosition().X; + } + } + + const auto bufferAttributes = screenInfo.GetAttributes(); + + const auto relativeMargins = screenInfo.GetRelativeScrollMargins(); + auto viewport = screenInfo.GetViewport(); + SMALL_RECT srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); + const bool fMarginsSet = srMargins.Bottom > srMargins.Top; + COORD currentCursor = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + const int iCurrentCursorY = currentCursor.Y; + const bool inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const bool fCursorInMargins = iCurrentCursorY <= srMargins.Bottom && iCurrentCursorY >= srMargins.Top; + const bool cursorAboveViewport = coordCursor.Y < 0 && inVtMode; + const bool fScrollDown = fMarginsSet && fCursorInMargins && (coordCursor.Y > srMargins.Bottom); + bool fScrollUp = fMarginsSet && fCursorInMargins && (coordCursor.Y < srMargins.Top); + + const bool fScrollUpWithoutMargins = (!fMarginsSet) && cursorAboveViewport; + // if we're in VT mode, AND MARGINS AREN'T SET and a Reverse Line Feed took the cursor up past the top of the viewport, + // VT style scroll the contents of the screen. + // This can happen in applications like `less`, that don't set margins, because they're going to + // scroll the entire screen anyways, so no need for them to ever set the margins. + if (fScrollUpWithoutMargins) + { + fScrollUp = true; + srMargins.Top = 0; + srMargins.Bottom = screenInfo.GetViewport().BottomInclusive(); + } + + const bool scrollDownAtTop = fScrollDown && relativeMargins.Top() == 0; + if (scrollDownAtTop) + { + // We're trying to scroll down, and the top margin is at the top of the viewport. + // In this case, we want the lines that are "scrolled off" to appear in + // the scrollback instead of being discarded. + // To do this, we're going to scroll everything starting at the bottom + // margin down, then move the viewport down. + + const SHORT delta = coordCursor.Y - srMargins.Bottom; + SMALL_RECT scrollRect{0}; + scrollRect.Left = 0; + scrollRect.Top = srMargins.Bottom + 1; // One below margins + scrollRect.Bottom = bufferSize.Y - 1; // -1, otherwise this would be an exclusive rect. + scrollRect.Right = bufferSize.X - 1; // -1, otherwise this would be an exclusive rect. + + // This is the Y position we're moving the contents below the bottom margin to. + SHORT moveToYPosition = scrollRect.Top + delta; + + // This is where the viewport will need to be to give the effect of + // scrolling the contents in the margins. + SHORT newViewTop = viewport.Top() + delta; + + // This is how many new lines need to be added to the buffer to support this operation. + const SHORT newRows = (viewport.BottomExclusive() + delta) - bufferSize.Y; + + // If we're near the bottom of the buffer, we might need to insert some + // new rows at the bottom. + // If we do this, then the viewport is now one line higher than it used + // to be, so it needs to move down by one less line. + for(auto i = 0; i < newRows; i++) + { + screenInfo.GetTextBuffer().IncrementCircularBuffer(); + moveToYPosition--; + newViewTop--; + scrollRect.Top--; + } + + const COORD newPostMarginsOrigin = { 0, moveToYPosition }; + const COORD newViewOrigin = { 0, newViewTop }; + + // Unset the margins to scroll the content below the margins, + // then restore them after. + screenInfo.SetScrollMargins(Viewport::FromInclusive({0})); + try + { + ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, bufferAttributes); + } + CATCH_LOG(); + screenInfo.SetScrollMargins(relativeMargins); + + // Move the viewport down + auto hr = screenInfo.SetViewportOrigin(true, newViewOrigin, true); + if (FAILED(hr)) + { + return NTSTATUS_FROM_HRESULT(hr); + } + // If we didn't actually move the viewport, it's because we're at the + // bottom of the buffer, and the top lines of the viewport have + // changed. Manually invalidate here, to make sure the screen + // displays the correct text. + if (newViewOrigin == viewport.Origin()) + { + Viewport invalid = Viewport::FromDimensions(viewport.Origin(), {viewport.Width(), delta}); + screenInfo.GetRenderTarget().TriggerRedraw(invalid); + } + + // reset where our local viewport is, and recalculate the cursor and + // margin positions. + viewport = screenInfo.GetViewport(); + if (newRows > 0) + { + currentCursor.Y -= newRows; + coordCursor.Y -= newRows; + } + srMargins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); + } + + // If we did the above scrollDownAtTop case, then we've already scrolled + // the margins content, and we can skip this. + if (fScrollUp || (fScrollDown && !scrollDownAtTop)) + { + SHORT diff = coordCursor.Y - (fScrollUp ? srMargins.Top : srMargins.Bottom); + + SMALL_RECT scrollRect = { 0 }; + scrollRect.Top = srMargins.Top; + scrollRect.Bottom = srMargins.Bottom; + scrollRect.Left = screenInfo.GetViewport().Left(); // NOTE: Left/Right Scroll margins don't do anything currently. + scrollRect.Right = screenInfo.GetViewport().RightInclusive(); + + COORD dest; + dest.X = scrollRect.Left; + dest.Y = scrollRect.Top - diff; + + SMALL_RECT clipRect = scrollRect; + // Typically ScrollRegion() clips by the scroll margins. However, if + // we're scrolling down at the top of the viewport, we'll need to + // not clip at the margins, instead move the contents of the margins + // up above the viewport. So we'll clear out the current margins, and + // set them to the viewport+(#diff rows above the viewport). + if (scrollDownAtTop) + { + clipRect.Top -= diff; + auto fakeMargins = srMargins; + fakeMargins.Top -= diff; + auto fakeRelative = viewport.ConvertToOrigin(Viewport::FromInclusive(fakeMargins)); + screenInfo.SetScrollMargins(fakeRelative); + } + + try + { + ScrollRegion(screenInfo, scrollRect, clipRect, dest, UNICODE_SPACE, bufferAttributes); + } + CATCH_LOG(); + + if (scrollDownAtTop) + { + // Undo the fake margins we set above + screenInfo.SetScrollMargins(relativeMargins); + } + coordCursor.Y -= diff; + } + + NTSTATUS Status = STATUS_SUCCESS; + + if (coordCursor.Y >= bufferSize.Y) + { + // At the end of the buffer. Scroll contents of screen buffer so new position is visible. + FAIL_FAST_IF(!(coordCursor.Y == bufferSize.Y)); + if (!StreamScrollRegion(screenInfo)) + { + Status = STATUS_NO_MEMORY; + } + + if (nullptr != psScrollY) + { + *psScrollY += (SHORT)(bufferSize.Y - coordCursor.Y - 1); + } + coordCursor.Y += (SHORT)(bufferSize.Y - coordCursor.Y - 1); + } + + const bool cursorMovedPastViewport = coordCursor.Y > screenInfo.GetViewport().BottomInclusive(); + const bool cursorMovedPastVirtualViewport = coordCursor.Y > screenInfo.GetVirtualViewport().BottomInclusive(); + if (NT_SUCCESS(Status)) + { + // if at right or bottom edge of window, scroll right or down one char. + if (cursorMovedPastViewport) + { + COORD WindowOrigin; + WindowOrigin.X = 0; + WindowOrigin.Y = coordCursor.Y - screenInfo.GetViewport().BottomInclusive(); + Status = screenInfo.SetViewportOrigin(false, WindowOrigin, true); + } + } + + if (NT_SUCCESS(Status)) + { + if (fKeepCursorVisible) + { + screenInfo.MakeCursorVisible(coordCursor); + } + Status = screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible); + + // MSFT:19989333 - Only re-initialize the cursor row if the cursor moved + // below the terminal section of the buffer (the virtual viewport), + // and the visible part of the buffer (the actual viewport). + // If this is only cursorMovedPastViewport, and you scroll up, then type + // a character, we'll re-initialize the line the cursor is on. + // If this is only cursorMovedPastVirtualViewport and you scroll down, + // (with terminal scrolling disabled) then all lines newly exposed + // will get their attributes constantly cleared out. + // Both cursorMovedPastViewport and cursorMovedPastVirtualViewport works + if (inVtMode && cursorMovedPastViewport && cursorMovedPastVirtualViewport) + { + screenInfo.InitializeCursorRowAttributes(); + } + } + + return Status; +} + +// Routine Description: +// - This routine writes a string to the screen, processing any embedded +// unicode characters. The string is also copied to the input buffer, if +// the output mode is line mode. +// Arguments: +// - screenInfo - reference to screen buffer information structure. +// - pwchBufferBackupLimit - Pointer to beginning of buffer. +// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. +// This pointer is updated to point to the next position in the buffer. +// - pwchRealUnicode - Pointer to string to write. +// - 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_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge +// WC_ECHO if called by Read (echoing characters) +// Return Value: +// Note: +// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. +[[nodiscard]] +NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, + _In_range_(<= , pwchBuffer) const wchar_t* const pwchBufferBackupLimit, + _In_ const wchar_t* pwchBuffer, + _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, + _Inout_ size_t* const pcb, + _Out_opt_ size_t* const pcSpaces, + const SHORT sOriginalXPosition, + const DWORD dwFlags, + _Inout_opt_ PSHORT const psScrollY) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + TextBuffer& textBuffer = screenInfo.GetTextBuffer(); + Cursor& cursor = textBuffer.GetCursor(); + COORD CursorPosition = cursor.GetPosition(); + NTSTATUS Status = STATUS_SUCCESS; + SHORT XPosition; + WCHAR LocalBuffer[LOCAL_BUFFER_SIZE]; + size_t TempNumSpaces = 0; + const bool fUnprocessed = WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_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 TextAttribute Attributes = screenInfo.GetAttributes(); + const size_t BufferSize = *pcb; + *pcb = 0; + + const wchar_t* lpString = pwchRealUnicode; + + const COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); + + while (*pcb < BufferSize) + { + // correct for delayed EOL + if (cursor.IsDelayedEOLWrap()) + { + const COORD 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) + { + bool fDoEolWrap = false; + + if (WI_IsFlagSet(dwFlags, WC_DELAY_EOL_WRAP)) + { + // Correct if it's a printable character and whoever called us still understands/wants delayed EOL wrap. + if (*lpString >= UNICODE_SPACE) + { + fDoEolWrap = true; + } + else if (*lpString == UNICODE_BACKSPACE) + { + // if we have an active wrap and a backspace comes in, process it by moving the cursor + // back one cell position unless it's already at the start of a row. + *pcb += sizeof(WCHAR); + lpString++; + pwchRealUnicode++; + if (CursorPosition.X != 0) + { + --CursorPosition.X; + Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); + CursorPosition = cursor.GetPosition(); + } + continue; + } + } + else + { + // Uh oh, we've hit a consumer that doesn't know about delayed end of lines. To rectify this, just quickly jump + // forward to the next line as if we had done it earlier, then let everything else play out normally. + fDoEolWrap = true; + } + + if (fDoEolWrap) + { + CursorPosition.X = 0; + CursorPosition.Y++; + + Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); + + CursorPosition = cursor.GetPosition(); + } + } + } + + if (screenInfo.InVTMode()) + { + // if we're at the beginning of a row and we get a backspace and told to limit backspacing, skip it + if (*lpString == UNICODE_BACKSPACE && CursorPosition.X == 0 && WI_IsFlagSet(dwFlags, WC_LIMIT_BACKSPACE)) + { + *pcb += sizeof(wchar_t); + ++lpString; + ++pwchRealUnicode; + continue; + } + } + + // As an optimization, collect characters in buffer and print out all at once. + XPosition = cursor.GetPosition().X; + size_t i = 0; + wchar_t* LocalBufPtr = LocalBuffer; + while (*pcb < BufferSize && i < LOCAL_BUFFER_SIZE && XPosition < coordScreenBufferSize.X) + { +#pragma prefast(suppress:26019, "Buffer is taken in multiples of 2. Validation is ok.") + const wchar_t Char = *lpString; + const wchar_t RealUnicodeChar = *pwchRealUnicode; + if (!IS_GLYPH_CHAR(RealUnicodeChar) || fUnprocessed) + { + if (IsGlyphFullWidth(Char)) + { + if (i < (LOCAL_BUFFER_SIZE - 1) && XPosition < (coordScreenBufferSize.X - 1)) + { + *LocalBufPtr++ = Char; + + // cursor adjusted by 2 because the char is double width + XPosition += 2; + i += 1; + pwchBuffer++; + } + else + { + goto EndWhile; + } + } + else + { + *LocalBufPtr = Char; + LocalBufPtr++; + XPosition++; + i++; + pwchBuffer++; + } + } + else + { + FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))); + switch (RealUnicodeChar) + { + case UNICODE_BELL: + if (dwFlags & WC_ECHO) + { + 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. + + goto EndWhile; + break; + case UNICODE_TAB: + if (screenInfo.InVTMode()) + { + goto EndWhile; + } + else + { + const ULONG TabSize = NUMBER_OF_SPACES_IN_TAB(XPosition); + XPosition = (SHORT)(XPosition + TabSize); + if (XPosition >= coordScreenBufferSize.X || WI_IsFlagSet(dwFlags, WC_NONDESTRUCTIVE_TAB)) + { + goto EndWhile; + } + + for (ULONG j = 0; j < TabSize && i < LOCAL_BUFFER_SIZE; j++, i++) + { + *LocalBufPtr = UNICODE_SPACE; + LocalBufPtr++; + } + } + + pwchBuffer++; + break; + case UNICODE_LINEFEED: + case UNICODE_CARRIAGERETURN: + goto EndWhile; + default: + + // if char is ctrl char, write ^char. + if ((dwFlags & WC_ECHO) && (IS_CONTROL_CHAR(RealUnicodeChar))) + { + + CtrlChar: + if (i < (LOCAL_BUFFER_SIZE - 1)) + { + *LocalBufPtr = (WCHAR)'^'; + LocalBufPtr++; + XPosition++; + i++; + + *LocalBufPtr = (WCHAR)(RealUnicodeChar + (WCHAR)'@'); + LocalBufPtr++; + XPosition++; + i++; + + pwchBuffer++; + } + else + { + goto EndWhile; + } + } + 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 (CharType == C1_CNTRL) + { + ConvertOutputToUnicode(gci.OutputCP, + (LPSTR)&RealUnicodeChar, + 1, + LocalBufPtr, + 1); + } + else if (Char == UNICODE_NULL) + { + *LocalBufPtr = UNICODE_SPACE; + } + else + { + *LocalBufPtr = Char; + } + + LocalBufPtr++; + XPosition++; + i++; + pwchBuffer++; + } + } + } + lpString++; + pwchRealUnicode++; + *pcb += sizeof(WCHAR); + } + EndWhile: + if (i != 0) + { + CursorPosition = cursor.GetPosition(); + + // Make sure we don't write past the end of the buffer. + if (i > (ULONG)coordScreenBufferSize.X - CursorPosition.X) + { + i = (ULONG)coordScreenBufferSize.X - CursorPosition.X; + } + + // 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 + screenInfo.NotifyAccessibilityEventing(CursorPosition.X, CursorPosition.Y, + CursorPosition.X + gsl::narrow(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); + CursorPosition.X = XPosition; + + // enforce a delayed newline if we're about to pass the end and the WC_DELAY_EOL_WRAP flag is set. + if (WI_IsFlagSet(dwFlags, WC_DELAY_EOL_WRAP) && CursorPosition.X >= coordScreenBufferSize.X) + { + // Our cursor position as of this time is going to remain on the last position in this column. + CursorPosition.X = coordScreenBufferSize.X - 1; + + // Update in the structures that we're still pointing to the last character in the row + cursor.SetPosition(CursorPosition); + + // Record for the delay comparison that we're delaying on the last character in the row + cursor.DelayEOLWrap(CursorPosition); + } + else + { + Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); + } + + if (*pcb == BufferSize) + { + if (nullptr != pcSpaces) + { + *pcSpaces = TempNumSpaces; + } + return STATUS_SUCCESS; + } + continue; + } + else if (*pcb >= BufferSize) + { + 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; + } + + FAIL_FAST_IF(!(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))); + 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; + + const size_t 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()); + } + + for (i = 0, Tmp2 = buffer.get(), Tmp = pwchBufferBackupLimit; + i < bufferSize; i++, Tmp++) + { + // see 18120085, these two need to be seperate 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()) + { + Tmp2--; + } + } + else + { + FAIL_FAST_IF(!(Tmp2 >= buffer.get())); + *Tmp2++ = *Tmp; + } + } + 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) + { + CursorPosition.X -= (SHORT)(RetrieveNumberOfSpaces(sOriginalXPosition, + pwchBufferBackupLimit, + (ULONG)(pwchBuffer - pwchBufferBackupLimit - 1))); + if (CursorPosition.X < 0) + { + CursorPosition.X = (coordScreenBufferSize.X - 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).GetCharRow().SetWrapForced(false); + } + } + else if (IS_CONTROL_CHAR(LastChar)) + { + CursorPosition.X -= 1; + TempNumSpaces -= 1; + + // overwrite second character of ^x sequence. + if (dwFlags & WC_DESTRUCTIVE_BACKSPACE) + { + try + { + screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition); + Status = STATUS_SUCCESS; + } + CATCH_LOG(); + } + + 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) + { + try + { + screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), CursorPosition); + Status = STATUS_SUCCESS; + } + CATCH_LOG(); + } + CursorPosition.X -= 1; + } + else + { + CursorPosition.X--; + } + } + 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 + { + screenInfo.Write(OutputCellIterator(UNICODE_SPACE, Attributes, 1), cursor.GetPosition()); + } + CATCH_LOG(); + } + if (cursor.GetPosition().X == 0 && (screenInfo.OutputMode & ENABLE_WRAP_AT_EOL_OUTPUT) && pwchBuffer > pwchBufferBackupLimit) + { + if (CheckBisectProcessW(screenInfo, + pwchBufferBackupLimit, + pwchBuffer + 1 - pwchBufferBackupLimit, + coordScreenBufferSize.X - sOriginalXPosition, + sOriginalXPosition, + dwFlags & WC_ECHO)) + { + CursorPosition.X = coordScreenBufferSize.X - 1; + CursorPosition.Y = (SHORT)(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).GetCharRow().SetWrapForced(false); + + Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY); + } + } + break; + } + case UNICODE_TAB: + { + // if VT-style tabs are set, then handle them the VT way, including not inserting spaces. + // just move the cursor to the next tab stop. + if (screenInfo.InVTMode()) + { + const COORD cCursorOld = cursor.GetPosition(); + // Get Forward tab handles tabbing past the end of the buffer + CursorPosition = screenInfo.GetForwardTab(cCursorOld); + } + else + { + const size_t TabSize = NUMBER_OF_SPACES_IN_TAB(cursor.GetPosition().X); + CursorPosition.X = (SHORT)(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.X) + { + NumChars = gsl::narrow(coordScreenBufferSize.X - 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).GetCharRow().SetWrapForced(true); + } + else + { + NumChars = gsl::narrow(CursorPosition.X - cursor.GetPosition().X); + CursorPosition.Y = cursor.GetPosition().Y; + } + + if (!WI_IsFlagSet(dwFlags, WC_NONDESTRUCTIVE_TAB)) + { + try + { + const OutputCellIterator it(UNICODE_SPACE, Attributes, NumChars); + const auto done = screenInfo.Write(it, cursor.GetPosition()); + NumChars = done.GetCellDistance(it); + } + 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 (gci.IsReturnOnNewlineAutomatic()) + { + // 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; + } + + CursorPosition.Y = (SHORT)(cursor.GetPosition().Y + 1); + + { + // since we explicitly just moved down a row, clear the wrap status on the row we just came from + textBuffer.GetRowByOffset(cursor.GetPosition().Y).GetCharRow().SetWrapForced(false); + } + + Status = AdjustCursorPosition(screenInfo, CursorPosition, (dwFlags & WC_KEEP_CURSOR_VISIBLE) != 0, psScrollY); + break; + } + default: + { + const wchar_t Char = *lpString; + if (Char >= UNICODE_SPACE && + IsGlyphFullWidth(Char) && + XPosition >= (coordScreenBufferSize.X - 1) && + (screenInfo.OutputMode & ENABLE_WRAP_AT_EOL_OUTPUT)) + { + const COORD TargetPoint = cursor.GetPosition(); + ROW& Row = textBuffer.GetRowByOffset(TargetPoint.Y); + CharRow& charRow = Row.GetCharRow(); + + try + { + // If we're on top of a trailing cell, clear it and the previous cell. + if (charRow.DbcsAttrAt(TargetPoint.X).IsTrailing()) + { + // 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 (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + CursorPosition.X = 0; + CursorPosition.Y = (SHORT)(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 + charRow.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). + charRow.SetDoubleBytePadded(true); + + Status = AdjustCursorPosition(screenInfo, CursorPosition, dwFlags & WC_KEEP_CURSOR_VISIBLE, psScrollY); + continue; + } + break; + } + } + if (!NT_SUCCESS(Status)) + { + return Status; + } + + *pcb += sizeof(WCHAR); + lpString++; + pwchRealUnicode++; + } + + if (nullptr != pcSpaces) + { + *pcSpaces = TempNumSpaces; + } + + return STATUS_SUCCESS; +} + +// Routine Description: +// - This routine writes a string to the screen, processing any embedded +// unicode characters. The string is also copied to the input buffer, if +// the output mode is line mode. +// Arguments: +// - screenInfo - reference to screen buffer information structure. +// - pwchBufferBackupLimit - Pointer to beginning of buffer. +// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. +// This pointer is updated to point to the next position in the buffer. +// - pwchRealUnicode - Pointer to string to write. +// - 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_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge +// WC_ECHO if called by Read (echoing characters) +// Return Value: +// Note: +// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. +[[nodiscard]] +NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, + _In_range_(<= , pwchBuffer) const wchar_t* const pwchBufferBackupLimit, + _In_ const wchar_t* pwchBuffer, + _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, + _Inout_ size_t* const pcb, + _Out_opt_ size_t* const pcSpaces, + const SHORT sOriginalXPosition, + const DWORD dwFlags, + _Inout_opt_ PSHORT const psScrollY) +{ + if (!WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) || + !WI_IsFlagSet(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) + { + return WriteCharsLegacy(screenInfo, + pwchBufferBackupLimit, + pwchBuffer, + pwchRealUnicode, + pcb, + pcSpaces, + sOriginalXPosition, + dwFlags, + psScrollY); + } + + NTSTATUS Status = STATUS_SUCCESS; + + size_t const BufferSize = *pcb; + *pcb = 0; + + { + size_t TempNumSpaces = 0; + + { + if (NT_SUCCESS(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))); + + StateMachine& machine = screenInfo.GetStateMachine(); + size_t const cch = BufferSize / sizeof(WCHAR); + + machine.ProcessString(pwchRealUnicode, cch); + *pcb += BufferSize; + } + } + + if (nullptr != pcSpaces) + { + *pcSpaces = TempNumSpaces; + } + } + + return Status; +} + +// Routine Description: +// - Takes the given text and inserts it into the given screen buffer. +// Note: +// - Console lock must be held when calling this routine +// - String has been translated to unicode at this point. +// Arguments: +// - pwchBuffer - wide character text to be inserted into buffer +// - pcbBuffer - byte count of pwchBuffer on the way in, number of bytes consumed on the way out. +// - screenInfo - Screen Information class to write the text into at the current cursor position +// - ppWaiter - If writing to the console is blocked for whatever reason, this will be filled with a pointer to context +// that can be used by the server to resume the call at a later time. +// Return Value: +// - STATUS_SUCCESS if OK. +// - CONSOLE_STATUS_WAIT if we couldn't finish now and need to be called back later (see ppWaiter). +// - Or a suitable NTSTATUS format error code for memory/string/math failures. +[[nodiscard]] +NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer, + _Inout_ size_t* const pcbBuffer, + SCREEN_INFORMATION& screenInfo, + std::unique_ptr& waiter) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (WI_IsAnyFlagSet(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING))) + { + try + { + waiter = std::make_unique(screenInfo, + pwchBuffer, + *pcbBuffer, + gci.OutputCP); + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + return CONSOLE_STATUS_WAIT; + } + + const auto& textBuffer = screenInfo.GetTextBuffer(); + return WriteChars(screenInfo, + pwchBuffer, + pwchBuffer, + pwchBuffer, + pcbBuffer, + nullptr, + textBuffer.GetCursor().GetPosition().X, + WC_LIMIT_BACKSPACE, + nullptr); +} + +// Routine Description: +// - This method performs the actual work of attempting to write to the console, converting data types as necessary +// to adapt from the server types to the legacy internal host types. +// - It operates on Unicode data only. It's assumed the text is translated by this point. +// Arguments: +// - OutContext - the console output object to write the new text into +// - pwsTextBuffer - wide character text buffer provided by client application to insert +// - cchTextBufferLength - text buffer counted in characters +// - pcchTextBufferRead - character count of the number of characters we were able to insert before returning +// - ppWaiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later +// Return Value: +// - S_OK if successful. +// - S_OK if we need to wait (check if ppWaiter is not nullptr). +// - Or a suitable HRESULT code for math/string/memory failures. +[[nodiscard]] +HRESULT WriteConsoleWImplHelper(IConsoleOutputObject& context, + const std::wstring_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept +{ + try + { + // Set out variables in case we exit early. + read = 0; + waiter.reset(); + + // Convert characters to bytes to give to DoWriteConsole. + size_t cbTextBufferLength; + RETURN_IF_FAILED(SizeTMult(buffer.size(), sizeof(wchar_t), &cbTextBufferLength)); + + NTSTATUS Status = DoWriteConsole(const_cast(buffer.data()), &cbTextBufferLength, context, waiter); + + // Convert back from bytes to characters for the resulting string length written. + read = cbTextBufferLength / sizeof(wchar_t); + + if (Status == CONSOLE_STATUS_WAIT) + { + FAIL_FAST_IF_NULL(waiter.get()); + Status = STATUS_SUCCESS; + } + + RETURN_NTSTATUS(Status); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Writes non-Unicode formatted data into the given console output object. +// - This method will convert from the given input into wide characters before chain calling the wide character version of the function. +// It uses the current Output Codepage for conversions (set via SetConsoleOutputCP). +// - NOTE: This may be blocked for various console states and will return a wait context pointer if necessary. +// Arguments: +// - context - the console output object to write the new text into +// - buffer - char/byte text buffer provided by client application to insert +// - read - character count of the number of characters (also bytes because A version) we were able to insert before returning +// - waiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later +// Return Value: +// - S_OK if successful. +// - S_OK if we need to wait (check if ppWaiter is not nullptr). +// - Or a suitable HRESULT code for math/string/memory failures. +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleAImpl(IConsoleOutputObject& context, + const std::string_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Ensure output variables are initialized. + read = 0; + waiter.reset(); + + bool fLeadByteCaptured = false; + bool fLeadByteConsumed = false; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + if (buffer.size() == 0) + { + return S_OK; + } + + const auto codepage = gci.OutputCP; + + // Convert our input parameters to Unicode + std::unique_ptr wideCharBuffer{ nullptr }; + static Utf8ToWideCharParser parser{ gci.OutputCP }; + + // update current codepage in case it was changed from last time + // this was called. We do this outside the UTF-8 check because the parser drops its state + // when the codepage changes. + parser.SetCodePage(gci.OutputCP); + + SCREEN_INFORMATION& ScreenInfo = context.GetActiveBuffer(); + wchar_t* pwchBuffer; + size_t cchBuffer; + if (codepage == CP_UTF8) + { + wideCharBuffer.release(); + unsigned int charCount; + unsigned int charsConsumed; + unsigned int charsGenerated; + RETURN_IF_FAILED(SizeTToUInt(buffer.size(), &charCount)); + RETURN_IF_FAILED(parser.Parse(reinterpret_cast(buffer.data()), + charCount, + charsConsumed, + wideCharBuffer, + charsGenerated)); + + pwchBuffer = reinterpret_cast(wideCharBuffer.get()); + cchBuffer = charsGenerated; + read = charsConsumed; + } + else + { + NTSTATUS Status = STATUS_SUCCESS; + PWCHAR TransBuffer; + PWCHAR TransBufferOriginalLocation; + DWORD Length; + ULONG dbcsNumBytes = 0; + ULONG BufPtrNumBytes = 0; + const char* BufPtr = buffer.data(); + + // (cchTextBufferLength + 2) I think because we might be shoving another unicode char + // from ScreenInfo->WriteConsoleDbcsLeadByte in front + TransBuffer = new WCHAR[buffer.size() + 2]; + RETURN_IF_NULL_ALLOC(TransBuffer); + ZeroMemory(TransBuffer, sizeof(WCHAR) * (buffer.size() + 2)); + + TransBufferOriginalLocation = TransBuffer; + + unsigned int uiTextBufferLength; + RETURN_IF_FAILED(SizeTToUInt(buffer.size(), &uiTextBufferLength)); + + if (!ScreenInfo.WriteConsoleDbcsLeadByte[0] || *(PUCHAR)BufPtr < (UCHAR) ' ') + { + dbcsNumBytes = 0; + BufPtrNumBytes = uiTextBufferLength; + } + else if (buffer.size()) + { + // there was a portion of a dbcs character stored from a previous + // call so we take the 2nd half from BufPtr[0], put them together + // and write the wide char to TransBuffer[0] + ScreenInfo.WriteConsoleDbcsLeadByte[1] = *(PCHAR)BufPtr; + + try + { + const std::string_view leadByte(reinterpret_cast(ScreenInfo.WriteConsoleDbcsLeadByte), + ARRAYSIZE(ScreenInfo.WriteConsoleDbcsLeadByte)); + + const std::wstring converted = ConvertToW(gci.OutputCP, leadByte); + + FAIL_FAST_IF(converted.size() != 1); + dbcsNumBytes = sizeof(wchar_t); + TransBuffer[0] = converted.at(0); + BufPtr++; + } + catch (...) + { + Status = STATUS_UNSUCCESSFUL; + dbcsNumBytes = 0; + } + + // this looks weird to be always incrementing even if the conversion failed, but this is the + // original behavior so it's left unchanged. + TransBuffer++; + BufPtrNumBytes = uiTextBufferLength - 1; + + // Note that we used a stored lead byte from a previous call in order to complete this write + // Use this to offset the "number of bytes consumed" calculation at the end by -1 to account + // for using a byte we had internally, not off the stream. + fLeadByteConsumed = true; + } + else + { + // nothing in ScreenInfo->WriteConsoleDbcsLeadByte and nothing in BufPtr + BufPtrNumBytes = 0; + } + + ScreenInfo.WriteConsoleDbcsLeadByte[0] = 0; + + // if the last byte in BufPtr is a lead byte for the current code page, + // save it for the next time this function is called and we can piece it + // back together then + __analysis_assume(BufPtrNumBytes <= uiTextBufferLength); + if (BufPtrNumBytes && CheckBisectStringA((PCHAR)BufPtr, BufPtrNumBytes, &gci.OutputCPInfo)) + { + ScreenInfo.WriteConsoleDbcsLeadByte[0] = *((PCHAR)BufPtr + BufPtrNumBytes - 1); + BufPtrNumBytes--; + + // Note that we captured a lead byte during this call, but won't actually draw it until later. + // Use this to offset the "number of bytes consumed" calculation at the end by +1 to account + // for taking a byte off the stream. + fLeadByteCaptured = true; + } + + if (BufPtrNumBytes != 0) + { + // convert the remaining bytes in BufPtr to wide chars + Length = sizeof(WCHAR) * MultiByteToWideChar(gci.OutputCP, + 0, + (LPCCH)BufPtr, + BufPtrNumBytes, + TransBuffer, + BufPtrNumBytes); + + if (Length == 0) + { + Status = STATUS_UNSUCCESSFUL; + } + BufPtrNumBytes = Length; + } + + pwchBuffer = TransBufferOriginalLocation; + cchBuffer = (dbcsNumBytes + BufPtrNumBytes) / sizeof(wchar_t); + } + + // Make the W version of the call + size_t cchBufferRead; + + // Hold the specific version of the waiter locally so we can tinker with it if we must to store additional context. + std::unique_ptr writeDataWaiter; + + HRESULT const hr = WriteConsoleWImplHelper(ScreenInfo, { pwchBuffer, cchBuffer }, cchBufferRead, writeDataWaiter); + + // If there is no waiter, process the byte count now. + if (nullptr == writeDataWaiter.get()) + { + // Calculate how many bytes of the original A buffer were consumed in the W version of the call to satisfy pcchTextBufferRead. + // For UTF-8 conversions, we've already returned this information above. + if (CP_UTF8 != codepage) + { + size_t cchTextBufferRead = 0; + + // Start by counting the number of A bytes we used in printing our W string to the screen. + try + { + cchTextBufferRead = GetALengthFromW(codepage, { pwchBuffer, cchBufferRead }); + } + CATCH_LOG(); + + // If we captured a byte off the string this time around up above, it means we didn't feed + // it into the WriteConsoleW above, and therefore its consumption isn't accounted for + // in the count we just made. Add +1 to compensate. + if (fLeadByteCaptured) + { + cchTextBufferRead++; + } + + // If we consumed an internally-stored lead byte this time around up above, it means that we + // fed a byte into WriteConsoleW that wasn't a part of this particular call's request. + // We need to -1 to compensate and tell the caller the right number of bytes consumed this request. + if (fLeadByteConsumed) + { + cchTextBufferRead--; + } + + read = cchTextBufferRead; + } + } + else + { + // If there is a waiter, then we need to stow some additional information in the wait structure so + // we can synthesize the correct byte count later when the wait routine is triggered. + if (CP_UTF8 != codepage) + { + // For non-UTF8 codepages, save the lead byte captured/consumed data so we can +1 or -1 the final decoded count + // in the WaitData::Notify method later. + writeDataWaiter->SetLeadByteAdjustmentStatus(fLeadByteCaptured, fLeadByteConsumed); + } + else + { + // For UTF8 codepages, just remember the consumption count from the UTF-8 parser. + writeDataWaiter->SetUtf8ConsumedCharacters(read); + } + } + + // Free remaining data + if (codepage != CP_UTF8) + { + delete[] pwchBuffer; + } + + // Give back the waiter now that we're done with tinkering with it. + waiter.reset(writeDataWaiter.release()); + + return hr; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Writes Unicode formatted data into the given console output object. +// - NOTE: This may be blocked for various console states and will return a wait context pointer if necessary. +// Arguments: +// - OutContext - the console output object to write the new text into +// - pwsTextBuffer - wide character text buffer provided by client application to insert +// - cchTextBufferLength - text buffer counted in characters +// - pcchTextBufferRead - character count of the number of characters we were able to insert before returning +// - ppWaiter - If we are blocked from writing now and need to wait, this is filled with contextual data for the server to restore the call later +// Return Value: +// - S_OK if successful. +// - S_OK if we need to wait (check if ppWaiter is not nullptr). +// - Or a suitable HRESULT code for math/string/memory failures. +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleWImpl(IConsoleOutputObject& context, + const std::wstring_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + std::unique_ptr writeDataWaiter; + RETURN_IF_FAILED(WriteConsoleWImplHelper(context.GetActiveBuffer(), buffer, read, writeDataWaiter)); + + // Transfer specific waiter pointer into the generic interface wrapper. + waiter.reset(writeDataWaiter.release()); + + return S_OK; + } + CATCH_RETURN(); +} diff --git a/src/host/_stream.h b/src/host/_stream.h new file mode 100644 index 000000000..8b60d1a54 --- /dev/null +++ b/src/host/_stream.h @@ -0,0 +1,103 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- _stream.h + +Abstract: +- Process stream written content into the text buffer + +Author: +- KazuM Jun.09.1997 + +Revision History: +- Remove FE/Non-FE separation in preparation for refactoring. (MiNiksa, 2014) +--*/ + +#pragma once + +#include "..\server\IWaitRoutine.h" +#include "writeData.hpp" + +/*++ +Routine Description: + This routine updates the cursor position. Its input is the non-special + cased new location of the cursor. For example, if the cursor were being + moved one space backwards from the left edge of the screen, the X + coordinate would be -1. This routine would set the X coordinate to + the right edge of the screen and decrement the Y coordinate by one. + +Arguments: + pScreenInfo - Pointer to screen buffer information structure. + 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_ COORD coordCursor, + const BOOL fKeepCursorVisible, + _Inout_opt_ PSHORT psScrollY); + +#define LOCAL_BUFFER_SIZE 100 + + +/*++ +Routine Description: + This routine writes a string to the screen, processing any embedded + unicode characters. The string is also copied to the input buffer, if + the output mode is line mode. + +Arguments: + ScreenInfo - Pointer to screen buffer information structure. + lpBufferBackupLimit - Pointer to beginning of buffer. + lpBuffer - Pointer to buffer to copy string to. assumed to be at least + as long as lpRealUnicodeString. This pointer is updated to point to the + next position in the buffer. + lpRealUnicodeString - Pointer to string to write. + NumBytes - On input, number of bytes to write. On output, number of + bytes written. + NumSpaces - On output, the number of spaces consumed by the written characters. + dwFlags - + WC_DESTRUCTIVE_BACKSPACE backspace overwrites characters. + WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge + WC_ECHO if called by Read (echoing characters) + +Return Value: + +Note: + This routine does not process tabs and backspace properly. That code + will be implemented as part of the line editing services. +--*/ +[[nodiscard]] +NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, + _In_range_(<= , pwchBuffer) const wchar_t* const pwchBufferBackupLimit, + _In_ const wchar_t* pwchBuffer, + _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, + _Inout_ size_t* const pcb, + _Out_opt_ size_t* const pcSpaces, + const SHORT sOriginalXPosition, + const DWORD dwFlags, + _Inout_opt_ PSHORT const psScrollY); + +// The new entry point for WriteChars to act as an intercept in case we place a Virtual Terminal processor in the way. +[[nodiscard]] +NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, + _In_range_(<= , pwchBuffer) const wchar_t* const pwchBufferBackupLimit, + _In_ const wchar_t* pwchBuffer, + _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, + _Inout_ size_t* const pcb, + _Out_opt_ size_t* const pcSpaces, + const SHORT sOriginalXPosition, + const DWORD dwFlags, + _Inout_opt_ PSHORT const psScrollY); + +// NOTE: console lock must be held when calling this routine +// String has been translated to unicode at this point. +[[nodiscard]] +NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PWCHAR pwchBuffer, + _In_ size_t* const pcbBuffer, + SCREEN_INFORMATION& screenInfo, + std::unique_ptr& waiter); diff --git a/src/host/alias.cpp b/src/host/alias.cpp new file mode 100644 index 000000000..36f3a092d --- /dev/null +++ b/src/host/alias.cpp @@ -0,0 +1,1288 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "alias.h" + +#include "_output.h" +#include "output.h" +#include "stream.h" +#include "_stream.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "../types/inc/convert.hpp" +#include "srvinit.h" +#include "resource.h" + +#include "ApiRoutines.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +struct case_insensitive_hash +{ + std::size_t operator()(const std::wstring& key) const + { + std::wstring lower(key); + std::transform(lower.begin(), lower.end(), lower.begin(), ::towlower); + std::hash hash; + return hash(lower); + } +}; + +struct case_insensitive_equality +{ + bool operator()(const std::wstring& lhs, const std::wstring& rhs) const + { + return 0 == _wcsicmp(lhs.data(), rhs.data()); + } +}; + +std::unordered_map, + case_insensitive_hash, + case_insensitive_equality> g_aliasData; + +// Routine Description: +// - Adds a command line alias to the global set. +// - Converts and calls the W version of this function. +// Arguments: +// - source - The shorthand/alias or source buffer to set +// - target- The destination/expansion or target buffer to set +// - exeName - The client EXE application attached to the host to whom this substitution will apply +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::AddConsoleAliasAImpl(const std::string_view source, + const std::string_view target, + const std::string_view exeName) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + try + { + const auto sourceW = ConvertToW(codepage, source); + const auto targetW = ConvertToW(codepage, target); + const auto exeNameW = ConvertToW(codepage, exeName); + + return AddConsoleAliasWImpl(sourceW, targetW, exeNameW); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Adds a command line alias to the global set. +// Arguments: +// - source - The shorthand/alias or source buffer to set +// - target - The destination/expansion or target buffer to set +// - exeName - The client EXE application attached to the host to whom this substitution will apply +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::AddConsoleAliasWImpl(const std::wstring_view source, + const std::wstring_view target, + const std::wstring_view exeName) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + RETURN_HR_IF(E_INVALIDARG, source.size() == 0); + + try + { + std::wstring exeNameString(exeName); + std::wstring sourceString(source); + std::wstring targetString(target); + + std::transform(exeNameString.begin(), exeNameString.end(), exeNameString.begin(), towlower); + std::transform(sourceString.begin(), sourceString.end(), sourceString.begin(), towlower); + + if (targetString.size() == 0) + { + // Only try to dig in and erase if the exeName exists. + auto exeData = g_aliasData.find(exeNameString); + if (exeData != g_aliasData.end()) + { + g_aliasData[exeNameString].erase(sourceString); + } + } + else + { + // Map will auto-create each level as necessary + g_aliasData[exeNameString][sourceString] = targetString; + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Retrieves a command line alias from the global set. +// - It is permitted to call this function without having a target buffer. Use the result to allocate +// the appropriate amount of space and call again. +// - This behavior exists to allow the A version of the function to help allocate the right temp buffer for conversion of +// the output/result data. +// Arguments: +// - source - The shorthand/alias or source buffer to use in lookup +// - target - The destination/expansion or target buffer we are attempting to retrieve. Optionally nullopt to retrieve needed space. +// - writtenOrNeeded - Will specify how many characters were written (if target has value) +// or how many characters would have been consumed (if target is nullopt). +// - exeName - The client EXE application attached to the host whose set we should check +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT GetConsoleAliasWImplHelper(const std::wstring_view source, + std::optional> target, + size_t& writtenOrNeeded, + const std::wstring_view exeName) +{ + // Ensure output variables are initialized + writtenOrNeeded = 0; + + if (target.has_value() && target.value().size() > 0) + { + target.value().at(0) = UNICODE_NULL; + } + + std::wstring exeNameString(exeName); + std::wstring sourceString(source); + + // For compatibility, return ERROR_GEN_FAILURE for any result where the alias can't be found. + // We use .find for the iterators then dereference to search without creating entries. + const auto exeIter = g_aliasData.find(exeNameString); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_GEN_FAILURE), exeIter == g_aliasData.end()); + const auto exeData = exeIter->second; + const auto sourceIter = exeData.find(sourceString); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_GEN_FAILURE), sourceIter == exeData.end()); + const auto targetString = sourceIter->second; + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_GEN_FAILURE), targetString.size() == 0); + + // TargetLength is a byte count, convert to characters. + size_t targetSize = targetString.size(); + size_t const cchNull = 1; + + // The total space we need is the length of the string + the null terminator. + size_t neededSize; + RETURN_IF_FAILED(SizeTAdd(targetSize, cchNull, &neededSize)); + + writtenOrNeeded = neededSize; + + if (target.has_value()) + { + // if the user didn't give us enough space, return with insufficient buffer code early. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER), gsl::narrow(target.value().size()) < neededSize); + + RETURN_IF_FAILED(StringCchCopyNW(target.value().data(), target.value().size(), targetString.data(), targetSize)); + } + + return S_OK; +} + +// Routine Description: +// - Retrieves a command line alias from the global set. +// - This function will convert input parameters from A to W, call the W version of the routine, +// and attempt to convert the resulting data back to A for return. +// Arguments: +// - source - The shorthand/alias or source buffer to use in lookup +// - target - The destination/expansion or target buffer we are attempting to retrieve. +// - written - Will specify how many characters were written +// - exeName - The client EXE application attached to the host whose set we should check +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasAImpl(const std::string_view source, + gsl::span target, + size_t& written, + const std::string_view exeName) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + written = 0; + try + { + if (target.size() > 0) + { + target.at(0) = ANSI_NULL; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Convert our input parameters to Unicode. + const auto sourceW = ConvertToW(codepage, source); + const auto exeNameW = ConvertToW(codepage, exeName); + + // Figure out how big our temporary Unicode buffer must be to retrieve output + size_t targetNeeded; + RETURN_IF_FAILED(GetConsoleAliasWImplHelper(sourceW, std::nullopt, targetNeeded, exeNameW)); + + // If there's nothing to get, then simply return. + RETURN_HR_IF(S_OK, 0 == targetNeeded); + + // If the user hasn't given us a buffer at all and we need one, return an error. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER), 0 == target.size()); + + // Allocate a unicode buffer of the right size. + std::unique_ptr targetBuffer = std::make_unique(targetNeeded); + RETURN_IF_NULL_ALLOC(targetBuffer); + + // Call the Unicode version of this method + size_t targetWritten; + RETURN_IF_FAILED(GetConsoleAliasWImplHelper(sourceW, + gsl::span(targetBuffer.get(), targetNeeded), + targetWritten, + exeNameW)); + + // Set the return size copied to the size given before we attempt to copy. + // Then multiply by sizeof(wchar_t) due to a long standing bug that we must preserve for compatibility. + // On failure, the API has historically given back this value. + written = target.size() * sizeof(wchar_t); + + // Convert result to A + const auto converted = ConvertToA(codepage, { targetBuffer.get(), targetWritten }); + + // Copy safely to output buffer + RETURN_IF_FAILED(StringCchCopyNA(target.data(), target.size(), converted.data(), converted.size())); + + // And return the size copied. + written = converted.size(); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves a command line alias from the global set. +// Arguments: +// - source - The shorthand/alias or source buffer to use in lookup +// - target - The destination/expansion or target buffer we are attempting to retrieve. +// - written - Will specify how many characters were written +// - exeName - The client EXE application attached to the host whose set we should check +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasWImpl(const std::wstring_view source, + gsl::span target, + size_t& written, + const std::wstring_view exeName) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + HRESULT hr = GetConsoleAliasWImplHelper(source, target, written, exeName); + + if (FAILED(hr)) + { + written = target.size(); + } + + return hr; + } + CATCH_RETURN(); +} + +// These variables define the seperator character and the length of the string. +// They will be used to as the joiner between source and target strings when returning alias data in list form. +static std::wstring aliasesSeparator(L"="); + +// Routine Description: +// - Retrieves the amount of space needed to hold all aliases (source=target pairs) for the given EXE name +// - Works for both Unicode and Multibyte text. +// - This method configuration is called for both A/W routines to allow us an efficient way of asking the system +// the lengths of how long each conversion would be without actually performing the full allocations/conversions. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - countInUnicode - True for W version (UTF-16 Unicode) calls. False for A version calls (all multibyte formats.) +// - codepage - Set to valid Windows Codepage for A version calls. Ignored for W (but typically just set to 0.) +// - bufferRequired - Receives the length of buffer that would be required to retrieve all aliases for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT GetConsoleAliasesLengthWImplHelper(const std::wstring_view exeName, + const bool countInUnicode, + const UINT codepage, + size_t& bufferRequired) +{ + // Ensure output variables are initialized + bufferRequired = 0; + + try + { + const std::wstring exeNameString(exeName); + + size_t cchNeeded = 0; + + // Each of the aliases will be made up of the source, a seperator, the target, then a null character. + // They are of the form "Source=Target" when returned. + size_t const cchNull = 1; + size_t cchSeperator = aliasesSeparator.size(); + // If we're counting how much multibyte space will be needed, trial convert the seperator before we add. + if (!countInUnicode) + { + cchSeperator = GetALengthFromW(codepage, aliasesSeparator); + } + + // Find without creating. + auto exeIter = g_aliasData.find(exeNameString); + if (exeIter != g_aliasData.end()) + { + auto list = exeIter->second; + for (auto& pair : list) + { + // Alias stores lengths in bytes. + size_t cchSource = pair.first.size(); + size_t cchTarget = pair.second.size(); + + // If we're counting how much multibyte space will be needed, trial convert the source and target strings before we add. + if (!countInUnicode) + { + cchSource = GetALengthFromW(codepage, pair.first); + cchTarget = GetALengthFromW(codepage, pair.second); + } + + // Accumulate all sizes to the final string count. + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchSource, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchSeperator, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchTarget, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchNull, &cchNeeded)); + } + } + + bufferRequired = cchNeeded; + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Retrieves the amount of space needed to hold all aliases (source=target pairs) for the given EXE name +// - Converts input text from A to W then makes the call to the W implementation. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - bufferRequired - Receives the length of buffer that would be required to retrieve all aliases for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasesLengthAImpl(const std::string_view exeName, + size_t& bufferRequired) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + bufferRequired = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Convert our input parameters to Unicode + try + { + const auto exeNameW = ConvertToW(codepage, exeName); + + return GetConsoleAliasesLengthWImplHelper(exeNameW, false, codepage, bufferRequired); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves the amount of space needed to hold all aliases (source=target pairs) for the given EXE name +// - Converts input text from A to W then makes the call to the W implementation. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - bufferRequired - Receives the length of buffer that would be required to retrieve all aliases for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasesLengthWImpl(const std::wstring_view exeName, + size_t& bufferRequired) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + return GetConsoleAliasesLengthWImplHelper(exeName, true, 0, bufferRequired); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Clears all aliases on CMD.exe. +void Alias::s_ClearCmdExeAliases() +{ + // find without creating. + auto exeIter = g_aliasData.find(L"cmd.exe"); + if (exeIter != g_aliasData.end()) + { + exeIter->second.clear(); + } +} + +// Routine Description: +// - Retrieves all source=target pairs representing alias definitions for a given EXE name +// - It is permitted to call this function without having a target buffer. Use the result to allocate +// the appropriate amount of space and call again. +// - This behavior exists to allow the A version of the function to help allocate the right temp buffer for conversion of +// the output/result data. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - aliasBuffer - The target buffer to hold all alias pairs we are trying to retrieve. +// Optionally nullopt to retrieve needed space. +// - writtenOrNeeded - Pointer to space that will specify how many characters were written (if buffer is valid) +// or how many characters would have been needed (if buffer is nullopt). +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT GetConsoleAliasesWImplHelper(const std::wstring_view exeName, + std::optional> aliasBuffer, + size_t& writtenOrNeeded) +{ + // Ensure output variables are initialized. + writtenOrNeeded = 0; + + if (aliasBuffer.has_value() && aliasBuffer.value().size() > 0) + { + aliasBuffer.value().at(0) = UNICODE_NULL; + } + + std::wstring exeNameString(exeName); + + LPWSTR AliasesBufferPtrW = aliasBuffer.has_value() ? aliasBuffer.value().data() : nullptr; + size_t cchTotalLength = 0; // accumulate the characters we need/have copied as we walk the list + + // Each of the alises will be made up of the source, a seperator, the target, then a null character. + // They are of the form "Source=Target" when returned. + size_t const cchNull = 1; + + // Find without creating. + auto exeIter = g_aliasData.find(exeNameString); + if (exeIter != g_aliasData.end()) + { + auto list = exeIter->second; + for (auto& pair : list) + { + // Alias stores lengths in bytes. + size_t const cchSource = pair.first.size(); + size_t const cchTarget = pair.second.size(); + + // Add up how many characters we will need for the full alias data. + size_t cchNeeded = 0; + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchSource, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, aliasesSeparator.size(), &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchTarget, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchNull, &cchNeeded)); + + // If we can return the data, attempt to do so until we're done or it overflows. + // If we cannot return data, we're just going to loop anyway and count how much space we'd need. + if (aliasBuffer.has_value()) + { + // Calculate the new final total after we add what we need to see if it will exceed the limit + size_t cchNewTotal; + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchNewTotal)); + + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), cchNewTotal > gsl::narrow(aliasBuffer.value().size())); + + size_t cchAliasBufferRemaining; + RETURN_IF_FAILED(SizeTSub(aliasBuffer.value().size(), cchTotalLength, &cchAliasBufferRemaining)); + + RETURN_IF_FAILED(StringCchCopyNW(AliasesBufferPtrW, cchAliasBufferRemaining, pair.first.data(), cchSource)); + RETURN_IF_FAILED(SizeTSub(cchAliasBufferRemaining, cchSource, &cchAliasBufferRemaining)); + AliasesBufferPtrW += cchSource; + + RETURN_IF_FAILED(StringCchCopyNW(AliasesBufferPtrW, cchAliasBufferRemaining, aliasesSeparator.data(), aliasesSeparator.size())); + RETURN_IF_FAILED(SizeTSub(cchAliasBufferRemaining, aliasesSeparator.size(), &cchAliasBufferRemaining)); + AliasesBufferPtrW += aliasesSeparator.size(); + + RETURN_IF_FAILED(StringCchCopyNW(AliasesBufferPtrW, cchAliasBufferRemaining, pair.second.data(), cchTarget)); + RETURN_IF_FAILED(SizeTSub(cchAliasBufferRemaining, cchTarget, &cchAliasBufferRemaining)); + AliasesBufferPtrW += cchTarget; + + // StringCchCopyNW ensures that the destination string is null terminated, so simply advance the pointer. + RETURN_IF_FAILED(SizeTSub(cchAliasBufferRemaining, 1, &cchAliasBufferRemaining)); + AliasesBufferPtrW += cchNull; + } + + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchTotalLength)); + } + } + + writtenOrNeeded = cchTotalLength; + + return S_OK; +} + +// Routine Description: +// - Retrieves all source=target pairs representing alias definitions for a given EXE name +// - Will convert all input from A to W, call the W version of the function, then convert resulting W to A text and return. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - alias - The target buffer to hold all alias pairs we are trying to retrieve. +// - written - Will specify how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasesAImpl(const std::string_view exeName, + gsl::span alias, + size_t& written) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + written = 0; + + try + { + if (alias.size() > 0) + { + alias.at(0) = '\0'; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Convert our input parameters to Unicode. + const auto exeNameW = ConvertToW(codepage, exeName); + wistd::unique_ptr pwsExeName; + + // Figure out how big our temporary Unicode buffer must be to retrieve output + size_t bufferNeeded; + RETURN_IF_FAILED(GetConsoleAliasesWImplHelper(exeNameW, std::nullopt, bufferNeeded)); + + // If there's nothing to get, then simply return. + RETURN_HR_IF(S_OK, 0 == bufferNeeded); + + // Allocate a unicode buffer of the right size. + std::unique_ptr aliasBuffer = std::make_unique(bufferNeeded); + RETURN_IF_NULL_ALLOC(aliasBuffer); + + // Call the Unicode version of this method + size_t bufferWritten; + RETURN_IF_FAILED(GetConsoleAliasesWImplHelper(exeNameW, gsl::span(aliasBuffer.get(), bufferNeeded), bufferWritten)); + + // Convert result to A + const auto converted = ConvertToA(codepage, { aliasBuffer.get(), bufferWritten }); + + // Copy safely to the output buffer + // - Aliases are a series of null terminated strings. We cannot use a SafeString function to copy. + // So instead, validate and use raw memory copy. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), converted.size() > gsl::narrow(alias.size())); + memcpy_s(alias.data(), alias.size(), converted.data(), converted.size()); + + // And return the size copied. + written = converted.size(); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves all source=target pairs representing alias definitions for a given EXE name +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - alias - The target buffer to hold all alias pairs we are trying to retrieve. +// - written - Will specify how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasesWImpl(const std::wstring_view exeName, + gsl::span alias, + size_t& written) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + return GetConsoleAliasesWImplHelper(exeName, alias, written); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves the amount of space needed to hold all EXE names with aliases defined that are known to the console +// - Works for both Unicode and Multibyte text. +// - This method configuration is called for both A/W routines to allow us an efficient way of asking the system +// the lengths of how long each conversion would be without actually performing the full allocations/conversions. +// Arguments: +// - countInUnicode - True for W version (UCS-2 Unicode) calls. False for A version calls (all multibyte formats.) +// - codepage - Set to valid Windows Codepage for A version calls. Ignored for W (but typically just set to 0.) +// - bufferRequired - Receives the length of buffer that would be required to retrieve all relevant EXE names. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT GetConsoleAliasExesLengthImplHelper(const bool countInUnicode, const UINT codepage, size_t& bufferRequired) +{ + // Ensure output variables are initialized + bufferRequired = 0; + + size_t cchNeeded = 0; + + // Each alias exe will be made up of the string payload and a null terminator. + size_t const cchNull = 1; + + for (auto& pair : g_aliasData) + { + size_t cchExe = pair.first.size(); + + // If we're counting how much multibyte space will be needed, trial convert the exe string before we add. + if (!countInUnicode) + { + cchExe = GetALengthFromW(codepage, pair.first); + } + + // Accumulate to total + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchExe, &cchNeeded)); + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchNull, &cchNeeded)); + } + + bufferRequired = cchNeeded; + + return S_OK; +} + +// Routine Description: +// - Retrieves the amount of space needed to hold all EXE names with aliases defined that are known to the console +// Arguments: +// - bufferRequired - Receives the length of buffer that would be required to retrieve all relevant EXE names. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasExesLengthAImpl(size_t& bufferRequired) noexcept +{ + LockConsole(); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleAliasExesLengthImplHelper(false, gci.CP, bufferRequired); +} + +// Routine Description: +// - Retrieves the amount of space needed to hold all EXE names with aliases defined that are known to the console +// Arguments: +// - bufferRequired - Pointer to receive the length of buffer that would be required to retrieve all relevant EXE names. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleAliasExesLengthImplHelper(true, 0, bufferRequired); +} + +// Routine Description: +// - Retrieves all EXE names with aliases defined that are known to the console. +// - It is permitted to call this function without having a target buffer. Use the result to allocate +// the appropriate amount of space and call again. +// - This behavior exists to allow the A version of the function to help allocate the right temp buffer for conversion of +// the output/result data. +// Arguments: +// - pwsAliasExesBuffer - The target buffer to hold all known EXE names we are trying to retrieve. +// Optionally nullopt to retrieve needed space. +// - cchAliasExesBufferLength - Length in characters of target buffer. Set to 0 when buffer is nullptr. +// - pcchAliasExesBufferWrittenOrNeeded - Pointer to space that will specify how many characters were written (if buffer is valid) +// or how many characters would have been needed (if buffer is nullopt). +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT GetConsoleAliasExesWImplHelper(std::optional> aliasExesBuffer, + size_t& writtenOrNeeded) +{ + // Ensure output variables are initialized. + writtenOrNeeded = 0; + if (aliasExesBuffer.has_value() && aliasExesBuffer.value().size() > 0) + { + aliasExesBuffer.value().at(0) = UNICODE_NULL; + } + + LPWSTR AliasExesBufferPtrW = aliasExesBuffer.has_value() ? aliasExesBuffer.value().data() : nullptr; + size_t cchTotalLength = 0; // accumulate the characters we need/have copied as we walk the list + + size_t const cchNull = 1; + + for (auto& pair : g_aliasData) + { + // AliasList stores length in bytes. Add 1 for null terminator. + size_t const cchExe = pair.first.size(); + + size_t cchNeeded; + RETURN_IF_FAILED(SizeTAdd(cchExe, cchNull, &cchNeeded)); + + // If we can return the data, attempt to do so until we're done or it overflows. + // If we cannot return data, we're just going to loop anyway and count how much space we'd need. + if (aliasExesBuffer.has_value()) + { + // Calculate the new total length after we add to the buffer + // Error out early if there is a problem. + size_t cchNewTotal; + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchNewTotal)); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), cchNewTotal > gsl::narrow(aliasExesBuffer.value().size())); + + size_t cchRemaining; + RETURN_IF_FAILED(SizeTSub(aliasExesBuffer.value().size(), cchTotalLength, &cchRemaining)); + + RETURN_IF_FAILED(StringCchCopyNW(AliasExesBufferPtrW, cchRemaining, pair.first.data(), cchExe)); + AliasExesBufferPtrW += cchNeeded; + } + + // Accumulate the total written amount. + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchTotalLength)); + + } + + writtenOrNeeded = cchTotalLength; + + + return S_OK; +} + +// Routine Description: +// - Retrieves all EXE names with aliases defined that are known to the console. +// - Will call the W version of the function and convert all text back to A on returning. +// Arguments: +// - aliasExes - The target buffer to hold all known EXE names we are trying to retrieve. +// - written - Specifies how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasExesAImpl(gsl::span aliasExes, + size_t& written) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + written = 0; + + try + { + if (aliasExes.size() > 0) + { + aliasExes.at(0) = '\0'; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Figure our how big our temporary Unicode buffer must be to retrieve output + size_t bufferNeeded; + RETURN_IF_FAILED(GetConsoleAliasExesWImplHelper(std::nullopt, bufferNeeded)); + + // If there's nothing to get, then simply return. + RETURN_HR_IF(S_OK, 0 == bufferNeeded); + + // Allocate a unicode buffer of the right size. + std::unique_ptr targetBuffer = std::make_unique(bufferNeeded); + RETURN_IF_NULL_ALLOC(targetBuffer); + + // Call the Unicode version of this method + size_t bufferWritten; + RETURN_IF_FAILED(GetConsoleAliasExesWImplHelper(gsl::span(targetBuffer.get(), bufferNeeded), bufferWritten)); + + // Convert result to A + const auto converted = ConvertToA(codepage, { targetBuffer.get(), bufferWritten }); + + // Copy safely to the output buffer + // - AliasExes are a series of null terminated strings. We cannot use a SafeString function to copy. + // So instead, validate and use raw memory copy. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), converted.size() > gsl::narrow(aliasExes.size())); + memcpy_s(aliasExes.data(), aliasExes.size(), converted.data(), converted.size()); + + // And return the size copied. + written = converted.size(); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves all EXE names with aliases defined that are known to the console. +// Arguments: +// - pwsAliasExesBuffer - The target buffer to hold all known EXE names we are trying to retrieve. +// - cchAliasExesBufferLength - Length in characters of target buffer. Set to 0 when buffer is nullptr. +// - pcchAliasExesBufferWrittenOrNeeded - Pointer to space that will specify how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleAliasExesWImpl(gsl::span aliasExes, + size_t& written) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + return GetConsoleAliasExesWImplHelper(aliasExes, written); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Trims leading spaces off of a string +// Arguments: +// - str - String to trim +void Alias::s_TrimLeadingSpaces(std::wstring& str) +{ + // Erase from the beginning of the string up until the first + // character found that is not a space. + str.erase(str.begin(), + std::find_if(str.begin(), str.end(), [](wchar_t ch) { return !std::iswspace(ch); })); +} + +// Routine Description: +// - Trims trailing \r\n off of a string +// Arguments: +// - str - String to trim +void Alias::s_TrimTrailingCrLf(std::wstring& str) +{ + const auto trailingCrLfPos = str.find_last_of(UNICODE_CARRIAGERETURN); + if (std::wstring::npos != trailingCrLfPos) + { + str.erase(trailingCrLfPos); + } +} + +// Routine Description: +// - Tokenizes a string into a collection using space as a separator +// Arguments: +// - str - String to tokenize +// Return Value: +// - Collection of tokenized strings +std::deque Alias::s_Tokenize(const std::wstring& str) +{ + std::deque result; + + size_t prevIndex = 0; + auto spaceIndex = str.find(L' '); + while (std::wstring::npos != spaceIndex) + { + const auto length = spaceIndex - prevIndex; + + result.emplace_back(str.substr(prevIndex, length)); + + spaceIndex++; + prevIndex = spaceIndex; + + spaceIndex = str.find(L' ', spaceIndex); + } + + // Place the final one into the set. + result.emplace_back(str.substr(prevIndex)); + + return result; +} + +// Routine Description: +// - Gets just the arguments portion of the command string +// Specifically, all text after the first space character. +// Arguments: +// - str - String to split into just args +// Return Value: +// - Only the arguments part of the string or empty if there are no arguments. +std::wstring Alias::s_GetArgString(const std::wstring& str) +{ + std::wstring result; + auto firstSpace = str.find_first_of(L' '); + if (std::wstring::npos != firstSpace) + { + firstSpace++; + if (firstSpace < str.size()) + { + result = str.substr(firstSpace); + } + } + + return result; +} + +// Routine Description: +// - Checks the given character to see if it is a numbered arg replacement macro +// and replaces it with the counted argument if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// - tokens - Tokens of the original command string. 0 is alias. 1-N are arguments. +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplaceNumberedArgMacro(const wchar_t ch, + std::wstring& appendToStr, + const std::deque& tokens) +{ + if (ch >= L'1' && ch <= L'9') + { + // Numerical macros substitute that numbered argument + const size_t index = ch - L'0'; + + if (index < tokens.size() && index > 0) + { + appendToStr.append(tokens[index]); + } + + return true; + } + + return false; +} + +// Routine Description: +// - Checks the given character to see if it is a wildcard arg replacement macro +// and replaces it with the entire argument string if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// - fullArgString - All of the arguments as one big string. +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplaceWildcardArgMacro(const wchar_t ch, + std::wstring& appendToStr, + const std::wstring fullArgString) +{ + if (L'*' == ch) + { + // Wildcard substitutes all arguments + appendToStr.append(fullArgString); + return true; + } + + return false; +} + +// Routine Description: +// - Checks the given character to see if it is an input redirection macro +// and replaces it with the < redirector if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplaceInputRedirMacro(const wchar_t ch, + std::wstring& appendToStr) +{ + if (L'L' == towupper(ch)) + { + // L (either case) replaces with input redirector < + appendToStr.push_back(L'<'); + return true; + } + return false; +} + +// Routine Description: +// - Checks the given character to see if it is an output redirection macro +// and replaces it with the > redirector if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplaceOutputRedirMacro(const wchar_t ch, + std::wstring& appendToStr) +{ + if (L'G' == towupper(ch)) + { + // G (either case) replaces with output redirector > + appendToStr.push_back(L'>'); + return true; + } + return false; +} + +// Routine Description: +// - Checks the given character to see if it is a pipe redirection macro +// and replaces it with the | redirector if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplacePipeRedirMacro(const wchar_t ch, + std::wstring& appendToStr) +{ + if (L'B' == towupper(ch)) + { + // B (either case) replaces with pipe operator | + appendToStr.push_back(L'|'); + return true; + } + return false; +} + +// Routine Description: +// - Checks the given character to see if it is a next command macro +// and replaces it with CRLF if there is a match +// Arguments: +// - ch - Character to test as a macro +// - appendToStr - Append the macro result here if it matched +// - lineCount - Updates the rolling count of lines if we add a CRLF. +// Return Value: +// - True if we found the macro and appended to the string. +// - False if the given character doesn't match this macro. +bool Alias::s_TryReplaceNextCommandMacro(const wchar_t ch, + std::wstring& appendToStr, + size_t& lineCount) +{ + if (L'T' == towupper(ch)) + { + // T (either case) inserts a CRLF to chain commands + s_AppendCrLf(appendToStr, lineCount); + return true; + } + return false; +} + +// Routine Description: +// - Appends the system line feed (CRLF) to the given string +// Arguments: +// - appendToStr - Append the system line feed here +// - lineCount - Updates the rolling count of lines if we add a CRLF. +void Alias::s_AppendCrLf(std::wstring& appendToStr, + size_t& lineCount) +{ + appendToStr.push_back(L'\r'); + appendToStr.push_back(L'\n'); + lineCount++; +} + +// Routine Description: +// - Searches through the given string for macros and replaces them +// with the matching action +// Arguments: +// - str - On input, the string to search. On output, the string is replaced. +// - tokens - The tokenized command line input. 0 is the alias, 1-N are arguments. +// - fullArgString - Shorthand to 1-N argument string in case of wildcard match. +// Return Value: +// - The number of commands in the final string (line feeds, CRLFs) +size_t Alias::s_ReplaceMacros(std::wstring& str, + const std::deque& tokens, + const std::wstring& fullArgString) +{ + size_t lineCount = 0; + std::wstring finalText; + + // The target text may contain substitution macros indicated by $. + // Walk through and substitute them as appropriate. + for (auto ch = str.cbegin(); ch < str.cend(); ch++) + { + if (L'$' == *ch) + { + // Attempt to read ahead by one character. + const auto chNext = ch + 1; + + if (chNext < str.cend()) + { + auto isProcessed = s_TryReplaceNumberedArgMacro(*chNext, finalText, tokens); + if (!isProcessed) + { + isProcessed = s_TryReplaceWildcardArgMacro(*chNext, finalText, fullArgString); + } + if (!isProcessed) + { + isProcessed = s_TryReplaceInputRedirMacro(*chNext, finalText); + } + if (!isProcessed) + { + isProcessed = s_TryReplaceOutputRedirMacro(*chNext, finalText); + } + if (!isProcessed) + { + isProcessed = s_TryReplacePipeRedirMacro(*chNext, finalText); + } + if (!isProcessed) + { + isProcessed = s_TryReplaceNextCommandMacro(*chNext, finalText, lineCount); + } + if (!isProcessed) + { + // If nothing matches, just push these two characters in. + finalText.push_back(*ch); + finalText.push_back(*chNext); + } + + // Since we read ahead and used that character, + // advance the iterator one extra to compensate. + ch++; + } + else + { + // If no read-ahead, just push this character and be done. + finalText.push_back(*ch); + } + } + else + { + // If it didn't match the macro specifier $, push the character. + finalText.push_back(*ch); + } + } + + // We always terminate with a CRLF to symbolize end of command. + s_AppendCrLf(finalText, lineCount); + + // Give back the final text and count. + str.swap(finalText); + return lineCount; +} + +// Routine Description: +// - Takes the source text and searches it for an alias belonging to exe name's list. +// Arguments: +// - sourceText - The string to search for an alias +// - exeName - The name of the EXE that has aliases associated +// - lineCount - Number of lines worth of text processed. +// Return Value: +// - If we found a matching alias, this will be the processed data +// and lineCount is updated to the new number of lines. +// - If we didn't match and process an alias, return an empty string. +std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, + const std::wstring& exeName, + size_t& lineCount) +{ + // Copy source text into a local for manipulation. + std::wstring sourceCopy(sourceText); + + // Trim trailing \r\n off of sourceCopy if it has one. + s_TrimTrailingCrLf(sourceCopy); + + // Trim leading spaces off of sourceCopy if it has any. + s_TrimLeadingSpaces(sourceCopy); + + // Check if we have an EXE in the list that matches the request first. + auto exeIter = g_aliasData.find(exeName); + if (exeIter == g_aliasData.end()) + { + // We found no data for this exe. Give back an empty string. + return std::wstring(); + } + + auto exeList = exeIter->second; + if (exeList.size() == 0) + { + // If there's no match, give back an empty string. + return std::wstring(); + } + + // Tokenize the text by spaces + const auto tokens = s_Tokenize(sourceCopy); + + // If there are no tokens, return an empty string + if (tokens.size() == 0) + { + return std::wstring(); + } + + // Find alias. If there isn't one, return an empty string + const auto alias = tokens.front(); + const auto aliasIter = exeList.find(alias); + if (aliasIter == exeList.end()) + { + // We found no alias pair with this name. Give back an empty string. + return std::wstring(); + } + + const auto target = aliasIter->second; + if (target.size() == 0) + { + return std::wstring(); + } + + // Get the string of all parameters as a shorthand for $* later. + const auto allParams = s_GetArgString(sourceCopy); + + // The final text will be the target but with macros replaced. + std::wstring finalText(target); + lineCount = s_ReplaceMacros(finalText, tokens, allParams); + + return finalText; +} + +// Routine Description: +// - This routine matches the input string with an alias and copies the alias to the input buffer. +// Arguments: +// - pwchSource - string to match +// - cbSource - length of pwchSource in bytes +// - pwchTarget - where to store matched string +// - cbTargetSize - on input, contains size of pwchTarget. +// - cbTargetWritten - On output, contains length of alias stored in pwchTarget. +// - pwchExe - Name of exe that command is associated with to find related aliases +// - cbExe - Length in bytes of exe name +// - LineCount - aliases can contain multiple commands. $T is the command separator +// Return Value: +// - None. It will just maintain the source as the target if we can't match an alias. +void Alias::s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PWCHAR pwchSource, + _In_ size_t cbSource, + _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, + _In_ const size_t cbTargetSize, + size_t& cbTargetWritten, + const std::wstring& exeName, + DWORD& lines) +{ + try + { + std::wstring sourceText(pwchSource, cbSource / sizeof(WCHAR)); + size_t lineCount = lines; + + const auto targetText = s_MatchAndCopyAlias(sourceText, exeName, lineCount); + + // Only return data if the reply was non-empty (we had a match). + if (!targetText.empty()) + { + const auto cchTargetSize = cbTargetSize / sizeof(wchar_t); + + // If the target text will fit in the result buffer, fill out the results. + if (targetText.size() <= cchTargetSize) + { + // Non-null terminated copy into memory space + std::copy_n(targetText.data(), targetText.size(), pwchTarget); + + // Return bytes copied. + cbTargetWritten = gsl::narrow(targetText.size() * sizeof(wchar_t)); + + // Return lines info. + lines = gsl::narrow(lineCount); + } + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +#ifdef UNIT_TESTING +void Alias::s_TestAddAlias(std::wstring& exe, + std::wstring& alias, + std::wstring& target) +{ + g_aliasData[exe][alias] = target; +} + +void Alias::s_TestClearAliases() +{ + g_aliasData.clear(); +} + +#endif diff --git a/src/host/alias.h b/src/host/alias.h new file mode 100644 index 000000000..2ee82c012 --- /dev/null +++ b/src/host/alias.h @@ -0,0 +1,74 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- alias.h + +Abstract: +- Encapsulates the cmdline functions and structures specifically related to + command alias functionality. +--*/ +#pragma once + + + + +class Alias +{ +public: + static void s_ClearCmdExeAliases(); + + static void s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PWCHAR pwchSource, + _In_ size_t cbSource, + _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, + _In_ const size_t cbTargetSize, + size_t& cbTargetWritten, + const std::wstring& exeName, + DWORD& lines); + + static std::wstring s_MatchAndCopyAlias(const std::wstring& sourceText, + const std::wstring& exeName, + size_t& lineCount); + + +private: + static void s_TrimLeadingSpaces(std::wstring& str); + static void s_TrimTrailingCrLf(std::wstring& str); + static std::deque s_Tokenize(const std::wstring& str); + static std::wstring s_GetArgString(const std::wstring& str); + static size_t s_ReplaceMacros(std::wstring& str, + const std::deque& tokens, + const std::wstring& fullArgString); + + static bool s_TryReplaceNumberedArgMacro(const wchar_t ch, + std::wstring& appendToStr, + const std::deque& tokens); + static bool s_TryReplaceWildcardArgMacro(const wchar_t ch, + std::wstring& appendToStr, + const std::wstring fullArgString); + + static bool s_TryReplaceInputRedirMacro(const wchar_t ch, + std::wstring& appendToStr); + static bool s_TryReplaceOutputRedirMacro(const wchar_t ch, + std::wstring& appendToStr); + static bool s_TryReplacePipeRedirMacro(const wchar_t ch, + std::wstring& appendToStr); + + static bool s_TryReplaceNextCommandMacro(const wchar_t ch, + std::wstring& appendToStr, + size_t& lineCount); + + static void s_AppendCrLf(std::wstring& appendToStr, + size_t& lineCount); + +#ifdef UNIT_TESTING + static void s_TestAddAlias(std::wstring& exe, + std::wstring& alias, + std::wstring& target); + + static void s_TestClearAliases(); + + friend class AliasTests; +#endif +}; diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp new file mode 100644 index 000000000..b83187e2b --- /dev/null +++ b/src/host/cmdline.cpp @@ -0,0 +1,1342 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "cmdline.h" +#include "popup.h" +#include "CommandNumberPopup.hpp" +#include "CommandListPopup.hpp" +#include "CopyFromCharPopup.hpp" +#include "CopyToCharPopup.hpp" + +#include "_output.h" +#include "output.h" +#include "stream.h" +#include "_stream.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "../types/inc/convert.hpp" +#include "srvinit.h" + +#include "ApiRoutines.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// Routine Description: +// - This routine is called when the user changes the screen/popup colors. +// - It goes through the popup structures and changes the saved contents to reflect the new screen/popup colors. +void CommandLine::UpdatePopups(const TextAttribute& NewAttributes, + const TextAttribute& NewPopupAttributes, + const TextAttribute& OldAttributes, + const TextAttribute& OldPopupAttributes) +{ + for (auto& popup : _popups) + { + try + { + popup->UpdateStoredColors(NewAttributes, NewPopupAttributes, OldAttributes, OldPopupAttributes); + } + CATCH_LOG(); + } +} + +// Routine Description: +// - This routine validates a string buffer and returns the pointers of where the strings start within the buffer. +// Arguments: +// - Unicode - Supplies a boolean that is TRUE if the buffer contains Unicode strings, FALSE otherwise. +// - Buffer - Supplies the buffer to be validated. +// - Size - Supplies the size, in bytes, of the buffer to be validated. +// - Count - Supplies the expected number of strings in the buffer. +// ... - Supplies a pair of arguments per expected string. The first one is the expected size, in bytes, of the string +// and the second one receives a pointer to where the string starts. +// Return Value: +// - TRUE if the buffer is valid, FALSE otherwise. +bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...) +{ + va_list Marker; + va_start(Marker, Count); + + while (Count > 0) + { + ULONG const StringSize = va_arg(Marker, ULONG); + PVOID* StringStart = va_arg(Marker, PVOID *); + + // Make sure the string fits in the supplied buffer and that it is properly aligned. + if (StringSize > Size) + { + break; + } + + if ((Unicode != false) && ((StringSize % sizeof(WCHAR)) != 0)) + { + break; + } + + *StringStart = Buffer; + + // Go to the next string. + Buffer = RtlOffsetToPointer(Buffer, StringSize); + Size -= StringSize; + Count -= 1; + } + + va_end(Marker); + + return Count == 0; +} + +// Routine Description: +// - Detects Word delimiters +bool IsWordDelim(const wchar_t wch) +{ + // the space character is always a word delimiter. Do not add it to the WordDelimiters global because + // that contains the user configurable word delimiters only. + if (wch == UNICODE_SPACE) + { + return true; + } + const auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; + return std::find(delimiters.begin(), delimiters.end(), wch) != delimiters.end(); +} + +bool IsWordDelim(const std::wstring_view charData) +{ + if (charData.size() != 1) + { + return false; + } + return IsWordDelim(charData.front()); +} + +CommandLine::CommandLine() : + _isVisible{ true } +{ + +} + +CommandLine::~CommandLine() +{ + +} + +CommandLine& CommandLine::Instance() +{ + static CommandLine c; + return c; +} + +bool CommandLine::IsEditLineEmpty() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + if (!gci.HasPendingCookedRead()) + { + // If the cooked read data pointer is null, there is no edit line data and therefore it's empty. + return true; + } + else if (0 == gci.CookedReadData().VisibleCharCount()) + { + // If we had a valid pointer, but there are no visible characters for the edit line, then it's empty. + // Someone started editing and back spaced the whole line out so it exists, but has no data. + return true; + } + else + { + return false; + } +} + +void CommandLine::Hide(const bool fUpdateFields) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (!IsEditLineEmpty()) + { + DeleteCommandLine(gci.CookedReadData(), fUpdateFields); + } + _isVisible = false; +} + +void CommandLine::Show() +{ + _isVisible = true; + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (!IsEditLineEmpty()) + { + RedrawCommandLine(gci.CookedReadData()); + } +} + +// Routine Description: +// - Returns true if the commandline is currently being displayed. This is false +// after Hide() is called, and before Show() is called again. +// Return Value: +// - true if the commandline should be displayed. Does not take into account +// the echo state of the input. This is only controlled by calls to Hide/Show +bool CommandLine::IsVisible() const noexcept +{ + return _isVisible; +} + +// Routine Description: +// - checks for the presence of a popup +// Return Value: +// - true if popup is present +bool CommandLine::HasPopup() const noexcept +{ + return !_popups.empty(); +} + +// Routine Description: +// - gets the topmost popup +// Arguments: +// Return Value: +// - ref to the topmost popup +Popup& CommandLine::GetPopup() +{ + return *_popups.front(); +} + +// Routine Description: +// - stops the current popup +void CommandLine::EndCurrentPopup() +{ + if (!_popups.empty()) + { + _popups.front()->End(); + _popups.pop_front(); + } +} + +// Routine Description: +// - stops all popups +void CommandLine::EndAllPopups() +{ + while (!_popups.empty()) + { + EndCurrentPopup(); + } +} + +void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields) +{ + size_t CharsToWrite = cookedReadData.VisibleCharCount(); + COORD coordOriginalCursor = cookedReadData.OriginalCursorPosition(); + const COORD coordBufferSize = cookedReadData.ScreenInfo().GetBufferSize().Dimensions(); + + // catch the case where the current command has scrolled off the top of the screen. + if (coordOriginalCursor.Y < 0) + { + CharsToWrite += coordBufferSize.X * coordOriginalCursor.Y; + CharsToWrite += cookedReadData.OriginalCursorPosition().X; // account for prompt + cookedReadData.OriginalCursorPosition().X = 0; + cookedReadData.OriginalCursorPosition().Y = 0; + coordOriginalCursor.X = 0; + coordOriginalCursor.Y = 0; + } + + if (!CheckBisectStringW(cookedReadData.BufferStartPtr(), + CharsToWrite, + coordBufferSize.X - cookedReadData.OriginalCursorPosition().X)) + { + CharsToWrite++; + } + + try + { + cookedReadData.ScreenInfo().Write(OutputCellIterator(UNICODE_SPACE, CharsToWrite), coordOriginalCursor); + } + CATCH_LOG(); + + if (fUpdateFields) + { + cookedReadData.Erase(); + } + + LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.OriginalCursorPosition(), true)); +} + +void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) +{ + if (cookedReadData.IsEchoInput()) + { + // Draw the command line + cookedReadData.OriginalCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + + SHORT ScrollY = 0; +#pragma prefast(suppress:28931, "Status is not unused. It's used in debug assertions.") + NTSTATUS Status = WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY); + FAIL_FAST_IF_NTSTATUS_FAILED(Status); + + cookedReadData.OriginalCursorPosition().Y += ScrollY; + + // Move the cursor back to the right position + COORD CursorPosition = cookedReadData.OriginalCursorPosition(); + CursorPosition.X += (SHORT)RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint()); + if (CheckBisectStringW(cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint(), + cookedReadData.ScreenInfo().GetBufferSize().Width() - cookedReadData.OriginalCursorPosition().X)) + { + CursorPosition.X++; + } + Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr); + FAIL_FAST_IF_NTSTATUS_FAILED(Status); + } +} + +// Routine Description: +// - This routine copies the commandline specified by Index into the cooked read buffer +void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) // index, not command number +{ + DeleteCommandLine(cookedReadData, TRUE); + FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, + cookedReadData.SpanWholeBuffer(), + cookedReadData.BytesRead())); + FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); + if (cookedReadData.IsEchoInput()) + { + SHORT ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + } + + size_t const CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); + cookedReadData.InsertionPoint() = CharsToWrite; + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); +} + +// Routine Description: +// - This routine handles the command list popup. It puts up the popup, then calls ProcessCommandListInput to get and process input. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - STATUS_SUCCESS - read was fully completed (user hit return) +[[nodiscard]] +NTSTATUS CommandLine::_startCommandListPopup(COOKED_READ_DATA& cookedReadData) +{ + if (cookedReadData.HasHistory() && + cookedReadData.History().GetNumberOfCommands()) + { + try + { + auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo(), + cookedReadData.History())); + popup.Draw(); + return popup.Process(cookedReadData); + } + CATCH_RETURN(); + } + else + { + return S_FALSE; + } +} + +// Routine Description: +// - This routine handles the "delete up to this char" popup. It puts up the popup, then calls ProcessCopyFromCharInput to get and process input. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - STATUS_SUCCESS - read was fully completed (user hit return) +[[nodiscard]] +NTSTATUS CommandLine::_startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData) +{ + // Delete the current command from cursor position to the + // letter specified by the user. The user is prompted via + // popup to enter a character. + if (cookedReadData.HasHistory()) + { + try + { + auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); + popup.Draw(); + return popup.Process(cookedReadData); + } + CATCH_RETURN(); + } + else + { + return S_FALSE; + } +} + +// Routine Description: +// - This routine handles the "copy up to this char" popup. It puts up the popup, then calls ProcessCopyToCharInput to get and process input. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - STATUS_SUCCESS - read was fully completed (user hit return) +// - S_FALSE - if we couldn't make a popup because we had no commands +[[nodiscard]] +NTSTATUS CommandLine::_startCopyToCharPopup(COOKED_READ_DATA& cookedReadData) +{ + // copy the previous command to the current command, up to but + // not including the character specified by the user. the user + // is prompted via popup to enter a character. + if (cookedReadData.HasHistory()) + { + try + { + auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); + popup.Draw(); + return popup.Process(cookedReadData); + } + CATCH_RETURN(); + } + else + { + return S_FALSE; + } +} + +// Routine Description: +// - This routine handles the "enter command number" popup. It puts up the popup, then calls ProcessCommandNumberInput to get and process input. +// Return Value: +// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created +// - STATUS_SUCCESS - read was fully completed (user hit return) +// - S_FALSE - if we couldn't make a popup because we had no commands or it wouldn't fit. +[[nodiscard]] +HRESULT CommandLine::StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData) +{ + if (cookedReadData.HasHistory() && + cookedReadData.History().GetNumberOfCommands() && + cookedReadData.ScreenInfo().GetBufferSize().Width() >= MINIMUM_COMMAND_PROMPT_SIZE + 2) + { + try + { + auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); + popup.Draw(); + + // Save the original cursor position in case the user cancels out of the dialog + cookedReadData.BeforeDialogCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + + // Move the cursor into the dialog so the user can type multiple characters for the command number + const COORD CursorPosition = popup.GetCursorPosition(); + LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(CursorPosition, TRUE)); + + // Transfer control to the handler routine + return popup.Process(cookedReadData); + } + CATCH_RETURN(); + } + else + { + return S_FALSE; + } +} + +// Routine Description: +// - Process virtual key code and updates the prompt line with the next history element in the direction +// specified by wch +// Arguments: +// - cookedReadData - The cooked read data to operate on +// - searchDirection - Direction in history to search +// Note: +// - May throw exceptions +void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData, + const CommandHistory::SearchDirection searchDirection) +{ + + // for doskey compatibility, buffer isn't circular. don't do anything if attempting + // to cycle history past the bounds of the history buffer + if (!cookedReadData.HasHistory()) + { + return; + } + else if (searchDirection == CommandHistory::SearchDirection::Previous + && cookedReadData.History().AtFirstCommand()) + { + return; + } + else if (searchDirection == CommandHistory::SearchDirection::Next + && cookedReadData.History().AtLastCommand()) + { + return; + } + + DeleteCommandLine(cookedReadData, true); + THROW_IF_FAILED(cookedReadData.History().Retrieve(searchDirection, + cookedReadData.SpanWholeBuffer(), + cookedReadData.BytesRead())); + FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + } + const size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); + cookedReadData.InsertionPoint() = CharsToWrite; + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); +} + +// Routine Description: +// - Sets the text on the prompt to the oldest run command in the cookedReadData's history +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Note: +// - May throw exceptions +void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData) +{ + if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) + { + DeleteCommandLine(cookedReadData, true); + const short commandNumber = 0; + THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, + cookedReadData.SpanWholeBuffer(), + cookedReadData.BytesRead())); + FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + } + size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); + cookedReadData.InsertionPoint() = CharsToWrite; + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); + } +} + +// Routine Description: +// - Sets the text on the prompt the most recently run command in cookedReadData's history +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Note: +// - May throw exceptions +void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData) +{ + DeleteCommandLine(cookedReadData, true); + if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) + { + const short commandNumber = (SHORT)(cookedReadData.History().GetNumberOfCommands() - 1); + THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, + cookedReadData.SpanWholeBuffer(), + cookedReadData.BytesRead())); + FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + } + size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); + cookedReadData.InsertionPoint() = CharsToWrite; + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); + } +} + +// Routine Description: +// - Deletes all prompt text to the right of the cursor +// Arguments: +// - cookedReadData - The cooked read data to operate on +void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept +{ + DeleteCommandLine(cookedReadData, false); + cookedReadData.BytesRead() = cookedReadData.InsertionPoint() * sizeof(WCHAR); + if (cookedReadData.IsEchoInput()) + { + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr)); + } +} + +// Routine Description: +// - Deletes all user input on the prompt to the left of the cursor +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept +{ + DeleteCommandLine(cookedReadData, false); + cookedReadData.BytesRead() -= cookedReadData.InsertionPoint() * sizeof(WCHAR); + cookedReadData.InsertionPoint() = 0; + memmove(cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BytesRead()); + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); + if (cookedReadData.IsEchoInput()) + { + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr)); + } + return cookedReadData.OriginalCursorPosition(); +} + +// Routine Description: +// - Moves the cursor to the end of the prompt text +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept +{ + cookedReadData.InsertionPoint() = cookedReadData.BytesRead() / sizeof(WCHAR); + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + cookedReadData.InsertionPoint()); + COORD cursorPosition{ 0, 0 }; + cursorPosition.X = (SHORT)(cookedReadData.OriginalCursorPosition().X + cookedReadData.VisibleCharCount()); + cursorPosition.Y = cookedReadData.OriginalCursorPosition().Y; + + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + if (CheckBisectProcessW(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint(), + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, + cookedReadData.OriginalCursorPosition().X, + true)) + { + cursorPosition.X++; + } + return cursorPosition; +} + +// Routine Description: +// - Moves the cursor to the start of the user input on the prompt +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept +{ + cookedReadData.InsertionPoint() = 0; + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); + return cookedReadData.OriginalCursorPosition(); +} + +// Routine Description: +// - Moves the cursor left by a word +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - New cursor position +COORD CommandLine::_moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept +{ + PWCHAR LastWord; + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) + { + // A bit better word skipping. + LastWord = cookedReadData.BufferCurrentPtr() - 1; + if (LastWord != cookedReadData.BufferStartPtr()) + { + if (*LastWord == L' ') + { + // Skip spaces, until the non-space character is found. + while (--LastWord != cookedReadData.BufferStartPtr()) + { + FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); + if (*LastWord != L' ') + { + break; + } + } + } + if (LastWord != cookedReadData.BufferStartPtr()) + { + if (IsWordDelim(*LastWord)) + { + // Skip WORD_DELIMs until space or non WORD_DELIM is found. + while (--LastWord != cookedReadData.BufferStartPtr()) + { + FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); + if (*LastWord == L' ' || !IsWordDelim(*LastWord)) + { + break; + } + } + } + else + { + // Skip the regular words + while (--LastWord != cookedReadData.BufferStartPtr()) + { + FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); + if (IsWordDelim(*LastWord)) + { + break; + } + } + } + } + FAIL_FAST_IF(!(LastWord >= cookedReadData.BufferStartPtr())); + if (LastWord != cookedReadData.BufferStartPtr()) + { + + // LastWord is currently pointing to the last character + // of the previous word, unless it backed up to the beginning + // of the buffer. + // Let's increment LastWord so that it points to the expeced + // insertion point. + ++LastWord; + } + cookedReadData.SetBufferCurrentPtr(LastWord); + } + cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); + cursorPosition = cookedReadData.OriginalCursorPosition(); + cursorPosition.X = (SHORT)(cursorPosition.X + + RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint())); + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + if (CheckBisectStringW(cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint() + 1, + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X)) + { + cursorPosition.X++; + } + } + return cursorPosition; +} + +// Routine Description: +// - Moves cursor left by a glyph +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - New cursor position +COORD CommandLine::_moveCursorLeft(COOKED_READ_DATA& cookedReadData) +{ + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) + { + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() - 1); + cookedReadData.InsertionPoint()--; + cursorPosition.X = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().X; + cursorPosition.Y = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().Y; + cursorPosition.X = (SHORT)(cursorPosition.X - + RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint())); + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + if (CheckBisectProcessW(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint() + 2, + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, + cookedReadData.OriginalCursorPosition().X, + true)) + { + if ((cursorPosition.X == -2) || (cursorPosition.X == -1)) + { + cursorPosition.X--; + } + } + } + return cursorPosition; +} + +// Routine Description: +// - Moves the cursor to the right by a word +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept +{ + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) + { + PWCHAR NextWord = cookedReadData.BufferCurrentPtr(); + + // A bit better word skipping. + PWCHAR BufLast = cookedReadData.BufferStartPtr() + cookedReadData.BytesRead() / sizeof(WCHAR); + + FAIL_FAST_IF(!(NextWord < BufLast)); + if (*NextWord == L' ') + { + // If the current character is space, skip to the next non-space character. + while (NextWord < BufLast) + { + if (*NextWord != L' ') + { + break; + } + ++NextWord; + } + } + else + { + // Skip the body part. + bool fStartFromDelim = IsWordDelim(*NextWord); + + while (++NextWord < BufLast) + { + if (fStartFromDelim != IsWordDelim(*NextWord)) + { + break; + } + } + + // Skip the space block. + if (NextWord < BufLast && *NextWord == L' ') + { + while (++NextWord < BufLast) + { + if (*NextWord != L' ') + { + break; + } + } + } + } + + cookedReadData.SetBufferCurrentPtr(NextWord); + cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); + cursorPosition = cookedReadData.OriginalCursorPosition(); + cursorPosition.X = (SHORT)(cursorPosition.X + + RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint())); + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + if (CheckBisectStringW(cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint() + 1, + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X)) + { + cursorPosition.X++; + } + } + return cursorPosition; +} + +// Routine Description: +// - Moves the cursor to the right by a glyph +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept +{ + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + // If not at the end of the line, move cursor position right. + if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) + { + cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + cursorPosition.X = (SHORT)(cursorPosition.X + + RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint())); + if (CheckBisectProcessW(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint() + 2, + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, + cookedReadData.OriginalCursorPosition().X, + true)) + { + if (cursorPosition.X == (sScreenBufferSizeX - 1)) + cursorPosition.X++; + } + + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); + cookedReadData.InsertionPoint()++; + } + // if at the end of the line, copy a character from the same position in the last command + else if (cookedReadData.HasHistory()) + { + size_t NumSpaces; + const auto LastCommand = cookedReadData.History().GetLastCommand(); + if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) + { + *cookedReadData.BufferCurrentPtr() = LastCommand[cookedReadData.InsertionPoint()]; + cookedReadData.BytesRead() += sizeof(WCHAR); + cookedReadData.InsertionPoint()++; + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + size_t CharsToWrite = sizeof(WCHAR); + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &CharsToWrite, + &NumSpaces, + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + cookedReadData.VisibleCharCount() += NumSpaces; + // update reported cursor position + if (ScrollY != 0) + { + cursorPosition.X = 0; + cursorPosition.Y += ScrollY; + } + else + { + cursorPosition.X += 1; + } + } + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); + } + } + return cursorPosition; +} + +// Routine Description: +// - Place a ctrl-z in the current command line +// Arguments: +// - cookedReadData - The cooked read data to operate on +void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept +{ + size_t NumSpaces = 0; + + *cookedReadData.BufferCurrentPtr() = (WCHAR)0x1a; // ctrl-z + cookedReadData.BytesRead() += sizeof(WCHAR); + cookedReadData.InsertionPoint()++; + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + size_t CharsToWrite = sizeof(WCHAR); + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &CharsToWrite, + &NumSpaces, + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + cookedReadData.VisibleCharCount() += NumSpaces; + } + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); +} + +// Routine Description: +// - Empties the command history for cookedReadData +// Arguments: +// - cookedReadData - The cooked read data to operate on +void CommandLine::_deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept +{ + if (cookedReadData.HasHistory()) + { + cookedReadData.History().Empty(); + cookedReadData.History().Flags |= CLE_ALLOCATED; + } +} + +// Routine Description: +// - Copy the remainder of the previous command to the current command. +// Arguments: +// - cookedReadData - The cooked read data to operate on +void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept +{ + if (cookedReadData.HasHistory()) + { + size_t NumSpaces, cchCount; + + const auto LastCommand = cookedReadData.History().GetLastCommand(); + if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) + { + cchCount = LastCommand.size() - cookedReadData.InsertionPoint(); + const auto bufferSpan = cookedReadData.SpanAtPointer(); + std::copy_n(LastCommand.cbegin() + cookedReadData.InsertionPoint(), cchCount, bufferSpan.begin()); + cookedReadData.InsertionPoint() += cchCount; + cchCount *= sizeof(WCHAR); + cookedReadData.BytesRead() = std::max(LastCommand.size() * sizeof(wchar_t), cookedReadData.BytesRead()); + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cchCount, + &NumSpaces, + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + cookedReadData.VisibleCharCount() += NumSpaces; + } + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + cchCount / sizeof(WCHAR)); + } + } +} + +// Routine Description: +// - Cycles through the stored commands that start with the characters in the current command. +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData) +{ + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + if (cookedReadData.HasHistory()) + { + SHORT index; + if (cookedReadData.History().FindMatchingCommand({ cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() }, + cookedReadData.History().LastDisplayed, + index, + CommandHistory::MatchOptions::None)) + { + SHORT CurrentPos; + + // save cursor position + CurrentPos = (SHORT)cookedReadData.InsertionPoint(); + + DeleteCommandLine(cookedReadData, true); + THROW_IF_FAILED(cookedReadData.History().RetrieveNth((SHORT)index, + cookedReadData.SpanWholeBuffer(), + cookedReadData.BytesRead())); + FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); + if (cookedReadData.IsEchoInput()) + { + short ScrollY = 0; + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + cookedReadData.OriginalCursorPosition().Y += ScrollY; + cursorPosition.Y += ScrollY; + } + + // restore cursor position + cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CurrentPos); + cookedReadData.InsertionPoint() = CurrentPos; + FAIL_FAST_IF_NTSTATUS_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cursorPosition, true)); + } + } + return cursorPosition; +} + +// Routine Description: +// - Deletes a glyph from the right side of the cursor +// Arguments: +// - cookedReadData - The cooked read data to operate on +// Return Value: +// - The new cursor position +COORD CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept +{ + // save cursor position + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + + if (!cookedReadData.AtEol()) + { + // Delete commandline. +#pragma prefast(suppress:__WARNING_BUFFER_OVERFLOW, "Not sure why prefast is getting confused here") + DeleteCommandLine(cookedReadData, false); + + // Delete char. + cookedReadData.BytesRead() -= sizeof(WCHAR); + memmove(cookedReadData.BufferCurrentPtr(), + cookedReadData.BufferCurrentPtr() + 1, + cookedReadData.BytesRead() - (cookedReadData.InsertionPoint() * sizeof(WCHAR))); + + { + PWCHAR buf = (PWCHAR)((PBYTE)cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()); + *buf = (WCHAR)' '; + } + + // Write commandline. + if (cookedReadData.IsEchoInput()) + { + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + cookedReadData.BufferStartPtr(), + &cookedReadData.BytesRead(), + &cookedReadData.VisibleCharCount(), + cookedReadData.OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr)); + } + + // restore cursor position + const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); + if (CheckBisectProcessW(cookedReadData.ScreenInfo(), + cookedReadData.BufferStartPtr(), + cookedReadData.InsertionPoint() + 1, + sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, + cookedReadData.OriginalCursorPosition().X, + true)) + { + cursorPosition.X++; + } + } + return cursorPosition; +} + +// TODO: [MSFT:4586207] Clean up this mess -- needs helpers. http://osgvsowi/4586207 +// Routine Description: +// - This routine process command line editing keys. +// Return Value: +// - CONSOLE_STATUS_WAIT - CommandListPopup ran out of input +// - CONSOLE_STATUS_READ_COMPLETE - user hit in CommandListPopup +// - STATUS_SUCCESS - everything's cool +[[nodiscard]] +NTSTATUS CommandLine::ProcessCommandLine(COOKED_READ_DATA& cookedReadData, + _In_ WCHAR wch, + const DWORD dwKeyState) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); + NTSTATUS Status; + + const bool altPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); + const bool ctrlPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); + bool UpdateCursorPosition = false; + switch (wch) + { + case VK_ESCAPE: + DeleteCommandLine(cookedReadData, true); + break; + case VK_DOWN: + try + { + _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + Status = STATUS_SUCCESS; + } + catch (...) + { + Status = wil::ResultFromCaughtException(); + } + break; + case VK_UP: + case VK_F5: + try + { + _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + Status = STATUS_SUCCESS; + } + catch (...) + { + Status = wil::ResultFromCaughtException(); + } + break; + case VK_PRIOR: + try + { + _setPromptToOldestCommand(cookedReadData); + Status = STATUS_SUCCESS; + } + catch (...) + { + Status = wil::ResultFromCaughtException(); + } + break; + case VK_NEXT: + try + { + _setPromptToNewestCommand(cookedReadData); + Status = STATUS_SUCCESS; + } + catch (...) + { + Status = wil::ResultFromCaughtException(); + } + break; + case VK_END: + if (ctrlPressed) + { + DeletePromptAfterCursor(cookedReadData); + } + else + { + cursorPosition = _moveCursorToEndOfPrompt(cookedReadData); + UpdateCursorPosition = true; + } + break; + case VK_HOME: + if (ctrlPressed) + { + cursorPosition = _deletePromptBeforeCursor(cookedReadData); + UpdateCursorPosition = true; + } + else + { + cursorPosition = _moveCursorToStartOfPrompt(cookedReadData); + UpdateCursorPosition = true; + } + break; + case VK_LEFT: + if (ctrlPressed) + { + cursorPosition = _moveCursorLeftByWord(cookedReadData); + UpdateCursorPosition = true; + } + else + { + cursorPosition = _moveCursorLeft(cookedReadData); + UpdateCursorPosition = true; + } + break; + case VK_F1: + { + // we don't need to check for end of buffer here because we've + // already done it. + cursorPosition = _moveCursorRight(cookedReadData); + UpdateCursorPosition = true; + break; + } + case VK_RIGHT: + // we don't need to check for end of buffer here because we've + // already done it. + if (ctrlPressed) + { + cursorPosition = _moveCursorRightByWord(cookedReadData); + UpdateCursorPosition = true; + } + else + { + cursorPosition = _moveCursorRight(cookedReadData); + UpdateCursorPosition = true; + } + break; + case VK_F2: + { + Status = _startCopyToCharPopup(cookedReadData); + if (S_FALSE == Status) + { + // We couldn't make the popup, so loop around and read the next character. + break; + } + else + { + return Status; + } + } + case VK_F3: + _fillPromptWithPreviousCommandFragment(cookedReadData); + break; + case VK_F4: + { + Status = _startCopyFromCharPopup(cookedReadData); + if (S_FALSE == Status) + { + // We couldn't display a popup. Go around a loop behind. + break; + } + else + { + return Status; + } + } + case VK_F6: + { + _insertCtrlZ(cookedReadData); + break; + } + case VK_F7: + if (!ctrlPressed && !altPressed) + { + Status = _startCommandListPopup(cookedReadData); + } + else if (altPressed) + { + _deleteCommandHistory(cookedReadData); + } + break; + + case VK_F8: + try + { + cursorPosition = _cycleMatchingCommandHistoryToPrompt(cookedReadData); + UpdateCursorPosition = true; + } + catch (...) + { + Status = wil::ResultFromCaughtException(); + } + break; + case VK_F9: + { + Status = StartCommandNumberPopup(cookedReadData); + if (S_FALSE == Status) + { + // If we couldn't make the popup, break and go around to read another input character. + break; + } + else + { + return Status; + } + } + case VK_F10: + // Alt+F10 clears the aliases for specifically cmd.exe. + if (altPressed) + { + Alias::s_ClearCmdExeAliases(); + } + break; + case VK_INSERT: + cookedReadData.SetInsertMode(!cookedReadData.IsInsertMode()); + cookedReadData.ScreenInfo().SetCursorDBMode(cookedReadData.IsInsertMode() != gci.GetInsertMode()); + break; + case VK_DELETE: + cursorPosition = DeleteFromRightOfCursor(cookedReadData); + UpdateCursorPosition = true; + break; + default: + FAIL_FAST_HR(E_NOTIMPL); + break; + } + + if (UpdateCursorPosition && cookedReadData.IsEchoInput()) + { + Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr); + FAIL_FAST_IF_NTSTATUS_FAILED(Status); + } + + return STATUS_SUCCESS; +} diff --git a/src/host/cmdline.h b/src/host/cmdline.h new file mode 100644 index 000000000..e705c858b --- /dev/null +++ b/src/host/cmdline.h @@ -0,0 +1,169 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- cmdline.h + +Abstract: +- This file contains the internal structures and definitions used by command line input and editing. + +Author: +- Therese Stowell (ThereseS) 15-Nov-1991 + +Revision History: +- Mike Griese (migrie) Jan 2018: + Refactored the history and alias functionality into their own files. +- Michael Niksa (miniksa) May 2018: + Split apart popup information. Started encapsulating command line things. Removed 0 length buffers. +Notes: + The input model for the command line editing popups is complex. + Here is the relevant pseudocode: + + CookedReadWaitRoutine + if (CookedRead->Popup) + Status = (*CookedRead->Popup->Callback)(); + if (Status == CONSOLE_STATUS_READ_COMPLETE) + return STATUS_SUCCESS; + return Status; + + CookedRead + if (Command Line Editing Key) + ProcessCommandLine + else + process regular key + + ProcessCommandLine + if F7 + return Popup + + Popup + draw popup + return ProcessCommandListInput + + ProcessCommandListInput + while (TRUE) + GetChar + if (wait) + return wait + switch (char) + . + . + . +--*/ + +#pragma once + +#include "input.h" +#include "screenInfo.hpp" +#include "server.h" + +#include "history.h" +#include "alias.h" +#include "readDataCooked.hpp" +#include "popup.h" + + +class CommandLine +{ +public: + ~CommandLine(); + + static CommandLine& Instance(); + + bool IsEditLineEmpty() const; + void Hide(const bool fUpdateFields); + void Show(); + bool IsVisible() const noexcept; + + [[nodiscard]] + NTSTATUS ProcessCommandLine(COOKED_READ_DATA& cookedReadData, + _In_ WCHAR wch, + const DWORD dwKeyState); + + [[nodiscard]] + HRESULT StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData); + + bool HasPopup() const noexcept; + Popup& GetPopup(); + + void UpdatePopups(const TextAttribute& NewAttributes, + const TextAttribute& NewPopupAttributes, + const TextAttribute& OldAttributes, + const TextAttribute& OldPopupAttributes); + + void EndCurrentPopup(); + void EndAllPopups(); + + void DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept; + COORD DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept; +protected: + CommandLine(); + + // delete these because we don't want to accidentally get copies of the singleton + CommandLine(CommandLine const&) = delete; + CommandLine& operator=(CommandLine const&) = delete; + + [[nodiscard]] + NTSTATUS CommandLine::_startCommandListPopup(COOKED_READ_DATA& cookedReadData); + [[nodiscard]] + NTSTATUS CommandLine::_startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData); + [[nodiscard]] + NTSTATUS CommandLine::_startCopyToCharPopup(COOKED_READ_DATA& cookedReadData); + + void _processHistoryCycling(COOKED_READ_DATA& cookedReadData, const CommandHistory::SearchDirection searchDirection); + void _setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData); + void _setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData); + COORD _deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _moveCursorLeft(COOKED_READ_DATA& cookedReadData); + COORD _moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept; + void _insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept; + void _deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept; + void _fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept; + COORD _cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData); + + +#ifdef UNIT_TESTING + friend class CommandLineTests; + friend class CommandNumberPopupTests; +#endif + +private: + + std::deque> _popups; + bool _isVisible; +}; + + +void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields); + +void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); + +// Values for WriteChars(), WriteCharsLegacy() dwFlags +#define WC_DESTRUCTIVE_BACKSPACE 0x01 +#define WC_KEEP_CURSOR_VISIBLE 0x02 +#define WC_ECHO 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 +//#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 + +// Word delimiters +bool IsWordDelim(const WCHAR wch); +bool IsWordDelim(const std::wstring_view charData); + +[[nodiscard]] +HRESULT DoSrvSetConsoleTitleW(const std::wstring_view title) noexcept; + +bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...); + +void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index); diff --git a/src/host/conapi.h b/src/host/conapi.h new file mode 100644 index 000000000..db5c2a41f --- /dev/null +++ b/src/host/conapi.h @@ -0,0 +1,24 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conapi.h + +Abstract: +- This module contains the internal structures and definitions used by the console server. + +Author: +- Therese Stowell (ThereseS) 12-Nov-1990 + +Revision History: +--*/ + +#pragma once + +#include +#include +#include +#include + +#include "..\server\ApiMessage.h" diff --git a/src/host/conareainfo.cpp b/src/host/conareainfo.cpp new file mode 100644 index 000000000..6b935de60 --- /dev/null +++ b/src/host/conareainfo.cpp @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "conareainfo.h" + +#include "_output.h" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/viewport.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +ConversionAreaBufferInfo::ConversionAreaBufferInfo(const COORD coordBufferSize) : + coordCaBuffer(coordBufferSize), + rcViewCaWindow({ 0 }), + coordConView({ 0 }) +{ +} + +ConversionAreaInfo::ConversionAreaInfo(const COORD bufferSize, + const COORD windowSize, + const CHAR_INFO fill, + const CHAR_INFO popupFill, + const FontInfo fontInfo) : + _caInfo{ bufferSize }, + _isHidden{ true }, + _screenBuffer{ nullptr } +{ + SCREEN_INFORMATION* pNewScreen = nullptr; + + // cursor has no height because it won't be rendered for conversion area + THROW_IF_NTSTATUS_FAILED(SCREEN_INFORMATION::CreateInstance(windowSize, + fontInfo, + bufferSize, + { fill.Attributes }, + { popupFill.Attributes }, + 0, + &pNewScreen)); + + // Suppress painting notifications for modifying a conversion area cursor as they're not actually rendered. + pNewScreen->GetTextBuffer().GetCursor().SetIsConversionArea(true); + pNewScreen->ConvScreenInfo = this; + + _screenBuffer.reset(pNewScreen); +} + +ConversionAreaInfo::ConversionAreaInfo(ConversionAreaInfo&& other) : + _caInfo(other._caInfo), + _isHidden(other._isHidden), + _screenBuffer(nullptr) +{ + std::swap(_screenBuffer, other._screenBuffer); +} + +// Routine Description: +// - Describes whether the conversion area should be drawn or should be hidden. +// Arguments: +// - +// Return Value: +// - True if it should not be drawn. False if it should be drawn. +bool ConversionAreaInfo::IsHidden() const noexcept +{ + return _isHidden; +} + +// Routine Description: +// - Sets a value describing whether the conversion area should be drawn or should be hidden. +// Arguments: +// - fIsHidden - True if it should not be drawn. False if it should be drawn. +// Return Value: +// - +void ConversionAreaInfo::SetHidden(const bool fIsHidden) noexcept +{ + _isHidden = fIsHidden; +} + +// Routine Description: +// - Retrieves the underlying text buffer for use in rendering data +const TextBuffer& ConversionAreaInfo::GetTextBuffer() const noexcept +{ + return _screenBuffer->GetTextBuffer(); +} + +// Routine Description: +// - Retrieves the layout/overlay information about where to place this conversion area relative to the +// existing screen buffers and viewports. +const ConversionAreaBufferInfo& ConversionAreaInfo::GetAreaBufferInfo() const noexcept +{ + return _caInfo; +} + +// Routine Description: +// - Forwards a color attribute setting request to the internal screen information +// Arguments: +// - attr - Color to apply to internal screen buffer +void ConversionAreaInfo::SetAttributes(const TextAttribute& attr) +{ + _screenBuffer->SetAttributes(attr); +} + +// Routine Description: +// - Writes text into the conversion area. Since conversion areas are only +// one line, you can only specify the column to write. +// Arguments: +// - text - Text to insert into the conversion area buffer +// - column - Column to start at (X position) +void ConversionAreaInfo::WriteText(const std::vector& text, + const SHORT column) +{ + std::basic_string_view view(text.data(), text.size()); + _screenBuffer->Write(view, { column, 0 }); +} + +// Routine Description: +// - Clears out a conversion area +void ConversionAreaInfo::ClearArea() noexcept +{ + SetHidden(true); + + try + { + _screenBuffer->ClearTextData(); + } + CATCH_LOG(); + + Paint(); +} + +[[nodiscard]] +HRESULT ConversionAreaInfo::Resize(const COORD newSize) noexcept +{ + // attempt to resize underlying buffers + RETURN_IF_NTSTATUS_FAILED(_screenBuffer->ResizeScreenBuffer(newSize, FALSE)); + + // store new size + _caInfo.coordCaBuffer = newSize; + + // restrict viewport to buffer size. + const COORD restriction = { newSize.X - 1i16, newSize.Y - 1i16 }; + _caInfo.rcViewCaWindow.Left = std::min(_caInfo.rcViewCaWindow.Left, restriction.X); + _caInfo.rcViewCaWindow.Right = std::min(_caInfo.rcViewCaWindow.Right, restriction.X); + _caInfo.rcViewCaWindow.Top = std::min(_caInfo.rcViewCaWindow.Top, restriction.Y); + _caInfo.rcViewCaWindow.Bottom = std::min(_caInfo.rcViewCaWindow.Bottom, restriction.Y); + + return S_OK; +} + + +void ConversionAreaInfo::SetWindowInfo(const SMALL_RECT view) noexcept +{ + if (view.Left != _caInfo.rcViewCaWindow.Left || + view.Top != _caInfo.rcViewCaWindow.Top || + view.Right != _caInfo.rcViewCaWindow.Right || + view.Bottom != _caInfo.rcViewCaWindow.Bottom) + { + if (!IsHidden()) + { + SetHidden(true); + Paint(); + + _caInfo.rcViewCaWindow = view; + SetHidden(false); + Paint(); + } + else + { + _caInfo.rcViewCaWindow = view; + } + } +} + +void ConversionAreaInfo::SetViewPos(const COORD pos) noexcept +{ + if (IsHidden()) + { + _caInfo.coordConView = pos; + } + else + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + SMALL_RECT OldRegion = _caInfo.rcViewCaWindow;; + OldRegion.Left += _caInfo.coordConView.X; + OldRegion.Right += _caInfo.coordConView.X; + OldRegion.Top += _caInfo.coordConView.Y; + OldRegion.Bottom += _caInfo.coordConView.Y; + WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(OldRegion)); + + _caInfo.coordConView = pos; + + SMALL_RECT NewRegion = _caInfo.rcViewCaWindow; + NewRegion.Left += _caInfo.coordConView.X; + NewRegion.Right += _caInfo.coordConView.X; + NewRegion.Top += _caInfo.coordConView.Y; + NewRegion.Bottom += _caInfo.coordConView.Y; + WriteToScreen(gci.GetActiveOutputBuffer(), Viewport::FromInclusive(NewRegion)); + } +} + +void ConversionAreaInfo::Paint() const noexcept +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + const auto viewport = ScreenInfo.GetViewport(); + + SMALL_RECT WriteRegion; + WriteRegion.Left = viewport.Left() + _caInfo.coordConView.X + _caInfo.rcViewCaWindow.Left; + WriteRegion.Right = WriteRegion.Left + (_caInfo.rcViewCaWindow.Right - _caInfo.rcViewCaWindow.Left); + WriteRegion.Top = viewport.Top() + _caInfo.coordConView.Y + _caInfo.rcViewCaWindow.Top; + WriteRegion.Bottom = WriteRegion.Top + (_caInfo.rcViewCaWindow.Bottom - _caInfo.rcViewCaWindow.Top); + + if (!IsHidden()) + { + WriteConvRegionToScreen(ScreenInfo, Viewport::FromInclusive(WriteRegion)); + } + else + { + WriteToScreen(ScreenInfo, Viewport::FromInclusive(WriteRegion)); + } +} diff --git a/src/host/conareainfo.h b/src/host/conareainfo.h new file mode 100644 index 000000000..8964faaa2 --- /dev/null +++ b/src/host/conareainfo.h @@ -0,0 +1,75 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conareainfo.h + +Abstract: +- This module contains the structures for the console IME conversion area +- The conversion area is the overlay on the screen where a user attempts to form + a string that they would like to insert into the buffer. + +Author: +- Michael Niksa (MiNiksa) 10-May-2018 + +Revision History: +- From pieces of convarea.cpp originally authored by KazuM +--*/ + +#pragma once + +#include "../buffer/out/OutputCell.hpp" +#include "../buffer/out/TextAttribute.hpp" +#include "../renderer/inc/FontInfo.hpp" + +class SCREEN_INFORMATION; +class TextBuffer; + +// Internal structures and definitions used by the conversion area. +class ConversionAreaBufferInfo final +{ +public: + COORD coordCaBuffer; + SMALL_RECT rcViewCaWindow; + COORD coordConView; + + ConversionAreaBufferInfo(const COORD coordBufferSize); +}; + +class ConversionAreaInfo final +{ +public: + ConversionAreaInfo(const COORD bufferSize, + const COORD windowSize, + const CHAR_INFO fill, + const CHAR_INFO popupFill, + const FontInfo fontInfo); + ~ConversionAreaInfo() = default; + ConversionAreaInfo(const ConversionAreaInfo&) = delete; + ConversionAreaInfo(ConversionAreaInfo&& other); + ConversionAreaInfo& operator=(const ConversionAreaInfo&) & = delete; + ConversionAreaInfo& operator=(ConversionAreaInfo&&) & = delete; + + bool IsHidden() const noexcept; + void SetHidden(const bool fIsHidden) noexcept; + void ClearArea() noexcept; + + [[nodiscard]] + HRESULT Resize(const COORD newSize) noexcept; + + void SetViewPos(const COORD pos) noexcept; + void SetWindowInfo(const SMALL_RECT view) noexcept; + void Paint() const noexcept; + + void WriteText(const std::vector& text, const SHORT column); + void SetAttributes(const TextAttribute& attr); + + const TextBuffer& GetTextBuffer() const noexcept; + const ConversionAreaBufferInfo& GetAreaBufferInfo() const noexcept; + +private: + ConversionAreaBufferInfo _caInfo; + std::unique_ptr _screenBuffer; + bool _isHidden; +}; diff --git a/src/host/conattrs.cpp b/src/host/conattrs.cpp new file mode 100644 index 000000000..453fcb1c7 --- /dev/null +++ b/src/host/conattrs.cpp @@ -0,0 +1,225 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conattrs.cpp + +Abstract: +- Defines common operations on console attributes, especially in regards to + finding the nearest color from a color table. + +Author(s): +- Mike Griese (migrie) 01-Sept-2017 +--*/ + +#include "precomp.h" +#include "..\inc\conattrs.hpp" +#include + +struct _HSL +{ + double h, s, l; + + // constructs an HSL color from a RGB Color. + _HSL(const COLORREF rgb) + { + const double r = (double) GetRValue(rgb); + const double g = (double) GetGValue(rgb); + const double b = (double) GetBValue(rgb); + + const auto[min, max] = std::minmax({ r, g, b }); + + const auto diff = max - min; + const auto sum = max + min; + // Luminence + l = max / 255.0; + + // Saturation + s = (max == 0) ? 0 : diff / max; + + //Hue + double q = (diff == 0)? 0 : 60.0/diff; + if (max == r) + { + h = (g < b)? ((360.0 + q * (g - b))/360.0) : ((q * (g - b))/360.0); + } + else if (max == g) + { + h = (120.0 + q * (b - r))/360.0; + } + else if (max == b) + { + h = (240.0 + q * (r - g))/360.0; + } + else + { + h = 0; + } + } +}; + +//Routine Description: +// Finds the "distance" between a given HSL color and an RGB color, using the HSL color space. +// This function is designed such that the caller would convert one RGB color to HSL ahead of time, +// then compare many RGB colors to that first color. +//Arguments: +// - phslColorA - a pointer to the first color, as a HSL color. +// - rgbColorB - The second color to compare, in RGB color. +// Return value: +// The "distance" between the two. +static double _FindDifference(const _HSL* const phslColorA, const COLORREF rgbColorB) +{ + const _HSL hslColorB = _HSL(rgbColorB); + return sqrt( pow((hslColorB.h - phslColorA->h), 2) + + pow((hslColorB.s - phslColorA->s), 2) + + pow((hslColorB.l - phslColorA->l), 2) ); +} + +//Routine Description: +// For a given RGB color Color, finds the nearest color from the array ColorTable, and returns the index of that match. +//Arguments: +// - Color - The RGB color to fine the nearest color to. +// - ColorTable - The array of colors to find a nearest color from. +// - cColorTable - The number of elements in ColorTable +// Return value: +// The index in ColorTable of the nearest match to Color. +WORD FindNearestTableIndex(const COLORREF Color, _In_reads_(cColorTable) const COLORREF* const ColorTable, const WORD cColorTable) +{ + // Quick check for an exact match in the color table: + for (WORD i = 0; i < cColorTable; i++) + { + if (Color == ColorTable[i]) + { + return i; + } + } + + // Did not find an exact match - do an expensive comparison to the elements + // of the table to find the nearest color. + const _HSL hslColor = _HSL(Color); + WORD closest = 0; + double minDiff = _FindDifference(&hslColor, ColorTable[0]); + for (WORD i = 1; i < cColorTable; i++) + { + double diff = _FindDifference(&hslColor, ColorTable[i]); + if (diff < minDiff) + { + minDiff = diff; + closest = i; + } + } + return closest; +} + +// Function Description: +// - Converts the value of a xterm color table index to the windows color table equivalent. +// Arguments: +// - xtermTableEntry: the xterm color table index +// Return Value: +// - The windows color table equivalent. +WORD XtermToWindowsIndex(const size_t xtermTableEntry) noexcept +{ + const bool fRed = WI_IsFlagSet(xtermTableEntry, XTERM_RED_ATTR); + const bool fGreen = WI_IsFlagSet(xtermTableEntry, XTERM_GREEN_ATTR); + const bool fBlue = WI_IsFlagSet(xtermTableEntry, XTERM_BLUE_ATTR); + const bool fBright = WI_IsFlagSet(xtermTableEntry, XTERM_BRIGHT_ATTR); + + return (fRed ? WINDOWS_RED_ATTR : 0x0) + + (fGreen ? WINDOWS_GREEN_ATTR : 0x0) + + (fBlue ? WINDOWS_BLUE_ATTR : 0x0) + + (fBright ? WINDOWS_BRIGHT_ATTR : 0x0); +} + +// Function Description: +// - Converts the value of a xterm color table index to the windows color table +// equivalent. The range of values is [0, 255], where the lowest 16 are +// mapped to the equivalent Windows index, and the rest of the values are +// passed through. +// Arguments: +// - xtermTableEntry: the xterm color table index +// Return Value: +// - The windows color table equivalent. +WORD Xterm256ToWindowsIndex(const size_t xtermTableEntry) noexcept +{ + return xtermTableEntry < 16 ? XtermToWindowsIndex(xtermTableEntry) : + static_cast(xtermTableEntry); +} + +// Function Description: +// - Converts the value of a pair of xterm color table indicies to the legacy attr equivalent. +// Arguments: +// - xtermForeground: the xterm color table foreground index +// - xtermBackground: the xterm color table background index +// Return Value: +// - The legacy windows attribute equivalent. +WORD XtermToLegacy(const size_t xtermForeground, const size_t xtermBackground) +{ + const WORD fgAttr = XtermToWindowsIndex(xtermForeground); + const WORD bgAttr = XtermToWindowsIndex(xtermBackground); + + return (bgAttr << 4) | fgAttr; +} + +//Routine Description: +// Returns the exact entry from the color table, if it's in there. +//Arguments: +// - Color - The RGB color to fine the nearest color to. +// - ColorTable - The array of colors to find a nearest color from. +// - cColorTable - The number of elements in ColorTable +// Return value: +// The index in ColorTable of the nearest match to Color. +bool FindTableIndex(const COLORREF Color, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable, + _Out_ WORD* const pFoundIndex) +{ + *pFoundIndex = 0; + for (WORD i = 0; i < cColorTable; i++) + { + if (ColorTable[i] == Color) + { + *pFoundIndex = i; + return true; + } + } + return false; +} + +// Method Description: +// - Get a COLORREF for the foreground component of the given legacy attributes. +// Arguments: +// - wLegacyAttrs - The legacy attributes to get the foreground color from. +// - ColorTable - The array of colors to to get the color from. +// - cColorTable - The number of elements in ColorTable +// Return Value: +// - the COLORREF for the foreground component +COLORREF ForegroundColor(const WORD wLegacyAttrs, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const size_t cColorTable) +{ + const byte iColorTableIndex = LOBYTE(wLegacyAttrs) & FG_ATTRS; + + return (iColorTableIndex < cColorTable && iColorTableIndex >= 0) ? + ColorTable[iColorTableIndex] : + INVALID_COLOR; +} + +// Method Description: +// - Get a COLORREF for the background component of the given legacy attributes. +// Arguments: +// - wLegacyAttrs - The legacy attributes to get the background color from. +// - ColorTable - The array of colors to to get the color from. +// - cColorTable - The number of elements in ColorTable +// Return Value: +// - the COLORREF for the background component +COLORREF BackgroundColor(const WORD wLegacyAttrs, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const size_t cColorTable) +{ + const byte iColorTableIndex = (LOBYTE(wLegacyAttrs) & BG_ATTRS) >> 4; + + return (iColorTableIndex < cColorTable && iColorTableIndex >= 0) ? + ColorTable[iColorTableIndex] : + INVALID_COLOR; +} diff --git a/src/host/conddkrefs.h b/src/host/conddkrefs.h new file mode 100644 index 000000000..e5978a46e --- /dev/null +++ b/src/host/conddkrefs.h @@ -0,0 +1,236 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conddkrefs.h + +Abstract: +- Contains headers that are a part of the public DDK. +- We don't include both the DDK and the SDK at the same time because they mesh poorly +and it's easier to include a copy of the infrequently changing defs here. +--*/ + +#pragma once + +#ifndef _DDK_INCLUDED + +#pragma region wdm.h (public DDK) +// +// Define the base asynchronous I/O argument types +// +extern "C" +{ + + // + // ClientId + // + + typedef struct _CLIENT_ID { + HANDLE UniqueProcess; + HANDLE UniqueThread; + } CLIENT_ID; + typedef CLIENT_ID *PCLIENT_ID; + + // POBJECT_ATTRIBUTES + + // + // Unicode strings are counted 16-bit character strings. If they are + // NULL terminated, Length does not include trailing NULL. + // + + typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; +#ifdef MIDL_PASS + [size_is(MaximumLength / 2), length_is((Length) / 2)] USHORT * Buffer; +#else // MIDL_PASS + _Field_size_bytes_part_(MaximumLength, Length) PWCH Buffer; +#endif // MIDL_PASS + } UNICODE_STRING; + typedef UNICODE_STRING *PUNICODE_STRING; + typedef const UNICODE_STRING *PCUNICODE_STRING; + + // OBJECT_ATTRIBUTES + +#define OBJ_INHERIT 0x00000002L +#define OBJ_PERMANENT 0x00000010L +#define OBJ_EXCLUSIVE 0x00000020L +#define OBJ_CASE_INSENSITIVE 0x00000040L +#define OBJ_OPENIF 0x00000080L +#define OBJ_OPENLINK 0x00000100L +#define OBJ_KERNEL_HANDLE 0x00000200L +#define OBJ_FORCE_ACCESS_CHECK 0x00000400L +#define OBJ_VALID_ATTRIBUTES 0x000007F2L + + typedef struct _OBJECT_ATTRIBUTES { + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; // Points to type SECURITY_DESCRIPTOR + PVOID SecurityQualityOfService; // Points to type SECURITY_QUALITY_OF_SERVICE + } OBJECT_ATTRIBUTES; + typedef OBJECT_ATTRIBUTES *POBJECT_ATTRIBUTES; + typedef CONST OBJECT_ATTRIBUTES *PCOBJECT_ATTRIBUTES; + + //++ + // + // VOID + // InitializeObjectAttributes( + // _Out_ POBJECT_ATTRIBUTES p, + // _In_ PUNICODE_STRING n, + // _In_ ULONG a, + // _In_ HANDLE r, + // _In_ PSECURITY_DESCRIPTOR s + // ) + // + //-- + +#define InitializeObjectAttributes( p, n, a, r, s ) { \ + (p)->Length = sizeof( OBJECT_ATTRIBUTES ); \ + (p)->RootDirectory = r; \ + (p)->Attributes = a; \ + (p)->ObjectName = n; \ + (p)->SecurityDescriptor = s; \ + (p)->SecurityQualityOfService = NULL; \ + } + + // UNICODE_STRING + + + + // OBJ_CASE_INSENSITIVE + // OBJ_INHERIT + // InitializeObjectAttributes + + typedef struct _IO_STATUS_BLOCK { + union { + NTSTATUS Status; + PVOID Pointer; + } DUMMYUNIONNAME; + + ULONG_PTR Information; + } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; + + // + // Define the file system information class values + // + // WARNING: The order of the following values are assumed by the I/O system. + // Any changes made here should be reflected there as well. + + typedef enum _FSINFOCLASS { + FileFsVolumeInformation = 1, + FileFsLabelInformation, // 2 + FileFsSizeInformation, // 3 + FileFsDeviceInformation, // 4 + FileFsAttributeInformation, // 5 + FileFsControlInformation, // 6 + FileFsFullSizeInformation, // 7 + FileFsObjectIdInformation, // 8 + FileFsDriverPathInformation, // 9 + FileFsVolumeFlagsInformation, // 10 + FileFsSectorSizeInformation, // 11 + FileFsDataCopyInformation, // 12 + FileFsMetadataSizeInformation, // 13 + FileFsMaximumInformation + } FS_INFORMATION_CLASS, *PFS_INFORMATION_CLASS; + +#ifndef DEVICE_TYPE +#define DEVICE_TYPE DWORD +#endif + + typedef struct _FILE_FS_DEVICE_INFORMATION { + DEVICE_TYPE DeviceType; + ULONG Characteristics; + } FILE_FS_DEVICE_INFORMATION, *PFILE_FS_DEVICE_INFORMATION; + +#pragma region IOCTL codes + // + // Define the various device type values. Note that values used by Microsoft + // Corporation are in the range 0-32767, and 32768-65535 are reserved for use + // by customers. + // + +#ifndef FILE_DEVICE_CONSOLE +#define FILE_DEVICE_CONSOLE 0x00000050 +#endif + + // + // Macro definition for defining IOCTL and FSCTL function control codes. Note + // that function codes 0-2047 are reserved for Microsoft Corporation, and + // 2048-4095 are reserved for customers. + // + +#ifndef CTL_CODE +#define CTL_CODE( DeviceType, Function, Method, Access ) ( \ + ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \ +) +#endif + + // + // Define the method codes for how buffers are passed for I/O and FS controls + // + +#define METHOD_BUFFERED 0 +#define METHOD_IN_DIRECT 1 +#define METHOD_OUT_DIRECT 2 + +#ifndef METHOD_NEITHER +#define METHOD_NEITHER 3 +#endif + + // + // Define some easier to comprehend aliases: + // METHOD_DIRECT_TO_HARDWARE (writes, aka METHOD_IN_DIRECT) + // METHOD_DIRECT_FROM_HARDWARE (reads, aka METHOD_OUT_DIRECT) + // + +#define METHOD_DIRECT_TO_HARDWARE METHOD_IN_DIRECT +#define METHOD_DIRECT_FROM_HARDWARE METHOD_OUT_DIRECT + + // + // Define the access check value for any access + // + // + // The FILE_READ_ACCESS and FILE_WRITE_ACCESS constants are also defined in + // ntioapi.h as FILE_READ_DATA and FILE_WRITE_DATA. The values for these + // constants *MUST* always be in sync. + // + // + // FILE_SPECIAL_ACCESS is checked by the NT I/O system the same as FILE_ANY_ACCESS. + // The file systems, however, may add additional access checks for I/O and FS controls + // that use this value. + // + + +#define FILE_ANY_ACCESS 0 +#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS) +#define FILE_READ_ACCESS ( 0x0001 ) // file & pipe +#define FILE_WRITE_ACCESS ( 0x0002 ) // file & pipe +#pragma endregion + +}; +#pragma endregion + +#pragma region ntifs.h (public DDK) + +extern "C" +{ +#define RtlOffsetToPointer(B,O) ((PCHAR)( ((PCHAR)(B)) + ((ULONG_PTR)(O)) )) + + __kernel_entry NTSYSCALLAPI + NTSTATUS + NTAPI + NtQueryVolumeInformationFile( + _In_ HANDLE FileHandle, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _Out_writes_bytes_(Length) PVOID FsInformation, + _In_ ULONG Length, + _In_ FS_INFORMATION_CLASS FsInformationClass + ); +}; + +#pragma endregion + +#endif // _DDK_INCLUDED diff --git a/src/host/conhost.rcv b/src/host/conhost.rcv new file mode 100644 index 000000000..3c7e0d6b9 --- /dev/null +++ b/src/host/conhost.rcv @@ -0,0 +1,5 @@ +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Window Host" +#define VER_INTERNALNAME_STR "ConHost" +#define VER_ORIGINALFILENAME_STR "CONHOST.EXE" diff --git a/src/host/conhostv2_traceviewpp.tvpp b/src/host/conhostv2_traceviewpp.tvpp new file mode 100644 index 000000000..cfcb88149 --- /dev/null +++ b/src/host/conhostv2_traceviewpp.tvpp @@ -0,0 +1,17 @@ + + + LocalLive + TVPP Session + + tl:{D58C1D55-51D2-4acf-A4BC-BA3899D8AA14} + + + + d58c1d55-51d2-4acf-a4bc-ba3899d8aa14 + 0 + 0 + + + + + diff --git a/src/host/conimeinfo.cpp b/src/host/conimeinfo.cpp new file mode 100644 index 000000000..02c46e5cf --- /dev/null +++ b/src/host/conimeinfo.cpp @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "conimeinfo.h" +#include "conareainfo.h" + +#include "_output.h" +#include "dbcs.h" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/GlyphWidth.hpp" +#include "../types/inc/Utf16Parser.hpp" + +// Attributes flags: +#define COMMON_LVB_GRID_SINGLEFLAG 0x2000 // DBCS: Grid attribute: use for ime cursor. + +ConsoleImeInfo::ConsoleImeInfo() : + _isSavedCursorVisible(false) +{ + +} + +// Routine Description: +// - Copies default attribute (color) data from the active screen buffer into the conversion area buffers +void ConsoleImeInfo::RefreshAreaAttributes() +{ + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto attributes = gci.GetActiveOutputBuffer().GetAttributes(); + + for (auto& area : ConvAreaCompStr) + { + area.SetAttributes(attributes); + } +} + +// Routine Description: +// - Takes the internally held composition message data from the last WriteCompMessage call +// and attempts to redraw it on the screen which will account for changes in viewport dimensions +void ConsoleImeInfo::RedrawCompMessage() +{ + if (!_text.empty()) + { + ClearAllAreas(); + _WriteUndeterminedChars(_text, _attributes, _colorArray); + } +} + +// Routine Description: +// - Writes an undetermined composition message to the screen including the text +// and color and cursor positioning attribute data so the user can walk through +// what they're proposing to insert into the buffer. +// Arguments: +// - text - The actual text of what the user would like to insert (UTF-16) +// - attributes - Encoded attributes including the cursor position and the color index (to the array) +// - colorArray - An array of colors to use for the text +void ConsoleImeInfo::WriteCompMessage(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray) +{ + // Backup the cursor visibility state and turn it off for drawing. + _SaveCursorVisibility(); + + ClearAllAreas(); + + // Save copies of the composition message in case we need to redraw it as things scroll/resize + _text = text; + _attributes = attributes; + _colorArray = colorArray; + + _WriteUndeterminedChars(text, attributes, colorArray); +} + +// Routine Description: +// - Writes the final result into the screen buffer through the input queue +// as if the user had inputted it (if their keyboard was able to) +// Arguments: +// - text - The actual text of what the user would like to insert (UTF-16) +void ConsoleImeInfo::WriteResultMessage(const std::wstring_view text) +{ + _RestoreCursorVisibility(); + + ClearAllAreas(); + + _InsertConvertedString(text); + + _ClearComposition(); +} + +// Routine Description: +// - Clears internally cached composition data from the last WriteCompMessage call. +void ConsoleImeInfo::_ClearComposition() +{ + _text.clear(); + _attributes.clear(); + _colorArray.clear(); +} + +// Routine Description: +// - Clears out all conversion areas +void ConsoleImeInfo::ClearAllAreas() +{ + for (auto& area : ConvAreaCompStr) + { + if (!area.IsHidden()) + { + area.ClearArea(); + } + } + + // Also clear internal buffer of string data. + _ClearComposition(); +} + +// Routine Description: +// - Resizes all conversion areas to the new dimensions +// Arguments: +// - newSize - New size for conversion areas +// Return Value: +// - S_OK or appropriate failure HRESULT. +[[nodiscard]] +HRESULT ConsoleImeInfo::ResizeAllAreas(const COORD newSize) +{ + for (auto& area : ConvAreaCompStr) + { + if (!area.IsHidden()) + { + area.SetHidden(true); + area.Paint(); + } + + RETURN_IF_FAILED(area.Resize(newSize)); + } + + return S_OK; +} + +// Routine Description: +// - Adds another conversion area to the current list of conversion areas (lines) available for IME candidate text +// Arguments: +// - +// Return Value: +// - Status successful or appropriate HRESULT response. +[[nodiscard]] +HRESULT ConsoleImeInfo::_AddConversionArea() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + COORD bufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); + bufferSize.Y = 1; + + const COORD windowSize = gci.GetActiveOutputBuffer().GetViewport().Dimensions(); + + CHAR_INFO fill; + fill.Attributes = gci.GetActiveOutputBuffer().GetAttributes().GetLegacyAttributes(); + + CHAR_INFO popupFill; + popupFill.Attributes = gci.GetActiveOutputBuffer().GetPopupAttributes()->GetLegacyAttributes(); + + const FontInfo& fontInfo = gci.GetActiveOutputBuffer().GetCurrentFont(); + + try + { + ConvAreaCompStr.emplace_back(bufferSize, + windowSize, + fill, + popupFill, + fontInfo); + } + CATCH_RETURN(); + + RefreshAreaAttributes(); + + return S_OK; +} + +// Routine Description: +// - Helper method to decode the cursor and color position out of the encoded attributes +// and color array and return it in the TextAttribute structure format +// Arguments: +// - pos - Character position in the string (and matching encoded attributes array) +// - attributes - Encoded attributes holding cursor and color array position +// - colorArray - Colors to choose from +// Return Value: +// - TextAttribute object with color and cursor and line drawing data. +TextAttribute ConsoleImeInfo::s_RetrieveAttributeAt(const size_t pos, + const std::basic_string_view attributes, + const std::basic_string_view colorArray) +{ + // Encoded attribute is the shorthand information passed from the IME + // that contains a cursor position packed in along with which color in the + // given array should apply to the text. + auto encodedAttribute = attributes[pos]; + + // Legacy attribute is in the color/line format that is understood for drawing + // We use the lower 3 bits (0-7) from the encoded attribute as the array index to start + // creating our legacy attribute. + WORD legacyAttribute = colorArray[encodedAttribute & (CONIME_ATTRCOLOR_SIZE - 1)]; + + if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_RIGHT)) + { + WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG); + WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_RVERTICAL); + } + else if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_LEFT)) + { + WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG); + WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_LVERTICAL); + } + + return TextAttribute(legacyAttribute); +} + +// Routine Description: +// - Converts IME-formatted information into OutputCells to determine what can fit into each +// displayable cell inside the console output buffer. +// Arguments: +// - text - Text data provided by the IME +// - attributes - Encoded color and cursor position data provided by the IME +// - colorArray - Array of color values provided by the IME. +// Return Value: +// - Vector of OutputCells where each one represents one cell of the output buffer. +std::vector ConsoleImeInfo::s_ConvertToCells(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray) +{ + std::vector cells; + + // - Convert incoming wchar_t stream into UTF-16 units. + const auto glyphs = Utf16Parser::Parse(text); + + // - Walk through all of the grouped up text, match up the correct attribute to it, and make a new cell. + size_t attributesUsed = 0; + for (const auto& parsedGlyph : glyphs) + { + const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() }; + // Collect up attributes that apply to this glyph range. + auto drawingAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray); + attributesUsed++; + + // The IME gave us an attribute for every glyph position in a surrogate pair. + // But the only important information will be the cursor position. + // Check all additional attributes to see if the cursor resides on top of them. + for (size_t i = 1; i < glyph.size(); i++) + { + TextAttribute additionalAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray); + attributesUsed++; + if (additionalAttr.IsLeftVerticalDisplayed()) + { + drawingAttr.SetLeftVerticalDisplayed(true); + } + if (additionalAttr.IsRightVerticalDisplayed()) + { + drawingAttr.SetRightVerticalDisplayed(true); + } + } + + // We have to determine if the glyph range is 1 column or two. + // If it's full width, it's two, and we need to make sure we don't draw the cursor + // right down the middle of the character. + // Otherwise it's one column and we'll push it in with the default empty DbcsAttribute. + DbcsAttribute dbcsAttr; + if (IsGlyphFullWidth(glyph)) + { + auto leftHalfAttr = drawingAttr; + + // Don't draw lines in the middle of full width glyphs. + // If we need a right vertical, don't apply it to the left side of the character + if (leftHalfAttr.IsRightVerticalDisplayed()) + { + leftHalfAttr.SetRightVerticalDisplayed(false); + } + + dbcsAttr.SetLeading(); + cells.emplace_back(glyph, dbcsAttr, leftHalfAttr); + dbcsAttr.SetTrailing(); + + // If we need a left vertical, don't apply it to the right side of the character + if (drawingAttr.IsLeftVerticalDisplayed()) + { + drawingAttr.SetLeftVerticalDisplayed(false); + } + } + cells.emplace_back(glyph, dbcsAttr, drawingAttr); + } + + return cells; +} + +// Routine Description: +// - Walks through the cells given and attempts to fill a conversion area line with as much data as can fit. +// - Each conversion area represents one line of the display starting at the cursor position filling to the right edge +// of the display. +// - The first conversion area should be placed from the screen buffer's current cursor position to the right +// edge of the viewport. +// - All subsequent areas should use one entire line of the viewport. +// Arguments: +// - begin - Beginning position in OutputCells for iteration +// - end - Ending position in OutputCells for iteration +// - pos - Reference to the coordinate position in the viewport that this conversion area will occupy. +// - Updated to set up the next conversion area down a line (and to the left viewport edge) +// - view - The rectangle representing the viewable area of the screen right now to let us know how many cells can fit. +// - screenInfo - A reference to the screen information we will use for accessibility notifications +// Return Value: +// - Updated begin position for the next call. It will normally be >begin and <= end. +// However, if text couldn't fit in our line (full-width character starting at the very last cell) +// then we will give back the same begin and update the position for the next call to try again. +// If the viewport is deemed too small, we'll skip past it and advance begin past the entire full-width character. +std::vector::const_iterator ConsoleImeInfo::_WriteConversionArea(const std::vector::const_iterator begin, + const std::vector::const_iterator end, + COORD& pos, + const Microsoft::Console::Types::Viewport view, + SCREEN_INFORMATION& screenInfo) +{ + // The position in the viewport where we will start inserting cells for this conversion area + // NOTE: We might exit early if there's not enough space to fit here, so we take a copy of + // the original and increment it up front. + const auto insertionPos = pos; + + // Advance the cursor position to set up the next call for success (insert the next conversion area + // at the beginning of the following line) + pos.X = view.Left(); + pos.Y++; + + // The index of the last column in the viewport. (view is inclusive) + const auto finalViewColumn = view.RightInclusive(); + + // The maximum number of cells we can insert into a line. + const auto lineWidth = finalViewColumn - insertionPos.X + 1; // +1 because view was inclusive + + // The iterator to the beginning position to form our line + const auto lineBegin = begin; + + // The total number of cells we could insert. + const auto size = end - begin; + FAIL_FAST_IF(size <= 0); // It's a programming error to have <= 0 cells to insert. + + // The end is the smaller of the remaining number of cells or the amount of line cells we can write before + // hitting the right edge of the viewport + auto lineEnd = lineBegin + std::min(size, (ptrdiff_t)lineWidth); + + // We must attempt to compensate for ending on a leading byte. We can't split a full-width character across lines. + // As such, if the last item is a leading byte, back the end up by one. + FAIL_FAST_IF(lineEnd <= lineBegin); // We should have at least 1 space we can back up. + + // Get the last cell in the run and if it's a leading byte, move the end position back one so we don't + // try to insert it. + const auto lastCell = lineEnd - 1; + if (lastCell->DbcsAttr().IsLeading()) + { + lineEnd--; + } + + // Copy out the substring into a vector. + const std::vector lineVec(lineBegin, lineEnd); + + // Add a conversion area to the internal state to hold this line. + THROW_IF_FAILED(_AddConversionArea()); + + // Get the added conversion area. + auto& area = ConvAreaCompStr.back(); + + // Write our text into the conversion area. + area.WriteText(lineVec, insertionPos.X); + + // Set the viewport and positioning parameters for the conversion area to describe to the renderer + // the appropriate location to overlay this conversion area on top of the main screen buffer inside the viewport. + const SMALL_RECT region{ insertionPos.X, 0, gsl::narrow(insertionPos.X + lineVec.size() - 1), 0 }; + area.SetWindowInfo(region); + area.SetViewPos({ 0 - view.Left(), insertionPos.Y - view.Top() }); + + // Make it visible and paint it. + area.SetHidden(false); + area.Paint(); + + // Notify accessibility that we have updated the text in this display region within the viewport. + screenInfo.NotifyAccessibilityEventing(insertionPos.X, insertionPos.Y, gsl::narrow(insertionPos.X + lineVec.size() - 1), insertionPos.Y); + + // Hand back the iterator representing the end of what we used to be fed into the beginning of the next call. + return lineEnd; +} + +// Routine Description: +// - Takes information from the IME message to write the "undetermined" text to the +// conversion area overlays on the screen. +// - The "undetermined" text represents the word or phrase that the user is currently building +// using the IME. They haven't "determined" what they want yet, so it's "undetermined" right now. +// Arguments: +// - text - View into the text characters provided by the IME. +// - attributes - Attributes specifying which color and cursor positioning information should apply to +// each text character. This view must be the same size as the text view. +// - colorArray - 8 colors to be used to format the text for display +void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + // Ensure cursor is visible for prompt line + screenInfo.MakeCurrentCursorVisible(); + + // Clear out existing conversion areas. + ConvAreaCompStr.clear(); + + // If the text length and attribute length don't match, + // it's a programming error on our part. We control the sizes here. + FAIL_FAST_IF(text.size() != attributes.size()); + + // If we have no text, return. We've already cleared above. + if (text.empty()) + { + return; + } + + // Convert data-to-be-stored into OutputCells. + const auto cells = s_ConvertToCells(text, attributes, colorArray); + + // Get some starting position information of where to place the conversion areas on top of the existing + // screen buffer and viewport positioning. + // Each conversion area write will adjust these to set up any subsequent calls to go onto the next line. + auto pos = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + const auto view = screenInfo.GetViewport(); + // Set cursor position relative to viewport + + // Set up our iterators. We will walk through the entire set of cells from beginning to end. + // The first time, we will give the iterators as the whole span and the begin + // will be moved forward by the conversion area write to set up the next call. + auto begin = cells.cbegin(); + const auto end = cells.cend(); + + // Write over and over updating the beginning iterator until we reach the end. + do + { + begin = _WriteConversionArea(begin, end, pos, view, screenInfo); + } while (begin < end); +} + +// Routine Description: +// - Takes the final text string and injects it into the input buffer +// Arguments: +// - text - The text to inject into the input buffer +void ConsoleImeInfo::_InsertConvertedString(const std::wstring_view text) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + auto& screenInfo = gci.GetActiveOutputBuffer(); + if (screenInfo.GetTextBuffer().GetCursor().IsOn()) + { + gci.GetCursorBlinker().TimerRoutine(screenInfo); + } + + const DWORD dwControlKeyState = GetControlKeyState(0); + std::deque> inEvents; + KeyEvent keyEvent{ TRUE, // keydown + 1, // repeatCount + 0, // virtualKeyCode + 0, // virtualScanCode + 0, // charData + dwControlKeyState }; // activeModifierKeys + + for (const auto& ch : text) + { + keyEvent.SetCharData(ch); + inEvents.push_back(std::make_unique(keyEvent)); + } + + gci.pInputBuffer->Write(inEvents); +} + +// Routine Description: +// - Backs up the global cursor visibility state if it is shown and disables +// it while we work on the conversion areas. +void ConsoleImeInfo::_SaveCursorVisibility() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + + // Cursor turn OFF. + if (cursor.IsVisible()) + { + _isSavedCursorVisible = true; + + cursor.SetIsVisible(false); + } +} + +// Routine Description: +// - Restores the global cursor visibility state if it was on when it was backed up. +void ConsoleImeInfo::_RestoreCursorVisibility() +{ + if (_isSavedCursorVisible) + { + _isSavedCursorVisible = false; + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + + cursor.SetIsVisible(true); + } +} diff --git a/src/host/conimeinfo.h b/src/host/conimeinfo.h new file mode 100644 index 000000000..1959bd96f --- /dev/null +++ b/src/host/conimeinfo.h @@ -0,0 +1,92 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conimeinfo.h + +Abstract: +- This module contains the structures for the console IME entrypoints + for overall control + +Author: +- Michael Niksa (MiNiksa) 10-May-2018 + +Revision History: +- From pieces of convarea.cpp originally authored by KazuM +--*/ + +#pragma once + +#include "../inc/conime.h" +#include "../buffer/out/OutputCell.hpp" +#include "../buffer/out/TextAttribute.hpp" +#include "../renderer/inc/FontInfo.hpp" +#include "../types/inc/viewport.hpp" + +#include "conareainfo.h" + +class SCREEN_INFORMATION; + +class ConsoleImeInfo final +{ +public: + // IME compositon string information + // There is one "composition string" per line that must be rendered on the screen + std::vector ConvAreaCompStr; + + ConsoleImeInfo(); + ~ConsoleImeInfo() = default; + ConsoleImeInfo(const ConsoleImeInfo&) = delete; + ConsoleImeInfo(ConsoleImeInfo&&) = delete; + ConsoleImeInfo& operator=(const ConsoleImeInfo&) & = delete; + ConsoleImeInfo& operator=(ConsoleImeInfo&&) & = delete; + + void RefreshAreaAttributes(); + void ClearAllAreas(); + + [[nodiscard]] + HRESULT ResizeAllAreas(const COORD newSize); + + void WriteCompMessage(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray); + + void WriteResultMessage(const std::wstring_view text); + + void RedrawCompMessage(); + +private: + [[nodiscard]] + HRESULT _AddConversionArea(); + + void _ClearComposition(); + + void _WriteUndeterminedChars(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray); + + void _InsertConvertedString(const std::wstring_view text); + + static TextAttribute s_RetrieveAttributeAt(const size_t pos, + const std::basic_string_view attributes, + const std::basic_string_view colorArray); + + static std::vector s_ConvertToCells(const std::wstring_view text, + const std::basic_string_view attributes, + const std::basic_string_view colorArray); + + std::vector::const_iterator _WriteConversionArea(const std::vector::const_iterator begin, + const std::vector::const_iterator end, + COORD& pos, + const Microsoft::Console::Types::Viewport view, + SCREEN_INFORMATION& screenInfo); + + void _SaveCursorVisibility(); + void _RestoreCursorVisibility(); + bool _isSavedCursorVisible; + + std::wstring _text; + std::basic_string _attributes; + std::basic_string _colorArray; +}; diff --git a/src/host/conserv.h b/src/host/conserv.h new file mode 100644 index 000000000..e5ddce7bf --- /dev/null +++ b/src/host/conserv.h @@ -0,0 +1,53 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conserv.h + +Abstract: +- This module contains the include files and definitions for the console server DLL. + +Author: +- Therese Stowell (ThereseS) 16-Nov-1990 + +Revision History: +- Many items removed into individual classes relevant to individual components (MiNiksa, PaulCam - 2014) +- Renamed from consrv.h due to naming conflict with the one published from minkernel. +--*/ + +#pragma once + +#include "cmdline.h" +#include "globals.h" +#include "server.h" +#include "settings.hpp" +#include "tracing.hpp" + +#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) +#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); + +/* + * Used to store some console attributes for the console. This is a means + * to cache the color in the extra-window-bytes, so USER/KERNEL can get + * at it for hungapp drawing. The window-offsets are defined in NTUSER\INC. + * + * The other macros are just convenient means for setting the other window + * bytes. + */ + +#define PACKCOORD(pt) (MAKELONG(((pt).X), ((pt).Y))) + +typedef struct _CONSOLE_API_CONNECTINFO +{ + Settings ConsoleInfo; + BOOLEAN ConsoleApp; + BOOLEAN WindowVisible; + DWORD ProcessGroupId; + DWORD TitleLength; + WCHAR Title[MAX_PATH + 1]; + DWORD AppNameLength; + WCHAR AppName[128]; + DWORD CurDirLength; + WCHAR CurDir[MAX_PATH + 1]; +} CONSOLE_API_CONNECTINFO, *PCONSOLE_API_CONNECTINFO; diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp new file mode 100644 index 000000000..065a90b5c --- /dev/null +++ b/src/host/consoleInformation.cpp @@ -0,0 +1,388 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include + +#include "misc.h" +#include "output.h" +#include "srvinit.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "..\types\inc\convert.hpp" + +CONSOLE_INFORMATION::CONSOLE_INFORMATION() : + // ProcessHandleList initializes itself + pInputBuffer(nullptr), + pCurrentScreenBuffer(nullptr), + ScreenBuffers(nullptr), + OutputQueue(), + // ExeAliasList initialized below + _OriginalTitle(), + _Title(), + _LinkTitle(), + Flags(0), + PopupCount(0), + CP(0), + OutputCP(0), + CtrlFlags(0), + LimitingProcessId(0), + // ColorTable initialized below + // CPInfo initialized below + // OutputCPInfo initialized below + _cookedReadData(nullptr), + ConsoleIme{}, + terminalMouseInput(HandleTerminalKeyEventCallback), + _vtIo(), + _blinker{}, + renderData{} +{ + ZeroMemory((void*)&CPInfo, sizeof(CPInfo)); + ZeroMemory((void*)&OutputCPInfo, sizeof(OutputCPInfo)); + InitializeCriticalSection(&_csConsoleLock); +} + +CONSOLE_INFORMATION::~CONSOLE_INFORMATION() +{ + DeleteCriticalSection(&_csConsoleLock); +} + +bool CONSOLE_INFORMATION::IsConsoleLocked() const +{ + // The critical section structure's OwningThread field contains the ThreadId despite having the HANDLE type. + // This requires us to hard cast the ID to compare. + return _csConsoleLock.OwningThread == (HANDLE)GetCurrentThreadId(); +} + +#pragma prefast(suppress:26135, "Adding lock annotation spills into entire project. Future work.") +void CONSOLE_INFORMATION::LockConsole() +{ + EnterCriticalSection(&_csConsoleLock); +} + +#pragma prefast(suppress:26135, "Adding lock annotation spills into entire project. Future work.") +bool CONSOLE_INFORMATION::TryLockConsole() +{ + return !!TryEnterCriticalSection(&_csConsoleLock); +} + +#pragma prefast(suppress:26135, "Adding lock annotation spills into entire project. Future work.") +void CONSOLE_INFORMATION::UnlockConsole() +{ + LeaveCriticalSection(&_csConsoleLock); +} + +ULONG CONSOLE_INFORMATION::GetCSRecursionCount() +{ + return _csConsoleLock.RecursionCount; +} + +// Routine Description: +// - This routine allocates and initialized a console and its associated +// data - input buffer and screen buffer. +// - NOTE: Will read global ServiceLocator::LocateGlobals().getConsoleInformation expecting Settings to already be filled. +// Arguments: +// - title - Window Title to display +// Return Value: +// - STATUS_SUCCESS if successful. +[[nodiscard]] +NTSTATUS CONSOLE_INFORMATION::AllocateConsole(const std::wstring_view title) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Synchronize flags + WI_SetFlagIf(gci.Flags, CONSOLE_AUTO_POSITION, !!gci.GetAutoPosition()); + WI_SetFlagIf(gci.Flags, CONSOLE_QUICK_EDIT_MODE, !!gci.GetQuickEdit()); + WI_SetFlagIf(gci.Flags, CONSOLE_HISTORY_NODUP, !!gci.GetHistoryNoDup()); + + Selection* const pSelection = &Selection::Instance(); + pSelection->SetLineSelection(!!gci.GetLineSelection()); + + SetConsoleCPInfo(TRUE); + SetConsoleCPInfo(FALSE); + + // Initialize input buffer. + try + { + gci.pInputBuffer = new InputBuffer(); + } + catch(...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + try + { + gci.SetTitle(title); + gci.SetOriginalTitle(std::wstring(TranslateConsoleTitle(gci.GetTitle().c_str(), TRUE, FALSE))); + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + NTSTATUS Status = DoCreateScreenBuffer(); + if (!NT_SUCCESS(Status)) + { + goto ErrorExit2; + } + + gci.pCurrentScreenBuffer = gci.ScreenBuffers; + + gci.GetActiveOutputBuffer().ScrollScale = gci.GetScrollScale(); + + gci.ConsoleIme.RefreshAreaAttributes(); + + if (NT_SUCCESS(Status)) + { + return STATUS_SUCCESS; + } + + RIPMSG1(RIP_WARNING, "Console init failed with status 0x%x", Status); + + delete gci.ScreenBuffers; + gci.ScreenBuffers = nullptr; + +ErrorExit2: + delete gci.pInputBuffer; + + return Status; +} + +VtIo* CONSOLE_INFORMATION::GetVtIo() +{ + return &_vtIo; +} + +bool CONSOLE_INFORMATION::IsInVtIoMode() const +{ + return _vtIo.IsUsingVt(); +} + +bool CONSOLE_INFORMATION::HasPendingCookedRead() const noexcept +{ + return _cookedReadData != nullptr; +} + +const COOKED_READ_DATA& CONSOLE_INFORMATION::CookedReadData() const noexcept +{ + return *_cookedReadData; +} + +COOKED_READ_DATA& CONSOLE_INFORMATION::CookedReadData() noexcept +{ + return *_cookedReadData; +} + +void CONSOLE_INFORMATION::SetCookedReadData(COOKED_READ_DATA* readData) noexcept +{ + _cookedReadData = readData; +} + +// Routine Description: +// - Handler for inserting key sequences into the buffer when the terminal emulation layer +// has determined a key can be converted appropriately into a sequence of inputs +// Arguments: +// - events - the input events to write to the input buffer +// Return Value: +// - +void CONSOLE_INFORMATION::HandleTerminalKeyEventCallback(_Inout_ std::deque>& events) +{ + ServiceLocator::LocateGlobals().getConsoleInformation().pInputBuffer->Write(events); +} + +// Method Description: +// - Return the active screen buffer of the console. +// Arguments: +// - +// Return Value: +// - the active screen buffer of the console. +SCREEN_INFORMATION& CONSOLE_INFORMATION::GetActiveOutputBuffer() +{ + return *pCurrentScreenBuffer; +} + +const SCREEN_INFORMATION& CONSOLE_INFORMATION::GetActiveOutputBuffer() const +{ + return *pCurrentScreenBuffer; +} + +bool CONSOLE_INFORMATION::HasActiveOutputBuffer() const +{ + return (pCurrentScreenBuffer != nullptr); +} + +// Method Description: +// - Return the active input buffer of the console. +// Arguments: +// - +// Return Value: +// - the active input buffer of the console. +InputBuffer* const CONSOLE_INFORMATION::GetActiveInputBuffer() const +{ + return pInputBuffer; +} + +// Method Description: +// - Return the default foreground color of the console. If the settings are +// configured to have a default foreground color (separate from the color +// table), this will return that value. Otherwise it will return the value +// from the colortable corresponding to our default attributes. +// Arguments: +// - +// Return Value: +// - the default foreground color of the console. +COLORREF CONSOLE_INFORMATION::GetDefaultForeground() const noexcept +{ + return Settings::CalculateDefaultForeground(); +} + +// Method Description: +// - Return the default background color of the console. If the settings are +// configured to have a default background color (separate from the color +// table), this will return that value. Otherwise it will return the value +// from the colortable corresponding to our default attributes. +// Arguments: +// - +// Return Value: +// - the default background color of the console. +COLORREF CONSOLE_INFORMATION::GetDefaultBackground() const noexcept +{ + return Settings::CalculateDefaultBackground(); +} + +// Method Description: +// - Set the console's title, and trigger a renderer update of the title. +// This does not include the title prefix, such as "Mark", "Select", or "Scroll" +// Arguments: +// - newTitle: The new value to use for the title +// Return Value: +// - +void CONSOLE_INFORMATION::SetTitle(const std::wstring_view newTitle) +{ + _Title = std::wstring{ newTitle.begin(), newTitle.end() }; + + auto* const pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->TriggerTitleChange(); + } +} + +// Method Description: +// - Set the console title's prefix, and trigger a renderer update of the title. +// This is the part of the title shuch as "Mark", "Select", or "Scroll" +// Arguments: +// - newTitlePrefix: The new value to use for the title prefix +// Return Value: +// - +void CONSOLE_INFORMATION::SetTitlePrefix(const std::wstring& newTitlePrefix) +{ + _TitlePrefix = newTitlePrefix; + + auto* const pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->TriggerTitleChange(); + } +} + +// Method Description: +// - Set the value of the console's original title. This is the title the +// console launched with. +// Arguments: +// - originalTitle: The new value to use for the console's original title +// Return Value: +// - +void CONSOLE_INFORMATION::SetOriginalTitle(const std::wstring& originalTitle) +{ + _OriginalTitle = originalTitle; +} + +// Method Description: +// - Set the value of the console's link title. If the console was launched +/// from a shortcut, this value will not be the empty string. +// Arguments: +// - linkTitle: The new value to use for the console's link title +// Return Value: +// - +void CONSOLE_INFORMATION::SetLinkTitle(const std::wstring& linkTitle) +{ + _LinkTitle = linkTitle; +} + +// Method Description: +// - return a reference to the console's title. +// Arguments: +// - +// Return Value: +// - a reference to the console's title. +const std::wstring& CONSOLE_INFORMATION::GetTitle() const noexcept +{ + return _Title; +} + +// Method Description: +// - Return a new wstring representing the actual display value of the title. +// This is the Prefix+Title. +// Arguments: +// - +// Return Value: +// - a new wstring containing the combined prefix and title. +const std::wstring CONSOLE_INFORMATION::GetTitleAndPrefix() const +{ + return _TitlePrefix + _Title; +} + +// Method Description: +// - return a reference to the console's original title. +// Arguments: +// - +// Return Value: +// - a reference to the console's original title. +const std::wstring& CONSOLE_INFORMATION::GetOriginalTitle() const noexcept +{ + return _OriginalTitle; +} + +// Method Description: +// - return a reference to the console's link title. +// Arguments: +// - +// Return Value: +// - a reference to the console's link title. +const std::wstring& CONSOLE_INFORMATION::GetLinkTitle() const noexcept +{ + return _LinkTitle; +} + +// Method Description: +// - return a reference to the console's cursor blinker. +// Arguments: +// - +// Return Value: +// - a reference to the console's cursor blinker. +Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexcept +{ + return _blinker; +} + +// Method Description: +// - Generates a CHAR_INFO for this output cell, using our +// GenerateLegacyAttributes method to generate the legacy style attributes. +// Arguments: +// - cell: The cell to get the CHAR_INFO from +// Return Value: +// - a CHAR_INFO containing legacy information about the cell +CHAR_INFO CONSOLE_INFORMATION::AsCharInfo(const OutputCellView& cell) const noexcept +{ + CHAR_INFO ci { 0 }; + ci.Char.UnicodeChar = Utf16ToUcs2(cell.Chars()); + + // If the current text attributes aren't legacy attributes, then + // use gci to look up the correct legacy attributes to use + // (for mapping RGB values to the nearest table value) + const auto& attr = cell.TextAttr(); + ci.Attributes = GenerateLegacyAttributes(attr);; + ci.Attributes |= cell.DbcsAttr().GeneratePublicApiAttributeFormat(); + return ci; +} diff --git a/src/host/conv.h b/src/host/conv.h new file mode 100644 index 000000000..e72e3724d --- /dev/null +++ b/src/host/conv.h @@ -0,0 +1,31 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conv.h + +Abstract: +- This module contains the internal structures and definitions used by the conversion area. +- "Conversion area" refers to either the in-line area where the text color changes and suggests options in IME-based languages + or to the reserved line at the bottom of the screen offering suggestions and the current IME mode. + +Author: +- KazuM March 8, 1993 + +Revision History: +--*/ + +#pragma once + +#include "server.h" + +#include "../types/inc/Viewport.hpp" + +void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, + const Microsoft::Console::Types::Viewport& convRegion); + +[[nodiscard]] +HRESULT ConsoleImeResizeCompStrView(); +[[nodiscard]] +HRESULT ConsoleImeResizeCompStrScreenBuffer(const COORD coordNewScreenSize); diff --git a/src/host/convarea.cpp b/src/host/convarea.cpp new file mode 100644 index 000000000..9b3c5b658 --- /dev/null +++ b/src/host/convarea.cpp @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "_output.h" + +#include "../interactivity/inc/ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +bool IsValidSmallRect(_In_ PSMALL_RECT const Rect) +{ + return (Rect->Right >= Rect->Left && Rect->Bottom >= Rect->Top); +} + +void WriteConvRegionToScreen(const SCREEN_INFORMATION& ScreenInfo, + const Viewport& convRegion) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (!ScreenInfo.IsActiveScreenBuffer()) + { + return; + } + + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + + for (unsigned int i = 0; i < pIme->ConvAreaCompStr.size(); ++i) + { + const auto& ConvAreaInfo = pIme->ConvAreaCompStr[i]; + + if (!ConvAreaInfo.IsHidden()) + { + const auto currentViewport = ScreenInfo.GetViewport().ToInclusive(); + const auto areaInfo = ConvAreaInfo.GetAreaBufferInfo(); + + // Do clipping region + SMALL_RECT Region; + Region.Left = currentViewport.Left + areaInfo.rcViewCaWindow.Left + areaInfo.coordConView.X; + Region.Right = Region.Left + (areaInfo.rcViewCaWindow.Right - areaInfo.rcViewCaWindow.Left); + Region.Top = currentViewport.Top + areaInfo.rcViewCaWindow.Top + areaInfo.coordConView.Y; + Region.Bottom = Region.Top + (areaInfo.rcViewCaWindow.Bottom - areaInfo.rcViewCaWindow.Top); + + SMALL_RECT ClippedRegion; + ClippedRegion.Left = std::max(Region.Left, currentViewport.Left); + ClippedRegion.Top = std::max(Region.Top, currentViewport.Top); + ClippedRegion.Right = std::min(Region.Right, currentViewport.Right); + ClippedRegion.Bottom = std::min(Region.Bottom, currentViewport.Bottom); + + if (IsValidSmallRect(&ClippedRegion)) + { + Region = ClippedRegion; + ClippedRegion.Left = std::max(Region.Left, convRegion.Left()); + ClippedRegion.Top = std::max(Region.Top, convRegion.Top()); + ClippedRegion.Right = std::min(Region.Right, convRegion.RightInclusive()); + ClippedRegion.Bottom = std::min(Region.Bottom, convRegion.BottomInclusive()); + if (IsValidSmallRect(&ClippedRegion)) + { + // if we have a renderer, we need to update. + // we've already confirmed (above with an early return) that we're on conversion areas that are a part of the active (visible/rendered) screen + // so send invalidates to those regions such that we're queried for data on the next frame and repainted. + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + // convert inclusive rectangle to exclusive rectangle + SMALL_RECT srExclusive = ClippedRegion; + srExclusive.Right++; + srExclusive.Bottom++; + + ServiceLocator::LocateGlobals().pRender->TriggerRedraw(Viewport::FromExclusive(srExclusive)); + } + } + } + } + } +} + +[[nodiscard]] +HRESULT ConsoleImeResizeCompStrView() +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + pIme->RedrawCompMessage(); + } + CATCH_RETURN(); + + return S_OK; +} + +[[nodiscard]] +HRESULT ConsoleImeResizeCompStrScreenBuffer(const COORD coordNewScreenSize) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + + return pIme->ResizeAllAreas(coordNewScreenSize); +} + +[[nodiscard]] +HRESULT ImeStartComposition() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + gci.pInputBuffer->fInComposition = true; + return S_OK; +} + +[[nodiscard]] +HRESULT ImeEndComposition() +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + gci.pInputBuffer->fInComposition = false; + return S_OK; +} + +[[nodiscard]] +HRESULT ImeComposeData(std::wstring_view text, + std::basic_string_view attributes, + std::basic_string_view colorArray) +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + pIme->WriteCompMessage(text, attributes, colorArray); + } + CATCH_RETURN(); + return S_OK; +} + +[[nodiscard]] +HRESULT ImeClearComposeData() +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + pIme->ClearAllAreas(); + } + CATCH_RETURN(); + return S_OK; +} + +[[nodiscard]] +HRESULT ImeComposeResult(std::wstring_view text) +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + ConsoleImeInfo* const pIme = &gci.ConsoleIme; + pIme->WriteResultMessage(text); + } + CATCH_RETURN(); + return S_OK; +} diff --git a/src/host/conwinuserrefs.h b/src/host/conwinuserrefs.h new file mode 100644 index 000000000..130ae2440 --- /dev/null +++ b/src/host/conwinuserrefs.h @@ -0,0 +1,84 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conwinuserrefs.h + +Abstract: +- Contains private definitions from WinUserK.h that we'll need to publish. +--*/ + +#pragma once + +#pragma region WinUserK.h (private internal) + +extern "C" +{ + /* WinUserK */ + /* + * Console window startup optimization. + */ + + typedef enum _CONSOLECONTROL { + Reserved1, + ConsoleNotifyConsoleApplication, + Reserved2, + ConsoleSetCaretInfo, + Reserved3, + ConsoleSetForeground, + ConsoleSetWindowOwner, + ConsoleEndTask, + } CONSOLECONTROL; + + // + // CtrlFlags definitions + // +#define CONSOLE_CTRL_C_FLAG 0x00000001 +#define CONSOLE_CTRL_BREAK_FLAG 0x00000002 +#define CONSOLE_CTRL_CLOSE_FLAG 0x00000004 + +#define CONSOLE_CTRL_LOGOFF_FLAG 0x00000010 +#define CONSOLE_CTRL_SHUTDOWN_FLAG 0x00000020 + + typedef struct _CONSOLEENDTASK { + HANDLE ProcessId; + HWND hwnd; + ULONG ConsoleEventCode; + ULONG ConsoleFlags; + } CONSOLEENDTASK, *PCONSOLEENDTASK; + + typedef struct _CONSOLEWINDOWOWNER { + HWND hwnd; + ULONG ProcessId; + ULONG ThreadId; + } CONSOLEWINDOWOWNER, *PCONSOLEWINDOWOWNER; + + typedef struct _CONSOLESETFOREGROUND { + HANDLE hProcess; + BOOL bForeground; + } CONSOLESETFOREGROUND, *PCONSOLESETFOREGROUND; + + /* + * Console window startup optimization. + */ +#define CPI_NEWPROCESSWINDOW 0x0001 + + typedef struct _CONSOLE_PROCESS_INFO { + IN DWORD dwProcessID; + IN DWORD dwFlags; + } CONSOLE_PROCESS_INFO, *PCONSOLE_PROCESS_INFO; + + typedef struct _CONSOLE_CARET_INFO { + IN HWND hwnd; + IN RECT rc; + } CONSOLE_CARET_INFO, *PCONSOLE_CARET_INFO; + + NTSTATUS ConsoleControl( + __in CONSOLECONTROL Command, + __in_bcount_opt(ConsoleInformationLength) PVOID ConsoleInformation, + __in DWORD ConsoleInformationLength); + + /* END WinUserK */ +}; +#pragma endregion diff --git a/src/host/dbcs.cpp b/src/host/dbcs.cpp new file mode 100644 index 000000000..c64edaae3 --- /dev/null +++ b/src/host/dbcs.cpp @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "dbcs.h" +#include "misc.h" + +#include "../types/inc/convert.hpp" +#include "../types/inc/GlyphWidth.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" + +#pragma hdrstop + +// Routine Description: +// - This routine check bisected on Ascii string end. +// Arguments: +// - pchBuf - Pointer to Ascii string buffer. +// - cbBuf - Number of Ascii string. +// Return Value: +// - TRUE - Bisected character. +// - FALSE - Correctly. +bool CheckBisectStringA(_In_reads_bytes_(cbBuf) PCHAR pchBuf, _In_ DWORD cbBuf, const CPINFO * const pCPInfo) +{ + while (cbBuf) + { + if (IsDBCSLeadByteConsole(*pchBuf, pCPInfo)) + { + if (cbBuf <= 1) + { + return true; + } + else + { + pchBuf += 2; + cbBuf -= 2; + } + } + else + { + pchBuf++; + cbBuf--; + } + } + + return false; +} + +// Routine Description: +// - This routine removes the double copies of characters used when storing DBCS/Double-wide characters in the text buffer. +// - It munges up Unicode cells that are about to be returned whenever there is DBCS data and a raster font is enabled. +// - This function is ONLY FOR COMPATIBILITY PURPOSES. Please do not introduce new usages. +// Arguments: +// - buffer - The buffer to walk and fix +// Return Value: +// - The length of the final modified buffer. +DWORD UnicodeRasterFontCellMungeOnRead(const gsl::span buffer) +{ + // Walk through the source CHAR_INFO and copy each to the destination. + // EXCEPT for trailing bytes (this will de-duplicate the leading/trailing byte double copies of the CHAR_INFOs as stored in the buffer). + + // Set up indices used for arrays. + DWORD iDst = 0; + + // Walk through every CHAR_INFO + for (DWORD iSrc = 0; iSrc < gsl::narrow(buffer.size()); iSrc++) + { + // If it's not a trailing byte, copy it straight over, stripping out the Leading/Trailing flags from the attributes field. + if (!WI_IsFlagSet(buffer.at(iSrc).Attributes, COMMON_LVB_TRAILING_BYTE)) + { + buffer.at(iDst) = buffer.at(iSrc); + WI_ClearAllFlags(buffer.at(iDst).Attributes, COMMON_LVB_SBCSDBCS); + iDst++; + } + + // If it was a trailing byte, we'll just walk past it and keep going. + } + + // Zero out the remaining part of the destination buffer that we didn't use. + DWORD const cchDstToClear = gsl::narrow(buffer.size()) - iDst; + + if (cchDstToClear > 0) + { + CHAR_INFO* const pciDstClearStart = buffer.data() + iDst; + ZeroMemory(pciDstClearStart, cchDstToClear * sizeof(CHAR_INFO)); + } + + // Add the additional length we just modified. + iDst += cchDstToClear; + + // now that we're done, we should have copied, left alone, or cleared the entire length. + FAIL_FAST_IF(!(iDst == gsl::narrow(buffer.size()))); + + return iDst; +} + +// Routine Description: +// - Checks if a char is a lead byte for a given code page. +// Arguments: +// - ch - the char to check. +// - pCPInfo - the code page to check the char in. +// Return Value: +// true if ch is a lead byte, false otherwise. +bool IsDBCSLeadByteConsole(const CHAR ch, const CPINFO * const pCPInfo) +{ + FAIL_FAST_IF_NULL(pCPInfo); + // NOTE: This must be unsigned for the comparison. If we compare signed, this will never hit + // because lead bytes are ironically enough always above 0x80 (signed char negative range). + unsigned char const uchComparison = (unsigned char)ch; + + int i = 0; + // this is ok because the the array is guaranteed to have 2 + // null bytes at the end. + while (pCPInfo->LeadByte[i]) + { + if (pCPInfo->LeadByte[i] <= uchComparison && uchComparison <= pCPInfo->LeadByte[i + 1]) + { + return true; + } + i += 2; + } + return false; +} + +BYTE CodePageToCharSet(const UINT uiCodePage) +{ + CHARSETINFO csi; + + const auto inputServices = ServiceLocator::LocateInputServices(); + if (nullptr == inputServices || !inputServices->TranslateCharsetInfo((DWORD *) IntToPtr(uiCodePage), &csi, TCI_SRCCODEPAGE)) + { + csi.ciCharset = OEM_CHARSET; + } + + return (BYTE) csi.ciCharset; +} + +BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage) +{ + BYTE const CharSet = CodePageToCharSet(uiCodePage); + + switch (CharSet) + { + case SHIFTJIS_CHARSET: + case HANGEUL_CHARSET: + case CHINESEBIG5_CHARSET: + case GB2312_CHARSET: + return true; + default: + return false; + } +} + +_Ret_range_(0, cbAnsi) +ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode, + const ULONG cchUnicode, + _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi, + const ULONG cbAnsi, + _Out_ std::unique_ptr& partialEvent) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + PWCHAR const TmpUni = new(std::nothrow) WCHAR[cchUnicode]; + if (TmpUni == nullptr) + { + return 0; + } + + memcpy(TmpUni, pwchUnicode, cchUnicode* sizeof(WCHAR)); + + BYTE AsciiDbcs[2]; + AsciiDbcs[1] = 0; + + ULONG i, j; + for (i = 0, j = 0; i < cchUnicode && j < cbAnsi; i++, j++) + { + if (IsGlyphFullWidth(TmpUni[i])) + { + ULONG const NumBytes = sizeof(AsciiDbcs); + ConvertToOem(gci.CP, &TmpUni[i], 1, (LPSTR) & AsciiDbcs[0], NumBytes); + if (IsDBCSLeadByteConsole(AsciiDbcs[0], &gci.CPInfo)) + { + if (j < cbAnsi - 1) + { // -1 is safe DBCS in buffer + pchAnsi[j] = AsciiDbcs[0]; + j++; + pchAnsi[j] = AsciiDbcs[1]; + AsciiDbcs[1] = 0; + } + else + { + pchAnsi[j] = AsciiDbcs[0]; + break; + } + } + else + { + pchAnsi[j] = AsciiDbcs[0]; + AsciiDbcs[1] = 0; + } + } + else + { + ConvertToOem(gci.CP, &TmpUni[i], 1, &pchAnsi[j], 1); + } + } + + if (AsciiDbcs[1]) + { + try + { + std::unique_ptr keyEvent = std::make_unique(); + if (keyEvent.get()) + { + keyEvent->SetCharData(AsciiDbcs[1]); + partialEvent.reset(static_cast(keyEvent.release())); + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } + + delete[] TmpUni; + return j; +} diff --git a/src/host/dbcs.h b/src/host/dbcs.h new file mode 100644 index 000000000..7076506f4 --- /dev/null +++ b/src/host/dbcs.h @@ -0,0 +1,38 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- dbcs.h + +Abstract: +- Provides helpers to manage double-byte (double-width) characters for CJK languages within the text buffer +- Some items historically referred to as "FE" or "Far East" (geopol sensitive) and converted to "East Asian". + Refers to Chinese, Japanese, and Korean languages that require significantly different handling from legacy ASCII/Latin1 characters. + +Author: +- KazuM (suspected) + +Revision History: +--*/ + +#pragma once + +#include "screenInfo.hpp" + +bool CheckBisectStringA(_In_reads_bytes_(cbBuf) PCHAR pchBuf, _In_ DWORD cbBuf, const CPINFO * const pCPInfo); + +DWORD UnicodeRasterFontCellMungeOnRead(const gsl::span buffer); + +bool IsDBCSLeadByteConsole(const CHAR ch, const CPINFO * const pCPInfo); + +BYTE CodePageToCharSet(const UINT uiCodePage); + +BOOL IsAvailableEastAsianCodePage(const UINT uiCodePage); + +_Ret_range_(0, cbAnsi) +ULONG TranslateUnicodeToOem(_In_reads_(cchUnicode) PCWCHAR pwchUnicode, + const ULONG cchUnicode, + _Out_writes_bytes_(cbAnsi) PCHAR pchAnsi, + const ULONG cbAnsi, + _Out_ std::unique_ptr& partialEvent); diff --git a/src/host/directio.cpp b/src/host/directio.cpp new file mode 100644 index 000000000..567be1796 --- /dev/null +++ b/src/host/directio.cpp @@ -0,0 +1,1255 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "directio.h" + +#include "_output.h" +#include "output.h" +#include "input.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "readDataDirect.hpp" +#include "ApiRoutines.h" + +#include "../types/inc/convert.hpp" +#include "../types/inc/GlyphWidth.hpp" +#include "../types/inc/viewport.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +class CONSOLE_INFORMATION; + +#define UNICODE_DBCS_PADDING 0xffff + +// Routine Description: +// - converts non-unicode InputEvents to unicode InputEvents +// Arguments: +// inEvents - InputEvents to convert +// partialEvent - on output, will contain a partial dbcs byte char +// data if the last event in inEvents is a dbcs lead byte +// Return Value: +// - inEvents will contain unicode InputEvents +// - partialEvent may contain a partial dbcs KeyEvent +void EventsToUnicode(_Inout_ std::deque>& inEvents, + _Out_ std::unique_ptr& partialEvent) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + std::deque> outEvents; + + while (!inEvents.empty()) + { + std::unique_ptr currentEvent = std::move(inEvents.front()); + inEvents.pop_front(); + + if (currentEvent->EventType() != InputEventType::KeyEvent) + { + outEvents.push_back(std::move(currentEvent)); + } + else + { + const KeyEvent* const keyEvent = static_cast(currentEvent.get()); + + std::wstring outWChar; + HRESULT hr = S_OK; + + // convert char data to unicode + if (IsDBCSLeadByteConsole(static_cast(keyEvent->GetCharData()), &gci.CPInfo)) + { + if (inEvents.empty()) + { + // we ran out of data and have a partial byte leftover + partialEvent = std::move(currentEvent); + break; + } + + // get the 2nd byte and convert to unicode + const KeyEvent* const keyEventEndByte = static_cast(inEvents.front().get()); + inEvents.pop_front(); + + char inBytes[] = + { + static_cast(keyEvent->GetCharData()), + static_cast(keyEventEndByte->GetCharData()) + }; + try + { + outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); + } + catch (...) + { + hr = wil::ResultFromCaughtException(); + } + } + else + { + char inBytes[] = + { + static_cast(keyEvent->GetCharData()) + }; + try + { + outWChar = ConvertToW(gci.CP, { inBytes, ARRAYSIZE(inBytes) }); + } + catch (...) + { + hr = wil::ResultFromCaughtException(); + } + } + + // push unicode key events back out + if (SUCCEEDED(hr) && outWChar.size() > 0) + { + KeyEvent unicodeKeyEvent = *keyEvent; + for (const auto wch : outWChar) + { + try + { + unicodeKeyEvent.SetCharData(wch); + outEvents.push_back(std::make_unique(unicodeKeyEvent)); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } + } + } + } + + inEvents.swap(outEvents); + return; +} + +// Routine Description: +// - This routine reads or peeks input events. In both cases, the events +// are copied to the user's buffer. In the read case they are removed +// from the input buffer and in the peek case they are not. +// Arguments: +// - pInputBuffer - The input buffer to take records from to return to the client +// - outEvents - The storage location to fill with input events +// - eventReadCount - The number of events to read +// - pInputReadHandleData - 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. +// - IsUnicode - Whether to operate on Unicode characters or convert +// on the current Input Codepage. +// - IsPeek - If this is a peek operation (a.k.a. do not remove +// characters from the input buffer while copying to client buffer.) +// - ppWaiter - 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. +// Return Value: +// - STATUS_SUCCESS - If data was found and ready for return to the client. +// - 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 +{ + try + { + waiter.reset(); + + if (eventReadCount == 0) + { + return STATUS_SUCCESS; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + std::deque> partialEvents; + if (!IsUnicode) + { + if (inputBuffer.IsReadPartialByteSequenceAvailable()) + { + partialEvents.push_back(inputBuffer.FetchReadPartialByteSequence(IsPeek)); + } + } + + size_t amountToRead; + if (FAILED(SizeTSub(eventReadCount, partialEvents.size(), &amountToRead))) + { + return STATUS_INTEGER_OVERFLOW; + } + std::deque> readEvents; + NTSTATUS Status = inputBuffer.Read(readEvents, + amountToRead, + IsPeek, + true, + IsUnicode, + false); + + if (CONSOLE_STATUS_WAIT == Status) + { + FAIL_FAST_IF(!(readEvents.empty())); + // If we're told to wait until later, move all of our context + // to the read data object and send it back up to the server. + waiter = std::make_unique(&inputBuffer, + &readHandleState, + eventReadCount, + std::move(partialEvents)); + } + else if (NT_SUCCESS(Status)) + { + // split key events to oem chars if necessary + if (!IsUnicode) + { + try + { + SplitToOem(readEvents); + } + CATCH_LOG(); + } + + // combine partial and readEvents + while (!partialEvents.empty()) + { + readEvents.push_front(std::move(partialEvents.back())); + partialEvents.pop_back(); + } + + // move events over + for (size_t i = 0; i < eventReadCount; ++i) + { + if (readEvents.empty()) + { + break; + } + outEvents.push_back(std::move(readEvents.front())); + readEvents.pop_front(); + } + + // store partial event if necessary + if (!readEvents.empty()) + { + inputBuffer.StoreReadPartialByteSequence(std::move(readEvents.front())); + readEvents.pop_front(); + FAIL_FAST_IF(!(readEvents.empty())); + } + } + 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 + { + RETURN_NTSTATUS(_DoGetConsoleInput(context, + outEvents, + eventsToRead, + readHandleState, + false, + true, + waiter)); + } + 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 + { + RETURN_NTSTATUS(_DoGetConsoleInput(context, + outEvents, + eventsToRead, + readHandleState, + true, + true, + waiter)); + } + 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 + { + RETURN_NTSTATUS(_DoGetConsoleInput(context, + outEvents, + eventsToRead, + readHandleState, + false, + false, + waiter)); + } + 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 + { + RETURN_NTSTATUS(_DoGetConsoleInput(context, + outEvents, + eventsToRead, + readHandleState, + true, + false, + waiter)); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Writes events to the input buffer +// Arguments: +// - context - the input buffer to write to +// - events - the events to written +// - written - on output, the number of events written +// - append - true if events should be written to the end of the input +// buffer, false if they should be written to the front +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +static HRESULT _WriteConsoleInputWImplHelper(InputBuffer& context, + std::deque>& events, + size_t& written, + const bool append) noexcept +{ + try + { + written = 0; + + // add to InputBuffer + if (append) + { + written = context.Write(events); + } + else + { + written = context.Prepend(events); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Writes events to the input buffer already formed into IInputEvents (private call) +// Arguments: +// - context - the input buffer to write to +// - events - the events to written +// - written - on output, the number of events written +// - append - true if events should be written to the end of the input +// buffer, false if they should be written to the front +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +HRESULT DoSrvPrivateWriteConsoleInputW(_Inout_ InputBuffer* const pInputBuffer, + _Inout_ std::deque>& events, + _Out_ size_t& eventsWritten, + const bool append) noexcept +{ + return _WriteConsoleInputWImplHelper(*pInputBuffer, events, eventsWritten, append); +} + +// Routine Description: +// - Writes events to the input buffer, translating from codepage to unicode first +// Arguments: +// - context - the input buffer to write to +// - buffer - the events to written +// - written - on output, the number of events written +// - append - true if events should be written to the end of the input +// buffer, false if they should be written to the front +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleInputAImpl(InputBuffer& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept +{ + written = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + auto events = IInputEvent::Create(buffer); + + // add partial byte event if necessary + if (context.IsWritePartialByteSequenceAvailable()) + { + events.push_front(context.FetchWritePartialByteSequence(false)); + } + + // convert to unicode if necessary + std::unique_ptr partialEvent; + EventsToUnicode(events, partialEvent); + + if (partialEvent.get()) + { + context.StoreWritePartialByteSequence(std::move(partialEvent)); + } + + return _WriteConsoleInputWImplHelper(context, events, written, append); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Writes events to the input buffer +// Arguments: +// - context - the input buffer to write to +// - buffer - the events to written +// - written - on output, the number of events written +// - append - true if events should be written to the end of the input +// buffer, false if they should be written to the front +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleInputWImpl(InputBuffer& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept +{ + written = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + auto events = IInputEvent::Create(buffer); + + return _WriteConsoleInputWImplHelper(context, events, written, append); + } + CATCH_RETURN(); +} + +// Function Description: +// - Writes the input records to the beginning of the input buffer. This is used +// by VT sequences that need a response immediately written back to the +// input. +// Arguments: +// - pInputBuffer - the input buffer to write to +// - events - the events to written +// - eventsWritten - on output, the number of events written +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +HRESULT DoSrvPrivatePrependConsoleInput(_Inout_ InputBuffer* const pInputBuffer, + _Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + eventsWritten = 0; + + try + { + // add partial byte event if necessary + if (pInputBuffer->IsWritePartialByteSequenceAvailable()) + { + events.push_front(pInputBuffer->FetchWritePartialByteSequence(false)); + } + } + CATCH_RETURN(); + + // add to InputBuffer + eventsWritten = pInputBuffer->Prepend(events); + + return S_OK; +} + +// Function Description: +// - Writes the input KeyEvent to the console as a console control event. This +// can be used for potentially generating Ctrl-C events, as +// HandleGenericKeyEvent will correctly generate the Ctrl-C response in +// the same way that it'd be handled from the window proc, with the proper +// processed vs raw input handling. +// If the input key is *not* a Ctrl-C key, then it will get written to the +// buffer just the same as any other KeyEvent. +// Arguments: +// - pInputBuffer - the input buffer to write to. Currently unused, as +// HandleGenericKeyEvent just gets the global input buffer, but all +// ConGetSet API's require a input or output object. +// - key - The keyevent to send to the console. +// Return Value: +// - HRESULT indicating success or failure +[[nodiscard]] +HRESULT DoSrvPrivateWriteConsoleControlInput(_Inout_ InputBuffer* const /*pInputBuffer*/, + _In_ KeyEvent key) +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + HandleGenericKeyEvent(key, false); + + return S_OK; +} + +// Routine Description: +// - This is used when the app is reading output as cells and needs them converted +// into a particular codepage on the way out. +// Arguments: +// - codepage - The relevant codepage for translation +// - buffer - This is the buffer containing all of the character data to be converted +// - rectangle - This is the rectangle describing the region that the buffer covers. +// Return Value: +// - Generally S_OK. Could be a memory or math error code. +[[nodiscard]] +static HRESULT _ConvertCellsToAInplace(const UINT codepage, const gsl::span buffer, const Viewport rectangle) noexcept +{ + try + { + std::vector tempBuffer(buffer.cbegin(), buffer.cend()); + + const auto size = rectangle.Dimensions(); + auto tempIter = tempBuffer.cbegin(); + auto outIter = buffer.begin(); + + for (int i = 0; i < size.Y; i++) + { + for (int j = 0; j < size.X; j++) + { + // Any time we see the lead flag, we presume there will be a trailing one following it. + // Giving us two bytes of space (one per cell in the ascii part of the character union) + // to fill with whatever this Unicode character converts into. + if (WI_IsFlagSet(tempIter->Attributes, COMMON_LVB_LEADING_BYTE)) + { + // As long as we're not looking at the exact last column of the buffer... + if (j < size.X - 1) + { + // Walk forward one because we're about to consume two cells. + j++; + + // Try to convert the unicode character (2 bytes) in the leading cell to the codepage. + CHAR AsciiDbcs[2] = { 0 }; + UINT NumBytes = gsl::narrow(sizeof(AsciiDbcs)); + NumBytes = ConvertToOem(codepage, &tempIter->Char.UnicodeChar, 1, &AsciiDbcs[0], NumBytes); + + // Fill the 1 byte (AsciiChar) portion of the leading and trailing cells with each of the bytes returned. + outIter->Char.AsciiChar = AsciiDbcs[0]; + outIter->Attributes = tempIter->Attributes; + outIter++; + tempIter++; + outIter->Char.AsciiChar = AsciiDbcs[1]; + outIter->Attributes = tempIter->Attributes; + outIter++; + tempIter++; + } + else + { + // When we're in the last column with only a leading byte, we can't return that without a trailing. + // Instead, replace the output data with just a space and clear all flags. + outIter->Char.AsciiChar = UNICODE_SPACE; + outIter->Attributes = tempIter->Attributes; + WI_ClearAllFlags(outIter->Attributes, COMMON_LVB_SBCSDBCS); + outIter++; + tempIter++; + } + } + else if (WI_AreAllFlagsClear(tempIter->Attributes, COMMON_LVB_SBCSDBCS)) + { + // If there are no leading/trailing pair flags, then we only have 1 ascii byte to try to fit the + // 2 byte UTF-16 character into. Give it a go. + ConvertToOem(codepage, &tempIter->Char.UnicodeChar, 1, &outIter->Char.AsciiChar, 1); + outIter->Attributes = tempIter->Attributes; + outIter++; + tempIter++; + } + } + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - This is used when the app writes oem to the output buffer we want +// UnicodeOem or Unicode in the buffer, depending on font +// Arguments: +// - codepage - The relevant codepage for translation +// - buffer - This is the buffer containing all of the character data to be converted +// - rectangle - This is the rectangle describing the region that the buffer covers. +// Return Value: +// - Generally S_OK. Could be a memory or math error code. +[[nodiscard]] +static HRESULT _ConvertCellsToWInplace(const UINT codepage, gsl::span buffer, const Viewport& rectangle) noexcept +{ + try + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + const auto size = rectangle.Dimensions(); + auto outIter = buffer.begin(); + + for (int i = 0; i < size.Y; i++) + { + for (int j = 0; j < size.X; j++) + { + // Clear lead/trailing flags. We'll determine it for ourselves versus the given codepage. + WI_ClearAllFlags(outIter->Attributes, COMMON_LVB_SBCSDBCS); + + // If the 1 byte given is a lead in this codepage, we likely need two cells for the width. + if (IsDBCSLeadByteConsole(outIter->Char.AsciiChar, &gci.OutputCPInfo)) + { + // If we're not on the last column, we have two cells to use. + if (j < size.X - 1) + { + // Mark we're consuming two cells. + j++; + + // Grab the lead/trailing byte pair from this cell and the next one forward. + CHAR AsciiDbcs[2]; + AsciiDbcs[0] = outIter->Char.AsciiChar; + AsciiDbcs[1] = (outIter + 1)->Char.AsciiChar; + + // Convert it to UTF-16. + WCHAR UnicodeDbcs[2]; + ConvertOutputToUnicode(codepage, &AsciiDbcs[0], 2, &UnicodeDbcs[0], 2); + + // Store the actual character in the first available position. + outIter->Char.UnicodeChar = UnicodeDbcs[0]; + WI_ClearAllFlags(outIter->Attributes, COMMON_LVB_SBCSDBCS); + WI_SetFlag(outIter->Attributes, COMMON_LVB_LEADING_BYTE); + outIter++; + + // Put a padding character in the second position. + outIter->Char.UnicodeChar = UNICODE_DBCS_PADDING; + WI_ClearAllFlags(outIter->Attributes, COMMON_LVB_SBCSDBCS); + WI_SetFlag(outIter->Attributes, COMMON_LVB_TRAILING_BYTE); + outIter++; + } + else + { + // If we were on the last column, put in a space. + outIter->Char.UnicodeChar = UNICODE_SPACE; + WI_ClearAllFlags(outIter->Attributes, COMMON_LVB_SBCSDBCS); + outIter++; + } + } + else + { + // If it's not detected as a lead byte of a pair, then just convert it in place and move on. + CHAR c = outIter->Char.AsciiChar; + + ConvertOutputToUnicode(codepage, &c, 1, &outIter->Char.UnicodeChar, 1); + outIter++; + } + } + } + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +static std::vector _ConvertCellsToMungedW(gsl::span buffer, const Viewport& rectangle) +{ + std::vector result; + result.reserve(buffer.size() * 2); // we estimate we'll need up to double the cells if they all expand. + + const auto size = rectangle.Dimensions(); + auto bufferIter = buffer.cbegin(); + + for (SHORT i = 0; i < size.Y; i++) + { + for (SHORT j = 0; j < size.X; j++) + { + // Prepare a candidate charinfo on the output side copying the colors but not the lead/trail information. + CHAR_INFO candidate; + candidate.Attributes = bufferIter->Attributes; + WI_ClearAllFlags(candidate.Attributes, COMMON_LVB_SBCSDBCS); + + // If the glyph we're given is full width, it needs to take two cells. + if (IsGlyphFullWidth(bufferIter->Char.UnicodeChar)) + { + // If we're not on the final cell of the row... + if (j < size.X - 1) + { + // Mark that we're consuming two cells. + j++; + + // Fill one cell with a copy of the color and character marked leading + candidate.Char.UnicodeChar = bufferIter->Char.UnicodeChar; + WI_SetFlag(candidate.Attributes, COMMON_LVB_LEADING_BYTE); + result.push_back(candidate); + + // Fill a second cell with a copy of the color marked trailing and a padding character. + candidate.Char.UnicodeChar = UNICODE_DBCS_PADDING; + candidate.Attributes = bufferIter->Attributes; + WI_ClearAllFlags(candidate.Attributes, COMMON_LVB_SBCSDBCS); + WI_SetFlag(candidate.Attributes, COMMON_LVB_TRAILING_BYTE); + + } + else + { + // If we're on the final cell, this won't fit. Replace with a space. + candidate.Char.UnicodeChar = UNICODE_SPACE; + } + } + else + { + // If we're not full-width, we're half-width. Just copy the character over. + candidate.Char.UnicodeChar = bufferIter->Char.UnicodeChar; + } + + // Push our candidate in. + result.push_back(candidate); + + // Advance to read the next item. + bufferIter++; + } + } + return result; +} + +[[nodiscard]] +static HRESULT _ReadConsoleOutputWImplHelper(const SCREEN_INFORMATION& context, + gsl::span targetBuffer, + const Microsoft::Console::Types::Viewport& requestRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept +{ + try + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& storageBuffer = context.GetActiveBuffer(); + const auto storageSize = storageBuffer.GetBufferSize().Dimensions(); + + const auto targetSize = requestRectangle.Dimensions(); + + // If either dimension of the request is too small, return an empty rectangle as read and exit early. + if (targetSize.X <= 0 || targetSize.Y <= 0) + { + readRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 }); + return S_OK; + } + + // The buffer given should be big enough to hold the dimensions of the request. + ptrdiff_t targetArea; + RETURN_IF_FAILED(PtrdiffTMult(targetSize.X, targetSize.Y, &targetArea)); + RETURN_HR_IF(E_INVALIDARG, targetArea < 0); + RETURN_HR_IF(E_INVALIDARG, targetArea < targetBuffer.size()); + + // Clip the request rectangle to the size of the storage buffer + SMALL_RECT clip = requestRectangle.ToExclusive(); + clip.Right = std::min(clip.Right, storageSize.X); + clip.Bottom = std::min(clip.Bottom, storageSize.Y); + + // Find the target point (where to write the user's buffer) + // It will either be 0,0 or offset into the buffer by the inverse of the negative values. + COORD targetPoint; + targetPoint.X = clip.Left < 0 ? -clip.Left : 0; + targetPoint.Y = clip.Top < 0 ? -clip.Top : 0; + + // The clipped rect must be inside the buffer size, so it has a minimum value of 0. (max of itself and 0) + clip.Left = std::max(clip.Left, 0i16); + clip.Top = std::max(clip.Top, 0i16); + + // The final "request rectangle" or the area inside the buffer we want to read, is the clipped dimensions. + const auto clippedRequestRectangle = Viewport::FromExclusive(clip); + + // We will start reading the buffer at the point of the top left corner (origin) of the (potentially adjusted) request + const auto sourcePoint = clippedRequestRectangle.Origin(); + + // Get an iterator to the beginning of the return buffer + // We might have to seek this forward or skip around if we clipped the request. + auto targetIter = targetBuffer.begin(); + COORD targetPos = { 0 }; + const auto targetLimit = Viewport::FromDimensions(targetPoint, clippedRequestRectangle.Dimensions()); + + // Get an iterator to the beginning of the request inside the screen buffer + // This should walk exactly along every cell of the clipped request. + auto sourceIter = storageBuffer.GetCellDataAt(sourcePoint, clippedRequestRectangle); + + // Walk through every cell of the target, advancing the buffer. + // Validate that we always still have a valid iterator to the backgin store, + // that we always are writing inside the user's buffer (before the end) + // and we're always targeting the user's buffer inside its original bounds. + while (sourceIter && targetIter < targetBuffer.end()) + { + // If the point we're trying to write is inside the limited buffer write zone... + if (targetLimit.IsInBounds(targetPos)) + { + // Copy the data into position... + *targetIter = gci.AsCharInfo(*sourceIter); + // ... and advance the read iterator. + sourceIter++; + } + + // Always advance the write iterator, we might have skipped it due to clipping. + targetIter++; + + // Increment the target + targetPos.X++; + if (targetPos.X >= targetSize.X) + { + targetPos.X = 0; + targetPos.Y++; + } + } + + // Reply with the region we read out of the backing buffer (potentially clipped) + readRectangle = clippedRequestRectangle; + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleOutputAImpl(const SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto codepage = gci.OutputCP; + + RETURN_IF_FAILED(_ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle)); + + LOG_IF_FAILED(_ConvertCellsToAInplace(codepage, buffer, readRectangle)); + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleOutputWImpl(const SCREEN_INFORMATION& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + RETURN_IF_FAILED(_ReadConsoleOutputWImplHelper(context, buffer, sourceRectangle, readRectangle)); + + if (!context.GetActiveBuffer().GetCurrentFont().IsTrueTypeFont()) + { + // For compatibility reasons, we must maintain the behavior that munges the data if we are writing while a raster font is enabled. + // This can be removed when raster font support is removed. + UnicodeRasterFontCellMungeOnRead(buffer); + } + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +static HRESULT _WriteConsoleOutputWImplHelper(SCREEN_INFORMATION& context, + gsl::span buffer, + const Viewport& requestRectangle, + Viewport& writtenRectangle) noexcept +{ + try + { + auto& storageBuffer = context.GetActiveBuffer(); + const auto storageRectangle = storageBuffer.GetBufferSize(); + const auto storageSize = storageRectangle.Dimensions(); + + const auto sourceSize = requestRectangle.Dimensions(); + + // If either dimension of the request is too small, return an empty rectangle as the read and exit early. + if (sourceSize.X <= 0 || sourceSize.Y <= 0) + { + writtenRectangle = Viewport::FromDimensions(requestRectangle.Origin(), { 0, 0 }); + return S_OK; + } + + // If the top and left of the destination we're trying to write it outside the buffer, + // give the original request rectangle back and exit early OK. + if (requestRectangle.Left() >= storageSize.X || requestRectangle.Top() >= storageSize.Y) + { + writtenRectangle = requestRectangle; + return S_OK; + } + + // Do clipping according to the legacy patterns. + SMALL_RECT writeRegion = requestRectangle.ToInclusive(); + SMALL_RECT sourceRect; + if (writeRegion.Right > storageSize.X - 1) + { + writeRegion.Right = storageSize.X - 1; + } + sourceRect.Right = writeRegion.Right - writeRegion.Left; + if (writeRegion.Bottom > storageSize.Y - 1) + { + writeRegion.Bottom = storageSize.Y - 1; + } + sourceRect.Bottom = writeRegion.Bottom - writeRegion.Top; + + if (writeRegion.Left < 0) + { + sourceRect.Left = -writeRegion.Left; + writeRegion.Left = 0; + } + else + { + sourceRect.Left = 0; + } + + if (writeRegion.Top < 0) + { + sourceRect.Top = -writeRegion.Top; + writeRegion.Top = 0; + } + else + { + sourceRect.Top = 0; + } + + if (sourceRect.Left > sourceRect.Right || sourceRect.Top > sourceRect.Bottom) + { + return E_INVALIDARG; + } + + const auto writeRectangle = Viewport::FromInclusive(writeRegion); + + auto target = writeRectangle.Origin(); + + // For every row in the request, create a view into the clamped portion of just the one line to write. + // This allows us to restrict the width of the call without allocating/copying any memory by just making + // a smaller view over the existing big blob of data from the original call. + for (; target.Y < writeRectangle.BottomExclusive(); target.Y++) + { + // We find the offset into the original buffer by the dimensions of the original request rectangle. + ptrdiff_t rowOffset = 0; + RETURN_IF_FAILED(PtrdiffTSub(target.Y, requestRectangle.Top(), &rowOffset)); + RETURN_IF_FAILED(PtrdiffTMult(rowOffset, requestRectangle.Width(), &rowOffset)); + + ptrdiff_t colOffset = 0; + RETURN_IF_FAILED(PtrdiffTSub(target.X, requestRectangle.Left(), &colOffset)); + + ptrdiff_t totalOffset = 0; + RETURN_IF_FAILED(PtrdiffTAdd(rowOffset, colOffset, &totalOffset)); + + // Now we make a subspan starting from that offset for as much of the original request as would fit + const auto subspan = buffer.subspan(totalOffset, writeRectangle.Width()); + + // Convert to a CHAR_INFO view to fit into the iterator + const auto charInfos = std::basic_string_view(subspan.data(), subspan.size()); + + // Make the iterator and write to the target position. + OutputCellIterator it(charInfos); + storageBuffer.Write(it, target); + } + + // Since we've managed to write part of the request, return the clamped part that we actually used. + writtenRectangle = writeRectangle; + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleOutputAImpl(SCREEN_INFORMATION& context, + gsl::span buffer, + const Viewport& requestRectangle, + Viewport& writtenRectangle) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto codepage = gci.OutputCP; + LOG_IF_FAILED(_ConvertCellsToWInplace(codepage, buffer, requestRectangle)); + + RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, buffer, requestRectangle, writtenRectangle)); + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::WriteConsoleOutputWImpl(SCREEN_INFORMATION& context, + gsl::span buffer, + const Viewport& requestRectangle, + Viewport& writtenRectangle) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + if (!context.GetActiveBuffer().GetCurrentFont().IsTrueTypeFont()) + { + // For compatibility reasons, we must maintain the behavior that munges the data if we are writing while a raster font is enabled. + // This can be removed when raster font support is removed. + auto translated = _ConvertCellsToMungedW(buffer, requestRectangle); + RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, translated, requestRectangle, writtenRectangle)); + } + else + { + RETURN_IF_FAILED(_WriteConsoleOutputWImplHelper(context, buffer, requestRectangle, writtenRectangle)); + } + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleOutputAttributeImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept +{ + written = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto attrs = ReadOutputAttributes(context.GetActiveBuffer(), origin, buffer.size()); + std::copy(attrs.cbegin(), attrs.cend(), buffer.begin()); + written = attrs.size(); + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleOutputCharacterAImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept +{ + written = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto chars = ReadOutputStringA(context.GetActiveBuffer(), + origin, + buffer.size()); + + // for compatibility reasons, if we receive more chars than can fit in the buffer + // then we don't send anything back. + if (chars.size() <= gsl::narrow(buffer.size())) + { + std::copy(chars.cbegin(), chars.cend(), buffer.begin()); + written = chars.size(); + } + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleOutputCharacterWImpl(const SCREEN_INFORMATION& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept +{ + written = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto chars = ReadOutputStringW(context.GetActiveBuffer(), + origin, + buffer.size()); + + // Only copy if the whole result will fit. + if (chars.size() <= gsl::narrow(buffer.size())) + { + std::copy(chars.cbegin(), chars.cend(), buffer.begin()); + written = chars.size(); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// There used to be a text mode and a graphics mode flag. +// Text mode was used for regular applications like CMD.exe. +// Graphics mode was used for bitmap VDM buffers and is no longer supported. +// OEM console font mode used to represent rewriting the entire buffer into codepage 437 so the renderer could handle it with raster fonts. +// But now the entire buffer is always kept in Unicode and the renderer asks for translation when/if necessary for raster fonts only. +// We keep these definitions here so the API can enforce that the only one we support any longer is the original text mode. +// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms682122(v=vs.85).aspx +#define CONSOLE_TEXTMODE_BUFFER 1 +//#define CONSOLE_GRAPHICS_BUFFER 2 +//#define CONSOLE_OEMFONT_DISPLAY 4 + +[[nodiscard]] +NTSTATUS ConsoleCreateScreenBuffer(std::unique_ptr& handle, + _In_ PCONSOLE_API_MSG /*Message*/, + _In_ PCD_CREATE_OBJECT_INFORMATION Information, + _In_ PCONSOLE_CREATESCREENBUFFER_MSG a) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::CreateConsoleScreenBuffer); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // If any buffer type except the one we support is set, it's invalid. + if (WI_IsAnyFlagSet(a->Flags, ~CONSOLE_TEXTMODE_BUFFER)) + { + // We no longer support anything other than a textmode buffer + return STATUS_INVALID_PARAMETER; + } + + ConsoleHandleData::HandleType const HandleType = ConsoleHandleData::HandleType::Output; + + const SCREEN_INFORMATION& siExisting = gci.GetActiveOutputBuffer(); + + // Create new screen buffer. + COORD WindowSize = siExisting.GetViewport().Dimensions(); + const FontInfo& existingFont = siExisting.GetCurrentFont(); + SCREEN_INFORMATION* ScreenInfo = nullptr; + NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(WindowSize, + existingFont, + WindowSize, + siExisting.GetAttributes(), + siExisting.GetAttributes(), + CURSOR_SMALL_SIZE, + &ScreenInfo); + + if (!NT_SUCCESS(Status)) + { + goto Exit; + } + + Status = NTSTATUS_FROM_HRESULT(ScreenInfo->AllocateIoHandle(HandleType, + Information->DesiredAccess, + Information->ShareMode, + handle)); + + if (!NT_SUCCESS(Status)) + { + goto Exit; + } + + SCREEN_INFORMATION::s_InsertScreenBuffer(ScreenInfo); + +Exit: + if (!NT_SUCCESS(Status)) + { + delete ScreenInfo; + } + + return Status; +} diff --git a/src/host/directio.h b/src/host/directio.h new file mode 100644 index 000000000..045d84d1a --- /dev/null +++ b/src/host/directio.h @@ -0,0 +1,43 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- directio.h + +Abstract: +- This file implements the NT console direct I/O API (read/write STDIO streams) + +Author: +- KazuM Apr.19.1996 + +Revision History: +--*/ + +#pragma once + +#include "conapi.h" +#include "inputBuffer.hpp" + +class SCREEN_INFORMATION; + +[[nodiscard]] +HRESULT DoSrvPrivateWriteConsoleInputW(_Inout_ InputBuffer* const pInputBuffer, + _Inout_ std::deque>& events, + _Out_ size_t& eventsWritten, + const bool append) noexcept; + +[[nodiscard]] +NTSTATUS ConsoleCreateScreenBuffer(std::unique_ptr& handle, + _In_ PCONSOLE_API_MSG Message, + _In_ PCD_CREATE_OBJECT_INFORMATION Information, + _In_ PCONSOLE_CREATESCREENBUFFER_MSG a); + +[[nodiscard]] +NTSTATUS DoSrvPrivatePrependConsoleInput(_Inout_ InputBuffer* const pInputBuffer, + _Inout_ std::deque>& events, + _Out_ size_t& eventsWritten); + +[[nodiscard]] +NTSTATUS DoSrvPrivateWriteConsoleControlInput(_Inout_ InputBuffer* const pInputBuffer, + _In_ KeyEvent key); diff --git a/src/host/dirs b/src/host/dirs new file mode 100644 index 000000000..7c34ce566 --- /dev/null +++ b/src/host/dirs @@ -0,0 +1,7 @@ +DIRS=exe \ + lib \ + ut_lib \ + ut_host \ + ft_host \ + ft_integrity \ + ft_uia \ diff --git a/src/host/dll/host.vcxproj b/src/host/dll/host.vcxproj new file mode 100644 index 000000000..7fcc7c497 --- /dev/null +++ b/src/host/dll/host.vcxproj @@ -0,0 +1,81 @@ + + + + + + + Create + ProgramDatabase + + + + + + + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {1c959542-bac2-4e55-9a6d-13251914cbb9} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + + + + + + + + {E437B604-3E98-4F40-A927-E173E818EA4B} + Win32Proj + host + Host.DLL + ConhostV2 + + + + + true + + + + %(PreprocessorDefinitions) + + + ConhostV2.def + true + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + + + + diff --git a/src/host/dll/host.vcxproj.filters b/src/host/dll/host.vcxproj.filters new file mode 100644 index 000000000..6bea88079 --- /dev/null +++ b/src/host/dll/host.vcxproj.filters @@ -0,0 +1,43 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + + + Source Files + + + + + Resource Files + + + diff --git a/src/host/exe/Conhost.exe.mui.lci b/src/host/exe/Conhost.exe.mui.lci new file mode 100644 index 000000000..0c2630111 --- /dev/null +++ b/src/host/exe/Conhost.exe.mui.lci @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/host/exe/Host.EXE.rc b/src/host/exe/Host.EXE.rc new file mode 100644 index 000000000..d1494f524 --- /dev/null +++ b/src/host/exe/Host.EXE.rc @@ -0,0 +1,70 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" +#include "..\\resource.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "#include ""..\\resource.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "#include ""..\\res.rc""\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +#include "..\\res.rc" + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/src/host/exe/Host.EXE.vcxproj b/src/host/exe/Host.EXE.vcxproj new file mode 100644 index 000000000..e7505b66f --- /dev/null +++ b/src/host/exe/Host.EXE.vcxproj @@ -0,0 +1,89 @@ + + + + + + + + + + Create + ProgramDatabase + + + + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {48d21369-3d7b-4431-9967-24e81292cf62} + + + {1c959542-bac2-4e55-9a6d-13251914cbb9} + + + {990f2657-8580-4828-943f-5dd657d11842} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + + + + + {9CBD7DFA-1754-4A9D-93D7-857A9D17CB1B} + Win32Proj + HostEXE + Host.EXE + OpenConsole + + + + + true + + + + ..;%(AdditionalIncludeDirectories) + + + true + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + + + + \ No newline at end of file diff --git a/src/host/exe/Host.EXE.vcxproj.filters b/src/host/exe/Host.EXE.vcxproj.filters new file mode 100644 index 000000000..09a26cd60 --- /dev/null +++ b/src/host/exe/Host.EXE.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + + + Resource Files + + + + + + \ No newline at end of file diff --git a/src/host/exe/SystemDefault.Manifest b/src/host/exe/SystemDefault.Manifest new file mode 100644 index 000000000..1583480d3 --- /dev/null +++ b/src/host/exe/SystemDefault.Manifest @@ -0,0 +1,36 @@ +/* +This is an odd use of Sxs. See the sources file for more comments. +Conhost is created via RtlCreateUserProcess instead of CreateProcess, +so it does not end up with a system default manifest. We work around +this by having this manifest which references the system default that +we load and set as our process default at runtime. + +The system default manifest is actually this: + SYSTEM_COMPATIBLE_ASSEMBLY_VERSION_00 = \ + "L\"$(SYSTEM_COMPATIBLE_ASSEMBLY_VERSION).0.0\"" +and not + SYSTEM_COMPATIBLE_ASSEMBLY_FULL_VERSION_A + +*/ + + + +Microsoft Console Host System Default + + + + + + diff --git a/src/host/exe/conhost.exe.manifest b/src/host/exe/conhost.exe.manifest new file mode 100644 index 000000000..3cd9d46ff --- /dev/null +++ b/src/host/exe/conhost.exe.manifest @@ -0,0 +1,22 @@ + + + +Console Window Host + + + + + + + + + + diff --git a/src/host/exe/conhost.man b/src/host/exe/conhost.man new file mode 100644 index 000000000..b0e31140f --- /dev/null +++ b/src/host/exe/conhost.man @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/host/exe/conhost.rcv b/src/host/exe/conhost.rcv new file mode 100644 index 000000000..3c7e0d6b9 --- /dev/null +++ b/src/host/exe/conhost.rcv @@ -0,0 +1,5 @@ +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Window Host" +#define VER_INTERNALNAME_STR "ConHost" +#define VER_ORIGINALFILENAME_STR "CONHOST.EXE" diff --git a/src/host/exe/conhost.resources.man b/src/host/exe/conhost.resources.man new file mode 100644 index 000000000..a8db3e6fb --- /dev/null +++ b/src/host/exe/conhost.resources.man @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/src/host/exe/console.ico b/src/host/exe/console.ico new file mode 100644 index 000000000..fc756afc1 Binary files /dev/null and b/src/host/exe/console.ico differ diff --git a/src/host/exe/makefile.inc b/src/host/exe/makefile.inc new file mode 100644 index 000000000..d99a8292d --- /dev/null +++ b/src/host/exe/makefile.inc @@ -0,0 +1,2 @@ +$(OBJ_PATH)\$O\$(TARGETNAME).res: $(O_MANIFESTS) + diff --git a/src/host/exe/product.pbxproj b/src/host/exe/product.pbxproj new file mode 100644 index 000000000..01078b1e4 --- /dev/null +++ b/src/host/exe/product.pbxproj @@ -0,0 +1,13 @@ + + + + v110 + Utility + + + + + $(High_UI_Langs) + + + \ No newline at end of file diff --git a/src/host/exe/resource.h b/src/host/exe/resource.h new file mode 100644 index 000000000..c578529e2 --- /dev/null +++ b/src/host/exe/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Host.EXE.rc +#include "..\\resource.h" +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 103 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/host/exe/sources b/src/host/exe/sources new file mode 100644 index 000000000..b540818c9 --- /dev/null +++ b/src/host/exe/sources @@ -0,0 +1,58 @@ +!include ..\sources.inc +# ------------------------------------- +# Windows Console +# - Console Host Core +# ------------------------------------- + +# This program provides the entry-point +# for when the Windows OS loader attempts to start a Win32 Console-type +# application that does not already have console handles attached. +# It will attempt to resolve the correct module for the OS platform and configuration +# and then pass control to that module for runtime services. + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = conhost +TARGETTYPE = PROGRAM +UMTYPE = windows +UMENTRY = wwinmain +TARGET_DESTINATION = retail + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + ..\exemain.cpp \ + ..\res.rc \ + +# ------------------------------------- +# Side-by-side Manifesting +# ------------------------------------- + +SYSTEM_COMPATIBLE_ASSEMBLY_VERSION_00 = $(SYSTEM_COMPATIBLE_ASSEMBLY_VERSION).0.0 +SXS_MANIFEST_DEFINES = \ + $(SXS_MANIFEST_DEFINES) \ + -DSYSTEM_COMPATIBLE_ASSEMBLY_VERSION_00_A="\"$(SYSTEM_COMPATIBLE_ASSEMBLY_VERSION_00)\"" \ + +SXS_ASSEMBLY_NAME = Microsoft.Console.Host.Core +SXS_ASSEMBLY_LANGUAGE_INDEPENDENT = 1 +SXS_MANIFEST = conhost.exe.Manifest +SXS_MANIFEST_IN_RESOURCES = 1 +SXS_NO_BINPLACE = 1 + +CMI_USE_VERSION_XML = 1 + +O_MANIFESTS= \ + $(OBJ_PATH)\$(O)\SystemDefault.man \ + conhost.man + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + +NTTARGETFILE0=\ + $(O_MANIFESTS) diff --git a/src/host/exemain.cpp b/src/host/exemain.cpp new file mode 100644 index 000000000..b302f3bf7 --- /dev/null +++ b/src/host/exemain.cpp @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleArguments.hpp" +#include "srvinit.h" +#include "..\server\Entrypoints.h" +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Define TraceLogging provider +TRACELOGGING_DEFINE_PROVIDER( + g_ConhostLauncherProvider, + "Microsoft.Windows.Console.Launcher", + // {770aa552-671a-5e97-579b-151709ec0dbd} + (0x770aa552, 0x671a, 0x5e97, 0x57, 0x9b, 0x15, 0x17, 0x09, 0xec, 0x0d, 0xbd), + TraceLoggingOptionMicrosoftTelemetry()); + +static bool ShouldUseConhostV2() +{ + // If the registry value doesn't exist, or exists and is non-zero, we should default to using the v2 console. + // Otherwise, in the case of an explicit value of 0, we should use the legacy console. + bool fShouldUseConhostV2 = true; + PCSTR pszErrorDescription = NULL; + bool fIgnoreError = false; + + // open HKCU\Console + wil::unique_hkey hConsoleSubKey; + LONG lStatus = NTSTATUS_FROM_WIN32(RegOpenKeyExW(HKEY_CURRENT_USER, L"Console", 0, KEY_READ, &hConsoleSubKey)); + if (ERROR_SUCCESS == lStatus) + { + // now get the value of the ForceV2 reg value, if it exists + DWORD dwValue; + DWORD dwType; + DWORD cbValue = sizeof(dwValue); + lStatus = RegQueryValueExW(hConsoleSubKey.get(), + L"ForceV2", + nullptr, + &dwType, + (PBYTE)&dwValue, + &cbValue); + + + if (ERROR_SUCCESS == lStatus && + dwType == REG_DWORD && // response is a DWORD + cbValue == sizeof(dwValue)) // response data exists + { + // Value exists. If non-zero use v2 console. + fShouldUseConhostV2 = dwValue != 0; + } + else + { + pszErrorDescription = "RegQueryValueKey Failed"; + fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; + } + } + else + { + pszErrorDescription = "RegOpenKey Failed"; + // ignore error caused by RegOpenKey if it's a simple case of the key not being found + fIgnoreError = lStatus == ERROR_FILE_NOT_FOUND; + } + + return fShouldUseConhostV2; +} + +[[nodiscard]] +static HRESULT ValidateServerHandle(const HANDLE handle) +{ + // Make sure this is a console file. + FILE_FS_DEVICE_INFORMATION DeviceInformation; + IO_STATUS_BLOCK IoStatusBlock; + NTSTATUS const Status = NtQueryVolumeInformationFile(handle, &IoStatusBlock, &DeviceInformation, sizeof(DeviceInformation), FileFsDeviceInformation); + if (!NT_SUCCESS(Status)) + { + RETURN_NTSTATUS(Status); + } + else if (DeviceInformation.DeviceType != FILE_DEVICE_CONSOLE) + { + return E_INVALIDARG; + } + else + { + return S_OK; + } +} + +static bool ShouldUseLegacyConhost(const bool fForceV1) +{ + return fForceV1 || !ShouldUseConhostV2(); +} + +[[nodiscard]] +static HRESULT ActivateLegacyConhost(const HANDLE handle) +{ + HRESULT hr = S_OK; + + // TraceLog that we're using the legacy console. We won't log new console + // because there's already a count of how many total processes were launched. + // Total - legacy = new console. + // We expect legacy launches to be infrequent enough to not cause an issue. + TraceLoggingWrite(g_ConhostLauncherProvider, "IsLegacyLoaded", + TraceLoggingBool(true, "ConsoleLegacy"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_TELEMETRY)); + + PCWSTR pszConhostDllName = L"ConhostV1.dll"; + + // Load our implementation, and then Load/Launch the IO thread. + wil::unique_hmodule hConhostBin(LoadLibraryExW(pszConhostDllName, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32)); + if (hConhostBin.get() != nullptr) + { + typedef NTSTATUS(*PFNCONSOLECREATEIOTHREAD)(__in HANDLE Server); + + PFNCONSOLECREATEIOTHREAD pfnConsoleCreateIoThread = (PFNCONSOLECREATEIOTHREAD)GetProcAddress(hConhostBin.get(), "ConsoleCreateIoThread"); + if (pfnConsoleCreateIoThread != nullptr) + { + hr = HRESULT_FROM_NT(pfnConsoleCreateIoThread(handle)); + } + else + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + else + { + // setup status error + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + if (SUCCEEDED(hr)) + { + hConhostBin.release(); + } + + return hr; +} + +// Routine Description: +// - Main entry point for EXE version of console launching. +// This can be used as a debugging/diagnostics tool as well as a method of testing the console without +// replacing the system binary. +// Arguments: +// - hInstance - This module instance pointer is saved for resource lookups. +// - hPrevInstance - Unused pointer to the module instances. See wWinMain definitions @ MSDN for more details. +// - pwszCmdLine - Unused variable. We will look up the command line using GetCommandLineW(). +// - nCmdShow - Unused variable specifying window show/hide state for Win32 mode applications. +// Return value: +// - [[noreturn]] - This function will not return. It will kill the thread we were called from and the console server threads will take over. +int CALLBACK wWinMain( + _In_ HINSTANCE hInstance, + _In_ HINSTANCE /*hPrevInstance*/, + _In_ PWSTR /*pwszCmdLine*/, + _In_ int /*nCmdShow*/) +{ + ServiceLocator::LocateGlobals().hInstance = hInstance; + + ConsoleCheckDebug(); + + // Register Trace provider by GUID + TraceLoggingRegister(g_ConhostLauncherProvider); + + // Pass command line and standard handles at this point in time as + // potential preferences for execution that were passed on process creation. + ConsoleArguments args(GetCommandLineW(), + GetStdHandle(STD_INPUT_HANDLE), + GetStdHandle(STD_OUTPUT_HANDLE)); + + HRESULT hr = args.ParseCommandline(); + if (SUCCEEDED(hr)) + { + if (ShouldUseLegacyConhost(args.GetForceV1())) + { + if (args.ShouldCreateServerHandle()) + { + hr = E_INVALIDARG; + } + else + { + hr = ValidateServerHandle(args.GetServerHandle()); + + if (SUCCEEDED(hr)) + { + hr = ActivateLegacyConhost(args.GetServerHandle()); + } + } + } + else + { + if (args.ShouldCreateServerHandle()) + { + hr = Entrypoints::StartConsoleForCmdLine(args.GetClientCommandline().c_str(), &args); + } + else + { + hr = ValidateServerHandle(args.GetServerHandle()); + + if (SUCCEEDED(hr)) + { + hr = Entrypoints::StartConsoleForServerHandle(args.GetServerHandle(), &args); + } + } + } + } + + // Unregister Tracelogging + TraceLoggingUnregister(g_ConhostLauncherProvider); + + // Only do this if startup was successful. Otherwise, this will leave conhost.exe running with no hosted application. + if (SUCCEEDED(hr)) + { + // Since the lifetime of conhost.exe is inextricably tied to the lifetime of its client processes we set our process + // shutdown priority to zero in order to effectively opt out of shutdown process enumeration. Conhost will exit when + // all of its client processes do. + SetProcessShutdownParameters(0, 0); + + ExitThread(hr); + } + + return hr; +} diff --git a/src/host/ft_host/API_AliasTests.cpp b/src/host/ft_host/API_AliasTests.cpp new file mode 100644 index 000000000..7293ceafe --- /dev/null +++ b/src/host/ft_host/API_AliasTests.cpp @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This particular block is necessary so we can include both a UNICODE and non-UNICODE version +// of our test supporting function so we can accurately portray and measure both types of text +// and test both versions of the console API. + +// 1. Turn on Unicode if it isn't on already (it really should be) and include the headers +#ifndef UNICODE +#define UNICODE +#endif +#ifndef _UNICODE +#define _UNICODE +#endif +#include "API_AliasTestsHelpers.hpp" + +// 2. Undefine Unicode and include the header again to get the other version of the functions +#undef UNICODE +#undef _UNICODE +#include "API_AliasTestsHelpers.hpp" + +// 3. Finish up by putting Unicode back on for the rest of the code (like it should have been in the first place) +#define UNICODE +#define _UNICODE +// End double include block. + +// This class is intended to test: +// GetConsoleAlias + +class AliasTests +{ + BEGIN_TEST_CLASS(AliasTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl3.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"api-ms-win-core-console-l3-2-0.lib") + END_TEST_CLASS() + + BEGIN_TEST_METHOD(TestGetConsoleAlias) + TEST_METHOD_PROPERTY(L"Data:strSource", L"{g}") + TEST_METHOD_PROPERTY(L"Data:strExpectedTarget", L"{cmd.exe /k echo foo}") + TEST_METHOD_PROPERTY(L"Data:strExeName", L"{cmd.exe}") + TEST_METHOD_PROPERTY(L"Data:dwSource", L"{0, 1}") + TEST_METHOD_PROPERTY(L"Data:dwTarget", L"{0, 1, 2, 3, 4, 5, 6}") + TEST_METHOD_PROPERTY(L"Data:dwExeName", L"{0, 1}") + TEST_METHOD_PROPERTY(L"Data:bUnicode", L"{FALSE, TRUE}") + TEST_METHOD_PROPERTY(L"Data:bSetFirst", L"{FALSE, TRUE}") + END_TEST_METHOD() +}; + +// Caller must free ppsz if not null. +void ConvertWToA(_In_ PCWSTR pwsz, + _Out_ char** ppsz) +{ + *ppsz = nullptr; + + UINT const cp = CP_ACP; + + DWORD const dwBytesNeeded = WideCharToMultiByte(cp, 0, pwsz, -1, nullptr, 0, nullptr, nullptr); + VERIFY_WIN32_BOOL_SUCCEEDED(dwBytesNeeded, L"Verify that WC2MB could detect bytes needed for conversion."); + + char* psz = new char[dwBytesNeeded]; + VERIFY_IS_NOT_NULL(psz, L"Verify we could allocate necessary bytes for conversion."); + + VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(cp, 0, pwsz, -1, psz, dwBytesNeeded, nullptr, nullptr), L"Verify that WC2MB did the conversion successfully."); + + *ppsz = psz; +} + +void AliasTests::TestGetConsoleAlias() +{ + // Retrieve combinatorial parameters. + DWORD dwSource, dwTarget, dwExeName; + bool bUnicode, bSetFirst; + + String strSource, strExpectedTarget, strExeName; + + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"strSource", strSource), L"Get source string"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"strExpectedTarget", strExpectedTarget), L"Get expected target string"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"strExeName", strExeName), L"Get EXE name"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"dwSource", dwSource), L"Get source string type"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"dwTarget", dwTarget), L"Get target string type"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"dwExeName", dwExeName), L"Get EXE Name type"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"bUnicode", bUnicode), L"Get whether this test is running in Unicode."); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"bSetFirst", bSetFirst), L"Whether we should set this alias before trying to get it."); + + Log::Comment(String().Format(L"Source type: %d Target type: %d Exe type: %d Unicode: %d Set First: %d\r\n", dwSource, dwTarget, dwExeName, bUnicode, bSetFirst)); + + if (bUnicode) + { + TestGetConsoleAliasHelperW((wchar_t*)strSource.GetBuffer(), + (wchar_t*)strExpectedTarget.GetBuffer(), + (wchar_t*)strExeName.GetBuffer(), + dwSource, + dwTarget, + dwExeName, + bUnicode, + bSetFirst); + } + else + { + // If we're not Unicode, we need to convert all the Unicode strings from our test into A strings. + char* szSource = nullptr; + char* szExpectedTarget = nullptr; + char* szExeName = nullptr; + + auto cleanupSource = wil::scope_exit([&] { + if (nullptr != szSource) + { + delete[] szSource; + szSource = nullptr; + } + }); + + auto cleanupExpectedTarget = wil::scope_exit([&] { + if (nullptr != szExpectedTarget) + { + delete[] szExpectedTarget; + szExpectedTarget = nullptr; + } + }); + + auto cleanupExeName = wil::scope_exit([&] { + if (nullptr != szExeName) + { + delete[] szExeName; + szExeName = nullptr; + } + }); + + ConvertWToA(strSource, &szSource); + ConvertWToA(strExpectedTarget, &szExpectedTarget); + ConvertWToA(strExeName, &szExeName); + + TestGetConsoleAliasHelperA(szSource, + szExpectedTarget, + szExeName, + dwSource, + dwTarget, + dwExeName, + bUnicode, + bSetFirst); + + } +} diff --git a/src/host/ft_host/API_AliasTestsHelpers.hpp b/src/host/ft_host/API_AliasTestsHelpers.hpp new file mode 100644 index 000000000..e79bb7810 --- /dev/null +++ b/src/host/ft_host/API_AliasTestsHelpers.hpp @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#ifdef TestGetConsoleAliasHelper +#undef TestGetConsoleAliasHelper +#endif + +#ifdef TCH +#undef TCH +#endif + +#ifdef TLEN +#undef TLEN +#endif + +#ifdef AddConsoleAliasT +#undef AddConsoleAliasT +#endif + +#ifdef StringCbCopyT +#undef StringCbCopyT +#endif + +#ifdef GetConsoleAliasT +#undef GetConsoleAliasT +#endif + +#ifdef TSTRFORMAT +#undef TSTRFORMAT +#endif + +#ifdef TCHFORMAT +#undef TCHFORMAT +#endif + +#if defined(UNICODE) || defined(_UNICODE) +#define TestGetConsoleAliasHelper TestGetConsoleAliasHelperW +#define TCH wchar_t +#define TLEN wcslen +#define AddConsoleAliasT OneCoreDelay::AddConsoleAliasW +#define StringCbCopyT StringCbCopyW +#define GetConsoleAliasT OneCoreDelay::GetConsoleAliasW +#define TSTRFORMAT L"%s" +#define TCHFORMAT L"%c" +#else +#define TestGetConsoleAliasHelper TestGetConsoleAliasHelperA +#define TCH char +#define TLEN strlen +#define AddConsoleAliasT OneCoreDelay::AddConsoleAliasA +#define StringCbCopyT StringCbCopyA +#define GetConsoleAliasT OneCoreDelay::GetConsoleAliasA +#define TSTRFORMAT L"%S" +#define TCHFORMAT L"%C" +#endif + +void TestGetConsoleAliasHelper(TCH* ptszSourceGiven, + TCH* ptszExpectedTargetGiven, + TCH* ptszExeNameGiven, + DWORD& dwSource, + DWORD& dwTarget, + DWORD& dwExeName, + bool& /*bUnicode*/, + bool& bSetFirst) +{ + TCH* ptszSource = nullptr; + TCH* ptszExeName = nullptr; + TCH* ptszExpectedTarget = ptszExpectedTargetGiven; + TCH* ptchTargetBuffer = nullptr; + DWORD cbTargetBuffer = 0; + + switch (dwSource) + { + case 0: + ptszSource = nullptr; + Log::Comment(L"Using null source arg."); + break; + case 1: + ptszSource = ptszSourceGiven; + Log::Comment(String().Format(L"Using source arg: '" TSTRFORMAT "'", ptszSource)); + break; + default: + VERIFY_FAIL(L"Unknown type."); + } + + switch (dwExeName) + { + case 0: + ptszExeName = nullptr; + Log::Comment(L"Using null exe name."); + break; + case 1: + ptszExeName = ptszExeNameGiven; + Log::Comment(String().Format(L"Using exe name arg: '" TSTRFORMAT "'", ptszExeName)); + break; + default: + VERIFY_FAIL(L"Unknown type."); + } + + DWORD const cbExpectedTargetString = (DWORD)TLEN(ptszExpectedTargetGiven) * sizeof(TCH); + + switch (dwTarget) + { + case 0: + cbTargetBuffer = 0; + break; + case 1: + cbTargetBuffer = sizeof(TCH); + break; + case 2: + cbTargetBuffer = cbExpectedTargetString - sizeof(TCH); + break; + case 3: + cbTargetBuffer = cbExpectedTargetString; + break; + case 4: + cbTargetBuffer = cbExpectedTargetString + sizeof(TCH); + break; + case 5: + cbTargetBuffer = cbExpectedTargetString + sizeof(TCH) + sizeof(TCH); + break; + case 6: + cbTargetBuffer = MAX_PATH * sizeof(TCH); + break; + default: + VERIFY_FAIL(L"Unknown type."); + } + + if (cbTargetBuffer == 0) + { + ptchTargetBuffer = nullptr; + } + else + { + ptchTargetBuffer = new TCH[cbTargetBuffer / sizeof(TCH)]; + ZeroMemory(ptchTargetBuffer, cbTargetBuffer); + } + + auto freeTargetBuffer = wil::scope_exit([&]() + { + if (ptchTargetBuffer != nullptr) + { + delete[] ptchTargetBuffer; + } + }); + + Log::Comment(String().Format(L"Using target buffer size: '%d'", cbTargetBuffer)); + + // Set the alias if we're supposed to and prepare for cleanup later. + if (bSetFirst) + { + AddConsoleAliasT(ptszSource, ptszExpectedTarget, ptszExeName); + } + // This is strange because it's a scope exit so we need to declare in the parent scope, then let it go if we didn't actually need it. + // I just prefer keeping the exit next to the allocation so it doesn't get lost. + auto removeAliasOnExit = wil::scope_exit([&] { + AddConsoleAliasT(ptszSource, NULL, ptszExeName); + }); + if (!bSetFirst) + { + removeAliasOnExit.release(); + } + + // Determine what the result codes should be + // See console client side in conlibk... + // a->TargetLength on the server side will become the return value + // The returned status will be put into SetLastError + // If there is an error and it's not STATUS_BUFFER_TOO_SMALL, then a->TargetLength (and the return) will be zeroed. + // Some sample errors: + // - 87 = 0x57 = ERROR_INVALID_PARAMETER + // - 122 = 0x7a = ERROR_INSUFFICIENT_BUFFER + + DWORD dwExpectedResult; + DWORD dwExpectedLastError; + + // NOTE: This order is important. Don't rearrange IF statements. + if (nullptr == ptszSource || + nullptr == ptszExeName) + { + // If the source or exe name aren't valid, invalid parameter. + dwExpectedResult = 0; + dwExpectedLastError = ERROR_INVALID_PARAMETER; + } + else if (!bSetFirst) + { + // If we didn't set an alias, generic failure. + dwExpectedResult = 0; + dwExpectedLastError = ERROR_GEN_FAILURE; + } + else if (ptchTargetBuffer == nullptr || + cbTargetBuffer < (cbExpectedTargetString + sizeof(TCH))) // expected target plus a null terminator. + { + // If the target isn't enough space, insufficient buffer. + dwExpectedResult = cbTargetBuffer; + + // For some reason, the console API *ALWAYS* says it needs enough space as if we were copying Unicode, + // even if the final result will be ANSI. + // Therefore, if we're mathing based on a char size buffer, multiple the expected result by 2. + #pragma warning(suppress:4127) // This is a constant, but conditionally compiled twice so we need the check. + if (1 == sizeof(TCH)) + { + dwExpectedResult *= sizeof(wchar_t); + } + + dwExpectedLastError = ERROR_INSUFFICIENT_BUFFER; + } + else + { + // Otherwise, success. API should always null terminate string. + dwExpectedResult = cbExpectedTargetString + sizeof(TCH); // expected target plus a null terminator. + dwExpectedLastError = 0; + } + + TCH* ptchExpectedTarget; + auto freeExpectedTarget = wil::scope_exit([&] { + if (ptchExpectedTarget != nullptr) + { + delete[] ptchExpectedTarget; + ptchExpectedTarget = nullptr; + } + }); + + if (0 == cbTargetBuffer) + { + // If no buffer, we should expect null back out. + ptchExpectedTarget = nullptr; + } + else + { + // If there is buffer space, allocate it. + ptchExpectedTarget = new TCH[cbTargetBuffer / sizeof(TCH)]; + ZeroMemory(ptchExpectedTarget, cbTargetBuffer); + } + + if (0 == dwExpectedLastError) + { + // If it was successful, it should have been filled. Otherwise it will be zeroed as when it started. + StringCbCopyT(ptchExpectedTarget, cbTargetBuffer, ptszExpectedTargetGiven); + } + + // Perform the test + SetLastError(S_OK); + DWORD const dwActualResult = GetConsoleAliasT(ptszSource, ptchTargetBuffer, cbTargetBuffer, ptszExeName); + DWORD const dwActualLastError = GetLastError(); + + VERIFY_ARE_EQUAL(dwExpectedResult, dwActualResult, L"Ensure result code/return value matches expected."); + VERIFY_ARE_EQUAL(dwExpectedLastError, dwActualLastError, L"Ensure last error code matches expected."); + + Log::Comment(L"Compare target buffer character by character..."); + for (size_t i = 0; i < (cbTargetBuffer / sizeof(TCH)); i++) + { + if (ptchExpectedTarget[i] != ptchTargetBuffer[i]) + { + VERIFY_FAIL(String().Format(L"Target mismatch at %d. Expected: '" TCHFORMAT "' Actual: '" TCHFORMAT "'", i, ptchExpectedTarget[i], ptchTargetBuffer[i])); + } + } +} diff --git a/src/host/ft_host/API_BufferTests.cpp b/src/host/ft_host/API_BufferTests.cpp new file mode 100644 index 000000000..c48dd9e03 --- /dev/null +++ b/src/host/ft_host/API_BufferTests.cpp @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test boundary conditions for: +// SetConsoleActiveScreenBuffer +class BufferTests +{ + BEGIN_TEST_CLASS(BufferTests) + TEST_CLASS_PROPERTY(L"IsolationLevel", L"Method") + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + + TEST_METHOD(TestSetConsoleActiveScreenBufferInvalid); + + BEGIN_TEST_METHOD(TestWritingInactiveScreenBuffer) + TEST_METHOD_PROPERTY(L"Data:UseVtOutput", L"{true, false}") + END_TEST_METHOD() + + TEST_METHOD(ScrollLargeBufferPerformance); +}; + +void BufferTests::TestSetConsoleActiveScreenBufferInvalid() +{ + VERIFY_WIN32_BOOL_FAILED(SetConsoleActiveScreenBuffer(INVALID_HANDLE_VALUE)); + VERIFY_WIN32_BOOL_FAILED(SetConsoleActiveScreenBuffer(nullptr)); +} + +void BufferTests::TestWritingInactiveScreenBuffer() +{ + bool useVtOutput; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"UseVtOutput", useVtOutput), L"Get whether this test should check VT output mode."); + + const std::wstring primary(L"You should see me"); + const std::wstring alternative(L"You should NOT see me!"); + const std::wstring newline(L"\n"); + + Log::Comment(L"Set up the output mode to either use VT processing or not (see test parameter)"); + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(out, &mode)); + WI_UpdateFlag(mode, ENABLE_VIRTUAL_TERMINAL_PROCESSING, useVtOutput); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(out, mode)); + + Log::Comment(L"Write one line of text to the active/main output buffer."); + DWORD written = 0; + // Ok in legacy mode, ok in modern mode + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleW(out, primary.data(), gsl::narrow(primary.size()), &written, nullptr)); + VERIFY_ARE_EQUAL(primary.size(), written); + + Log::Comment(L"Write a newline character to move the cursor down to the left most cell on the next line down."); + // write a newline too to move the cursor down + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleW(out, newline.data(), gsl::narrow(newline.size()), &written, nullptr)); + VERIFY_ARE_EQUAL(newline.size(), written); + + Log::Comment(L"Create an alternative backing screen buffer that we will NOT be setting as active."); + const auto handle = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr); + VERIFY_IS_NOT_NULL(handle); + + // Ok in legacy mode, NOT ok in modern mode. + Log::Comment(L"Try to write a second line of different text but to the alternative backing screen buffer."); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleW(handle, alternative.data(), gsl::narrow(alternative.size()), &written, nullptr)); + VERIFY_ARE_EQUAL(alternative.size(), written); + + std::unique_ptr primaryBuffer = std::make_unique(primary.size()); + std::unique_ptr alternativeBuffer = std::make_unique(alternative.size()); + + Log::Comment(L"Read the first line out of the main/visible screen buffer. It should contain the first thing we wrote."); + DWORD read = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterW(out, primaryBuffer.get(), gsl::narrow(primary.size()), { 0, 0 }, &read)); + VERIFY_ARE_EQUAL(primary.size(), read); + VERIFY_ARE_EQUAL(String(primary.data()), String(primaryBuffer.get(), gsl::narrow(primary.size()))); + + Log::Comment(L"Read the second line out of the main/visible screen buffer. It should be full of blanks. The second thing we wrote wasn't to this buffer so it shouldn't show."); + const std::wstring alternativeExpected(alternative.size(), L'\x20'); + read = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterW(out, alternativeBuffer.get(), gsl::narrow(alternative.size()), { 0, 1 }, &read)); + VERIFY_ARE_EQUAL(alternative.size(), read); + VERIFY_ARE_EQUAL(String(alternativeExpected.data()), String(alternativeBuffer.get(), gsl::narrow(alternative.size()))); + + Log::Comment(L"Now read the first line from the alternative/non-visible screen buffer. It should contain the second thing we wrote."); + read = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterW(handle, alternativeBuffer.get(), gsl::narrow(alternative.size()), { 0, 0 }, &read)); + VERIFY_ARE_EQUAL(alternative.size(), read); + VERIFY_ARE_EQUAL(String(alternative.data()), String(alternativeBuffer.get(), gsl::narrow(alternative.size()))); + +} + +void BufferTests::ScrollLargeBufferPerformance() +{ + // Cribbed from https://github.com/Microsoft/console/issues/279 issue report. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsPerfTest", L"true") + END_TEST_METHOD_PROPERTIES() + + const auto Out = GetStdHandle(STD_OUTPUT_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFO Info; + GetConsoleScreenBufferInfo(Out, &Info); + + // We need a large buffer + Info.dwSize.Y = 9999; + SetConsoleScreenBufferSize(Out, Info.dwSize); + + SetConsoleCursorPosition(Out, { 0, Info.dwSize.Y - 1 }); + Log::Comment(L"Working. Please wait..."); + + const auto count = 20; + + const auto WindowHeight = Info.srWindow.Bottom - Info.srWindow.Top + 1; + + // Set this to false to scroll the entire buffer. The issue will disappear! + const auto ScrollOnlyInvisibleArea = true; + + const SMALL_RECT Rect + { + 0, + 0, + Info.dwSize.X - 1, + static_cast(Info.dwSize.Y - (ScrollOnlyInvisibleArea ? WindowHeight : 0) - 1) + }; + + const CHAR_INFO CharInfo{ '^', Info.wAttributes }; + + const auto now = std::chrono::steady_clock::now(); + + // Scroll the buffer 1 line up several times + for (int i = 0; i != count; ++i) + { + ScrollConsoleScreenBuffer(Out, &Rect, nullptr, { 0, -1 }, &CharInfo); + } + + const auto delta = std::chrono::duration_cast(std::chrono::steady_clock::now() - now).count(); + + SetConsoleCursorPosition(Out, { 0, Info.dwSize.Y - 1 }); + Log::Comment(String().Format(L"%d calls took %d ms. Avg %d ms per call", count, delta, delta/count)); +} + diff --git a/src/host/ft_host/API_CursorTests.cpp b/src/host/ft_host/API_CursorTests.cpp new file mode 100644 index 000000000..7c9d55deb --- /dev/null +++ b/src/host/ft_host/API_CursorTests.cpp @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test: +// GetConsoleCursorInfo +// SetConsoleCursorInfo +// SetConsoleCursorPosition +class CursorTests +{ + BEGIN_TEST_CLASS(CursorTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl3.h") + END_TEST_CLASS() + + TEST_METHOD_SETUP(TestSetup); + TEST_METHOD_CLEANUP(TestCleanup); + + BEGIN_TEST_METHOD(TestGetSetConsoleCursorInfo) + // 0, max, boundaries, and a value in the middle + TEST_METHOD_PROPERTY(L"Data:dwSize", L"{0, 1, 50, 100, 101, 0xFFFFFFFF}") + // Both possible values + TEST_METHOD_PROPERTY(L"Data:bVisible", L"{TRUE, FALSE}") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestSetConsoleCursorPosition) + TEST_METHOD_PROPERTY(L"HostDestructive", L"True") + END_TEST_METHOD() +}; + +bool CursorTests::TestSetup() +{ + return Common::TestBufferSetup(); +} + +bool CursorTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void CursorTests::TestGetSetConsoleCursorInfo() +{ + DWORD dwSize; + bool bVisible; + + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"dwSize", dwSize), L"Get size parameter"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"bVisible", bVisible), L"Get visibility parameter"); + + // Get initial state of the cursor + CONSOLE_CURSOR_INFO cciInitial = { 0 }; + BOOL bResult = GetConsoleCursorInfo(Common::_hConsole, &cciInitial); + VERIFY_WIN32_BOOL_SUCCEEDED(bResult, L"Retrieve initial cursor state."); + + // Fill a structure with the value under test + CONSOLE_CURSOR_INFO cciTest = { 0 }; + cciTest.bVisible = bVisible; + cciTest.dwSize = dwSize; + + // If the cursor size is out of range, we expect a failure on set + BOOL fExpectedResult = TRUE; + if (cciTest.dwSize < 1 || cciTest.dwSize > 100) + { + fExpectedResult = FALSE; + } + + // Attempt to set and verify that we get the expected result + bResult = SetConsoleCursorInfo(Common::_hConsole, &cciTest); + VERIFY_ARE_EQUAL(bResult, fExpectedResult, L"Ensure that return matches success/failure state we were expecting."); + + // Get the state of the cursor again + CONSOLE_CURSOR_INFO cciReturned = { 0 }; + bResult = GetConsoleCursorInfo(Common::_hConsole, &cciReturned); + VERIFY_WIN32_BOOL_SUCCEEDED(bResult, L"GET back the cursor information we just set."); + + if (fExpectedResult) + { + // If we expected the set to be successful, the returned structure should match the test one + VERIFY_ARE_EQUAL(cciReturned, cciTest, L"If we expected SET success, the values we set should match what we retrieved."); + } + else + { + // If we expected the set to fail, the returned structure should match the initial one + VERIFY_ARE_EQUAL(cciReturned, cciInitial, L"If we expected SET failure, the initial values before the SET should match what we retrieved."); + } +} + +void TestSetConsoleCursorPositionImpl(WORD wCursorX, WORD wCursorY, BOOL bExpectedResult) +{ + COORD coordCursor; + coordCursor.X = wCursorX; + coordCursor.Y = wCursorY; + + // Get initial position data + CONSOLE_SCREEN_BUFFER_INFOEX sbiInitial = { 0 }; + sbiInitial.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + BOOL bResult = GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiInitial); + VERIFY_WIN32_BOOL_SUCCEEDED(bResult, L"Get the initial buffer data."); + + // Attempt to set cursor into valid area + bResult = SetConsoleCursorPosition(Common::_hConsole, coordCursor); + VERIFY_ARE_EQUAL(bResult, bExpectedResult, L"Ensure that return from SET matches success/failure state we were expecting."); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiTest = { 0 }; + sbiTest.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bResult = GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiTest); + VERIFY_WIN32_BOOL_SUCCEEDED(bResult, L"GET the values back to ensure they were set properly."); + + // Cursor is where it was set to if we were supposed to be successful + if (bExpectedResult) + { + VERIFY_ARE_EQUAL(coordCursor, sbiTest.dwCursorPosition, L"If SET was TRUE, we expect the cursor to be where we SET it."); + } + else + { + // otherwise, it's at where it was before + VERIFY_ARE_EQUAL(sbiInitial.dwCursorPosition, sbiTest.dwCursorPosition, L"If SET was FALSE, we expect the cursor to not have moved."); + } + + // Verify the viewport. + bool fViewportMoveExpected = false; + + // If we expected the cursor to be set successfully, the viewport might have moved. + if (bExpectedResult) + { + // If the position we set was outside the initial rectangle, then the viewport should have moved. + if (coordCursor.X > sbiInitial.srWindow.Right || + coordCursor.X < sbiInitial.srWindow.Left || + coordCursor.Y > sbiInitial.srWindow.Bottom || + coordCursor.Y < sbiInitial.srWindow.Top) + { + fViewportMoveExpected = true; + } + } + + if (fViewportMoveExpected) + { + // Something had to have changed in the viewport + VERIFY_ARE_NOT_EQUAL(sbiInitial.srWindow, sbiTest.srWindow, L"The viewports must have changed if we set the cursor outside the current area."); + } + else + { + VERIFY_ARE_EQUAL(sbiInitial.srWindow, sbiTest.srWindow, L"The viewports must remain the same if the cursor was set inside the existing one."); + } +} + +void CursorTests::TestSetConsoleCursorPosition() +{ + // Get initial buffer value for boundaries + CONSOLE_SCREEN_BUFFER_INFOEX sbiInitial = { 0 }; + sbiInitial.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + BOOL bResult = GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiInitial); + VERIFY_WIN32_BOOL_SUCCEEDED(bResult, L"Retrieve the initial buffer information to calculate the boundaries for testing."); + + // Try several cases + TestSetConsoleCursorPositionImpl(0, 0, TRUE); // Top left corner of buffer + TestSetConsoleCursorPositionImpl(sbiInitial.dwSize.X - 1, sbiInitial.dwSize.Y - 1, TRUE); // Bottom right corner of buffer + TestSetConsoleCursorPositionImpl(sbiInitial.dwSize.X, sbiInitial.dwSize.Y, FALSE); // 1 beyond bottom right corner (the size is 1 larger than the array indicies) + TestSetConsoleCursorPositionImpl(MAXWORD, MAXWORD, FALSE); // Max values +} diff --git a/src/host/ft_host/API_DimensionsTests.cpp b/src/host/ft_host/API_DimensionsTests.cpp new file mode 100644 index 000000000..90f0537ec --- /dev/null +++ b/src/host/ft_host/API_DimensionsTests.cpp @@ -0,0 +1,529 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test: +// GetConsoleScreenBufferInfo +// GetConsoleScreenBufferInfoEx +// GetLargestConsoleWindowSize +// SetConsoleScreenBufferInfoEx --> SetScreenBufferInfo internally +// SetConsoleScreenBufferSize +// SetConsoleWindowInfo +class DimensionsTests +{ + BEGIN_TEST_CLASS(DimensionsTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + + TEST_METHOD_SETUP(TestSetup); + TEST_METHOD_CLEANUP(TestCleanup); + + TEST_METHOD(TestGetLargestConsoleWindowSize); + TEST_METHOD(TestGetConsoleScreenBufferInfoAndEx); + + BEGIN_TEST_METHOD(TestSetConsoleWindowInfo) + // This needs to run in both absolute and relative modes. + TEST_METHOD_PROPERTY(L"Data:bAbsolute", L"{TRUE, FALSE}") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestSetConsoleScreenBufferSize) + // 0x1 = X, 0x2 = Y, 0x3 = Both + TEST_METHOD_PROPERTY(L"Data:scaleChoices", L"{1, 2, 3}") + END_TEST_METHOD() + + TEST_METHOD(TestZeroSizedConsoleScreenBuffers); + TEST_METHOD(TestSetConsoleScreenBufferInfoEx); +}; + +bool DimensionsTests::TestSetup() +{ + return Common::TestBufferSetup(); +} + +bool DimensionsTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void DimensionsTests::TestGetLargestConsoleWindowSize() +{ + if (!OneCoreDelay::IsIsWindowPresent()) + { + Log::Comment(L"Largest window size scenario can't be checked on platform without classic window operations."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + // Note that this API is named "window size" but actually refers to the maximum viewport. + // Viewport is defined as the character count that can fit within one client area of the window. + // It has nothing to do with the outer pixel dimensions of the window. + // To know the largest window size, we need: + // - The size of the monitor that the console window is on + // - The style of the window + // - The current size of the font used within that window + + // The "largest window size" is the maximum number of rows and columns worth of characters + // that can be displayed if the current console window was stretched as large as it is currently + // allowed to be on the given monitor. + + // NOTE: The legacy behavior of this function (in v1) was to give the "full screen window" size as the largest + // even if it was in windowed mode and wouldn't fit on the monitor. + + // Get the window handle + HWND const hWindow = GetConsoleWindow(); + VerifySucceededGLE(VERIFY_IS_TRUE(!!IsWindow(hWindow), L"Get the window handle for the window.")); + + // Get the dimensions of the monitor that the window is on. + HMONITOR const hMonitor = MonitorFromWindow(hWindow, MONITOR_DEFAULTTONULL); + VerifySucceededGLE(VERIFY_IS_NOT_NULL(hMonitor, L"Get the monitor handle corresponding to the console window.")); + + MONITORINFO mi = { 0 }; + mi.cbSize = sizeof(MONITORINFO); + VERIFY_WIN32_BOOL_SUCCEEDED(GetMonitorInfoW(hMonitor, &mi), L"Get monitor information for the handle."); + + // Get the styles for the window from the handle + DWORD const dwStyle = GetWindowStyle(hWindow); + DWORD const dwStyleEx = GetWindowExStyle(hWindow); + BOOL const bHasMenu = OneCoreDelay::GetMenu(hWindow) != nullptr; + + // Get the current font size + CONSOLE_FONT_INFO cfi; + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFont(Common::_hConsole, FALSE, &cfi), L"Get the current console font structure."); + + // Now use what we've learned to attempt to calculate the expected size + COORD coordExpected = { 0 }; + + RECT rcPixels = mi.rcWork; // start from the monitor work area as the maximum pixel size + + // we have to adjust the work area by the size of the window borders to compensate for a maximized window + // where the window manager will render the borders off the edges of the screen. + WINDOWINFO wi = { 0 }; + wi.cbSize = sizeof(WINDOWINFO); + VERIFY_WIN32_BOOL_SUCCEEDED(GetWindowInfo(hWindow, &wi), L"Get window information to obtain window border sizes."); + rcPixels.top -= wi.cyWindowBorders; + rcPixels.bottom += wi.cyWindowBorders; + rcPixels.left -= wi.cxWindowBorders; + rcPixels.right += wi.cxWindowBorders; + + UnadjustWindowRectEx(&rcPixels, dwStyle, bHasMenu, dwStyleEx); // convert outer window dimensions into client area size + + // Do not reserve space for scroll bars. + + // Now take width and height and divide them by the size of a character to get the max character count. + coordExpected.X = (SHORT)((rcPixels.right - rcPixels.left) / cfi.dwFontSize.X); + coordExpected.Y = (SHORT)((rcPixels.bottom - rcPixels.top) / cfi.dwFontSize.Y); + + // Now finally ask the console what it thinks its largest size should be and compare. + COORD const coordLargest = GetLargestConsoleWindowSize(Common::_hConsole); + VerifySucceededGLE(VERIFY_IS_NOT_NULL(coordLargest, L"Now ask what the console thinks the largest size should be.")); + + VERIFY_ARE_EQUAL(coordExpected, coordLargest, L"Compare what we calculated to what the console says the largest size should be."); +} + +void DimensionsTests::TestGetConsoleScreenBufferInfoAndEx() +{ + // Get both structures + CONSOLE_SCREEN_BUFFER_INFO sbi = { 0 }; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfo(Common::_hConsole, &sbi), L"Retrieve old-style buffer info."); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Retrieve extended buffer info."); + + Log::Comment(NoThrowString().Format(L"Verify overlapping values are the same between both call types.")); + + VERIFY_ARE_EQUAL(sbi.dwCursorPosition, sbiex.dwCursorPosition); + VERIFY_ARE_EQUAL(sbi.dwMaximumWindowSize, sbi.dwMaximumWindowSize); + VERIFY_ARE_EQUAL(sbi.dwSize, sbiex.dwSize); + VERIFY_ARE_EQUAL(sbi.srWindow, sbiex.srWindow); + VERIFY_ARE_EQUAL(sbi.wAttributes, sbiex.wAttributes); +} + +void ConvertAbsoluteToRelative(bool const bAbsolute, SMALL_RECT* const srViewport, const SMALL_RECT* const srOriginalWindow) +{ + if (!bAbsolute) + { + srViewport->Left -= srOriginalWindow->Left; + srViewport->Right -= srOriginalWindow->Right; + srViewport->Top -= srOriginalWindow->Top; + srViewport->Bottom -= srOriginalWindow->Bottom; + } +} + +void TestSetConsoleWindowInfoHelper(bool const bAbsolute, + const SMALL_RECT* const srViewport, + const SMALL_RECT* const srOriginalViewport, + bool const bExpectedResult, + PCWSTR pwszDescription) +{ + SMALL_RECT srTest = *srViewport; + + ConvertAbsoluteToRelative(bAbsolute, &srTest, srOriginalViewport); + + Log::Comment(NoThrowString().Format(L"Abs:%s Original:%s Viewport:%s", + bAbsolute ? L"True" : L"False", + VerifyOutputTraits::ToString(*srOriginalViewport).ToCStrWithFallbackTo(L"Fail To Display SMALL_RECT"), + VerifyOutputTraits::ToString(srTest).ToCStrWithFallbackTo(L"Fail To Display SMALL_RECT"))); + + if (bExpectedResult) + { + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(Common::_hConsole, bAbsolute, &srTest), pwszDescription); + } + else + { + VERIFY_WIN32_BOOL_FAILED(SetConsoleWindowInfo(Common::_hConsole, bAbsolute, &srTest), pwszDescription); + } +} + +void DimensionsTests::TestSetConsoleWindowInfo() +{ + bool bAbsolute; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"bAbsolute", bAbsolute), L"Get absolute vs. relative parameter"); + + // Get window and buffer information + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Get initial buffer and window information."); + + SMALL_RECT srViewport = { 0 }; + + // Test with and without absolute + // Left > Right, Top > Bottom (INVALID) + srViewport.Right = sbiex.srWindow.Left; + srViewport.Left = sbiex.srWindow.Right; + srViewport.Bottom = sbiex.srWindow.Top; + srViewport.Top = sbiex.srWindow.Bottom; + + TestSetConsoleWindowInfoHelper(bAbsolute, &srViewport, &sbiex.srWindow, false, L"Ensure Left > Right, Top > Bottom is marked invalid."); + + // Window greater than, equal to and less than the max client window + // Window > Max ( INVALID ) + srViewport.Left = 0; + srViewport.Top = 0; + srViewport.Right = sbiex.dwMaximumWindowSize.X; // this is 1 larger than the valid right bound since it's 0-based array indexes + srViewport.Bottom = sbiex.dwMaximumWindowSize.Y; + + TestSetConsoleWindowInfoHelper(bAbsolute, &srViewport, &sbiex.srWindow, false, L"Ensure window larger than max is marked invalid."); + + // Set to same position we were just at (full screen or not) + // VALID, SUCCESS + + srViewport = sbiex.srWindow; + + TestSetConsoleWindowInfoHelper(bAbsolute, &srViewport, &sbiex.srWindow, true, L"Set to the original window size"); + TestSetConsoleWindowInfoHelper(bAbsolute, &srViewport, &sbiex.srWindow, true, L"Confirm that setting it again to the same position works."); + + // Will fail while in full screen, but no current way to set that mode externally. :( + + // Finally, check roundtrip by changing window. + srViewport = sbiex.srWindow; + srViewport.Left += 1; + srViewport.Right -= 1; + srViewport.Top += 1; + srViewport.Bottom -= 1; + + // Verify the assumption that the viewport was sufficiently large to shrink it in the above manner. + if (srViewport.Left > srViewport.Right || + srViewport.Top > srViewport.Bottom || + (srViewport.Right - srViewport.Left) < 1 || + (srViewport.Bottom - srViewport.Top) < 1) + { + VERIFY_FAIL(NoThrowString().Format(L"Adjusted viewport is invalid. %s", VerifyOutputTraits::ToString(srViewport).GetBuffer())); + } + + // Store a copy of the original (for comparison in case the relative translation is applied). + SMALL_RECT const srViewportBefore = srViewport; + + TestSetConsoleWindowInfoHelper(bAbsolute, &srViewport, &sbiex.srWindow, true, L"Attempt shrinking the window in a valid manner."); + + // Get it back and ensure it's the same dimensions + CONSOLE_SCREEN_BUFFER_INFO sbi = { 0 }; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfo(Common::_hConsole, &sbi), L"Confirm the size we specified round-trips through to the Get API."); + + VERIFY_ARE_EQUAL(srViewportBefore, sbi.srWindow, L"Match before and after viewport sizes."); +} + +void RestrictDimensionsHelper(COORD* const coordTest, SHORT const x, SHORT const y, bool const fUseX, bool const fUseY) +{ + if (fUseX) + { + coordTest->X = x; + } + + if (fUseY) + { + coordTest->Y = y; + } +} + +void DimensionsTests::TestSetConsoleScreenBufferSize() +{ + DWORD dwMode = { 0 }; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"scaleChoices", dwMode), L"Get active mode"); + + bool fAdjustX = false; + bool fAdjustY = false; + + if ((dwMode & 0x1) != 0) + { + fAdjustX = true; + Log::Comment(L"Adjusting X dimension"); + } + if ((dwMode & 0x2) != 0) + { + fAdjustY = true; + Log::Comment(L"Adjusting Y dimension"); + } + + + CONSOLE_SCREEN_BUFFER_INFO sbi = { 0 }; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfo(Common::_hConsole, &sbi), L"Get initial buffer/window information."); + + COORD coordSize = { 0 }; + + // Ensure buffer size cannot be smaller than minimum + RestrictDimensionsHelper(&coordSize, 0, 0, fAdjustX, fAdjustY); + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Set buffer size to smaller than minimum possible."); + + // Ensure buffer size cannot be excessively large. + RestrictDimensionsHelper(&coordSize, SHRT_MAX, SHRT_MAX, fAdjustX, fAdjustY); + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Set buffer size to very, very large."); + + // Ensure buffer size cannot be excessively small (negative). + RestrictDimensionsHelper(&coordSize, SHRT_MIN, SHRT_MIN, fAdjustX, fAdjustY); + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Set buffer size to negative values."); + + // Ensure success on giving the same size back that we started with + coordSize = sbi.dwSize; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Set it to the same size as initial."); + + // save the dimensions of the window for use in tests relative to window size + COORD coordWindowDim; + coordWindowDim.X = sbi.srWindow.Right - sbi.srWindow.Left; + coordWindowDim.Y = sbi.srWindow.Bottom - sbi.srWindow.Top; + + // Ensure buffer size cannot be smaller than the window + coordSize = coordWindowDim; + RestrictDimensionsHelper(&coordSize, coordSize.X - 1, coordSize.Y - 1, fAdjustX, fAdjustY); + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Try to make buffer smaller than the window size."); + + // Success on setting a buffer larger than the window + coordSize = coordWindowDim; + coordSize.X++; + coordSize.Y++; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(Common::_hConsole, coordSize), L"Try to make buffer larger than the window size."); +} + +void DimensionsTests::TestZeroSizedConsoleScreenBuffers() +{ + // Make sure we never accept zero-sized console buffers through the public API + const COORD rgTestCoords[] = { + { 0, 0 }, + { 0, 1 }, + { 1, 0 } + }; + + for (size_t i = 0; i < ARRAYSIZE(rgTestCoords); i++) + { + const BOOL fSucceeded = SetConsoleScreenBufferSize(Common::_hConsole, rgTestCoords[i]); + VERIFY_IS_FALSE(!!fSucceeded, + NoThrowString().Format(L"Setting zero console size should always fail (x: %d y:%d)", + rgTestCoords[i].X, rgTestCoords[i].Y)); + VERIFY_ARE_EQUAL((DWORD)ERROR_INVALID_PARAMETER, GetLastError()); + } +} + +template +void TestSetConsoleScreenBufferInfoExHelper(bool const fShouldHaveChanged, + T const pOriginal, + T const pTest, + T const pReturned, + PCWSTR pwszDescriptor) +{ + if (fShouldHaveChanged) + { + VERIFY_ARE_EQUAL(pTest, pReturned, NoThrowString().Format(L"Verify %s has changed to match the test value.", pwszDescriptor)); + VERIFY_ARE_NOT_EQUAL(pOriginal, pReturned, NoThrowString().Format(L"Verify %s does not match original value.", pwszDescriptor)); + } + else + { + VERIFY_ARE_NOT_EQUAL(pTest, pReturned, NoThrowString().Format(L"Verify %s has NOT changed to match the test value.", pwszDescriptor)); + VERIFY_ARE_EQUAL(pOriginal, pReturned, NoThrowString().Format(L"Verify %s DOES match original value.", pwszDescriptor)); + } +} + +void DimensionsTests::TestSetConsoleScreenBufferInfoEx() +{ + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + + // = cbSize + // Attributes = wAttributes + // ColorTable = ColorTable + // CursorPosition = dwCursorPosition + // FullscreenSupported = bFullscreenSupported + // MaximumWindowSize = dwMaximumWindowSize + // PopupAttributes = wPopupAttributes + // Size = dwSize + + // combine to make srWindow. Translated inside the driver \minkernel\console\client\getset.c + // CurrentWindowSize + // ScrollPosition + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Get original buffer state."); + + // save a copy for the final comparison. + CONSOLE_SCREEN_BUFFER_INFOEX const sbiexOriginal = sbiex; + + // check invalid values of viewport size + sbiex = sbiexOriginal; + sbiex.dwSize.X = 0; + sbiex.dwSize.Y = 0; + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Try 0x0 viewport size."); + + sbiex = sbiexOriginal; + sbiex.dwSize.X = MAXSHORT; + sbiex.dwSize.Y = MAXSHORT; + VERIFY_WIN32_BOOL_FAILED(SetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Try MAX by MAX viewport size."); + + + // Fill the entire structure with new data and set + sbiex.dwSize.X = 200; + sbiex.dwSize.Y = 5555; + + sbiex.srWindow.Left = 0; + sbiex.srWindow.Right = 79; + sbiex.srWindow.Top = 0; + sbiex.srWindow.Bottom = 49; + + sbiex.wAttributes = BACKGROUND_BLUE | BACKGROUND_INTENSITY | FOREGROUND_RED; + sbiex.wPopupAttributes = BACKGROUND_GREEN | FOREGROUND_RED; + + sbiex.ColorTable[0] = 0x0000000F; + sbiex.ColorTable[1] = 0x000000F0; + sbiex.ColorTable[2] = 0x00000F00; + sbiex.ColorTable[3] = 0x0000F000; + sbiex.ColorTable[4] = 0x000F0000; + sbiex.ColorTable[5] = 0x00F00000; + sbiex.ColorTable[6] = 0x000000FF; + sbiex.ColorTable[7] = 0x00000FF0; + sbiex.ColorTable[8] = 0x0000FF00; + sbiex.ColorTable[9] = 0x000FF000; + sbiex.ColorTable[10] = 0x00FF0000; + sbiex.ColorTable[11] = 0x00000FFF; + sbiex.ColorTable[12] = 0x0000FFF0; + sbiex.ColorTable[13] = 0x000FFF00; + sbiex.ColorTable[14] = 0x00FFF000; + sbiex.ColorTable[15] = 0x0000FFFF; + + sbiex.dwMaximumWindowSize.X = 100; + sbiex.dwMaximumWindowSize.Y = 80; + + sbiex.bFullscreenSupported = !sbiex.bFullscreenSupported; // set to opposite + + // DO NOT TRY TO SET THE CURSOR. It may or may not be in the same place. The Set API actually never obeyed the request + // to set the position and we can't fix it now. + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiex), L"Attempt to set structure with all new data."); + + // Confirm that the prompt stored settings as appropriate + CONSOLE_SCREEN_BUFFER_INFOEX sbiexAfter = { 0 }; + sbiexAfter.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiexAfter), L"Retrieve set data with get."); + + // Verify that relevant properties were stored into the console. + + // The buffer size is weird because there are currently two valid answers. + // This is due to the word wrap status of the console which is currently not visible through the API. + // We must accept either answer as valid. + bool fBufferSizePassed = false; + + // 1. The buffer size we set matches exactly with what we retrieved after it was done. (classic behavior, no word wrap) + if (VerifyCompareTraits().AreEqual(sbiex.dwSize, sbiexAfter.dwSize)) + { + fBufferSizePassed = true; + } + + // 2. The buffer size is restricted/pegged to the width (X dimension) of the window. (new behavior, word wrap) + short sWidthLimit = (sbiex.srWindow.Right - sbiex.srWindow.Left) + 1; // the right index counts as valid, so right - left + 1 for total width. + + // 2a. Width expected might be reduced if the buffer is taller than the window. If so, reduce by a scroll bar in width. + if (sbiex.dwSize.Y > ((sbiex.srWindow.Bottom - sbiex.srWindow.Top) + 1)) // the bottom index counts as valid, so bottom - top + 1 for total height. + { + // Get pixel size of a vertical scroll bar. + short const sVerticalScrollWidthPx = (SHORT)GetSystemMetrics(SM_CXVSCROLL); + + // Get the current font size + CONSOLE_FONT_INFO cfi; + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFont(Common::_hConsole, FALSE, &cfi), L"Get the current console font structure."); + + if (VERIFY_ARE_NOT_EQUAL(0, cfi.dwFontSize.X, L"Verify that the font width is not zero or we'll have a division error.")) + { + // Figure out how many character widths to reduce by. + short sReduceBy = 0; + + // Divide the size of a scroll bar by the font widths. + sReduceBy = sVerticalScrollWidthPx / cfi.dwFontSize.X; + + // If there is a remainder, add one more. We can't render partial characters. + sReduceBy += sVerticalScrollWidthPx % cfi.dwFontSize.X ? 1 : 0; + + // Subtract the number of characters being reserved for the scroll bar. + sWidthLimit -= sReduceBy; + } + } + + // 2b. Do the comparison. Y should be correct, but X will be the lesser of the size we asked for or the window limit for word wrap. + if (sbiex.dwSize.Y == sbiexAfter.dwSize.Y && min(sbiex.dwSize.X, sWidthLimit) == sbiexAfter.dwSize.X) + { + fBufferSizePassed = true; + } + VERIFY_IS_TRUE(fBufferSizePassed, L"Verify Buffer Size has changed as expected."); + + // Test remaining parameters are the same + TestSetConsoleScreenBufferInfoExHelper(true, sbiexOriginal.wAttributes, sbiex.wAttributes, sbiexAfter.wAttributes, L"Attributes (Fg/Bg Colors)"); + TestSetConsoleScreenBufferInfoExHelper(true, sbiexOriginal.wPopupAttributes, sbiex.wPopupAttributes, sbiexAfter.wPopupAttributes, L"Popup Attributes (Fg/Bg Colors)"); + + // verify colors match + for (UINT i = 0; i < 16; i++) + { + TestSetConsoleScreenBufferInfoExHelper(true, sbiexOriginal.ColorTable[i], sbiex.ColorTable[i], sbiexAfter.ColorTable[i], NoThrowString().Format(L"Color %x", i)); + } + + // NOTE: Max window size and the positioning of the window are adjusted at the discretion of the console. + // They will not necessarily match, so we're not testing them. + + // NOTE: Full screen will NOT be changed by this API and should match the originals. + TestSetConsoleScreenBufferInfoExHelper(false, sbiexOriginal.bFullscreenSupported, sbiex.bFullscreenSupported, sbiexAfter.bFullscreenSupported, L"Fullscreen"); + + // NOTE: Ignore cursor position. It can change or not depending on the word wrap mode and the API set doesn't do anything. + + // BUG: This is a long standing bug in the console which some of our customers have documented on the MSDN page. + // The console driver (\minkernel\console\client\getset.c) is treating the viewport as an "exclusive" rectangle where it is actually "inclusive" + // of its edges. This means when it does a width calculation, it has an off-by-one error and will shrink the window in height and width by 1 each + // trip around. For example, normally we do viewport width as Right-Left+1, and the driver does it as Right-Left. + // As this has lasted so long, it's likely a compat issue to fix now. So we'll leave it in and compensate for it in the test here. + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms686039(v=vs.85).aspx + CONSOLE_SCREEN_BUFFER_INFOEX sbiexBug = sbiexOriginal; + sbiexBug.srWindow.Bottom++; + sbiexBug.srWindow.Right++; + + // Restore original settings + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiexBug), L"Restore original settings."); + + // Ensure originals are restored. + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(Common::_hConsole, &sbiexAfter), L"Retrieve what we just set."); + + // NOTE: Set the two cursor positions to the same thing because we don't care to compare them. They can + // be different or they may not be different. The SET API doesn't actually work so it depends on the other state, + // which we're not measuring now. + sbiexAfter.dwCursorPosition = sbiexOriginal.dwCursorPosition; + + VERIFY_ARE_EQUAL(sbiexAfter, sbiexOriginal, L"Ensure settings are back to original values."); +} diff --git a/src/host/ft_host/API_FileTests.cpp b/src/host/ft_host/API_FileTests.cpp new file mode 100644 index 000000000..ef7cbc5ac --- /dev/null +++ b/src/host/ft_host/API_FileTests.cpp @@ -0,0 +1,859 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "Common.hpp" + +#include + +// This class is intended to test: +// WriteFile + +class FileTests +{ + // Method isolation level will completely close and re-open the OpenConsole session for every + // TEST_METHOD below. This saves us the time of cleaning up the mode state and the contents of + // the buffer and cursor position for each test. Launching a new OpenConsole is much quicker. + BEGIN_TEST_CLASS(FileTests) + TEST_CLASS_PROPERTY(L"IsolationLevel", L"Method") + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS(); + + TEST_METHOD(TestUtf8WriteFileInvalid); + + TEST_METHOD(TestWriteFileRaw); + TEST_METHOD(TestWriteFileProcessed); + + BEGIN_TEST_METHOD(TestWriteFileWrapEOL) + TEST_METHOD_PROPERTY(L"Data:fFlagOn", L"{true, false}") + END_TEST_METHOD(); + + BEGIN_TEST_METHOD(TestWriteFileVTProcessing) + TEST_METHOD_PROPERTY(L"Data:fVtOn", L"{true, false}") + TEST_METHOD_PROPERTY(L"Data:fProcessedOn", L"{true, false}") + END_TEST_METHOD(); + + BEGIN_TEST_METHOD(TestWriteFileDisableNewlineAutoReturn) + TEST_METHOD_PROPERTY(L"Data:fDisableAutoReturn", L"{true, false}") + TEST_METHOD_PROPERTY(L"Data:fProcessedOn", L"{true, false}") + END_TEST_METHOD(); + + TEST_METHOD(TestWriteFileSuspended); + + TEST_METHOD(TestReadFileBasic); + TEST_METHOD(TestReadFileBasicSync); + TEST_METHOD(TestReadFileBasicEmpty); + TEST_METHOD(TestReadFileLine); + TEST_METHOD(TestReadFileLineSync); + + TEST_CLASS_SETUP(ClassSetup); + TEST_CLASS_CLEANUP(ClassCleanup); + + TEST_METHOD_SETUP(MethodSetup); + TEST_METHOD_CLEANUP(MethodCleanup); + + /*BEGIN_TEST_METHOD(TestReadFileEcho) + TEST_METHOD_PROPERTY(L"Data:fUseBlockedRead", L"{true, false}") + END_TEST_METHOD();*/ +}; + +static HANDLE _cancellationEvent = 0; + +bool FileTests::ClassSetup() +{ + _cancellationEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + VERIFY_WIN32_BOOL_SUCCEEDED(!!_cancellationEvent, L"Create cancellation event."); + return true; +} + +bool FileTests::ClassCleanup() +{ + VERIFY_WIN32_BOOL_SUCCEEDED(CloseHandle(_cancellationEvent), L"Cleanup cancellation event."); + return true; +} + +bool FileTests::MethodSetup() +{ + VERIFY_WIN32_BOOL_SUCCEEDED(ResetEvent(_cancellationEvent), L"Reset cancellation event."); + return true; +} + +bool FileTests::MethodCleanup() +{ + VERIFY_WIN32_BOOL_SUCCEEDED(SetEvent(_cancellationEvent), L"Set cancellation event."); + return true; +} + +void FileTests::TestUtf8WriteFileInvalid() +{ + Log::Comment(L"Backup original console codepage."); + UINT const uiOriginalCP = GetConsoleOutputCP(); + auto restoreOriginalCP = wil::scope_exit([&] { + Log::Comment(L"Restore original console codepage."); + SetConsoleOutputCP(uiOriginalCP); + }); + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(CP_UTF8), L"Set output codepage to UTF8"); + + DWORD dwWritten; + DWORD dwExpectedWritten; + char* str; + DWORD cbStr; + + // \x80 is an invalid UTF-8 continuation + // \x40 is the @ symbol which is valid. + str = "\x80\x40"; + cbStr = (DWORD)strlen(str); + dwWritten = 0; + dwExpectedWritten = cbStr; + + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr)); + VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten); + + // \x80 is an invalid UTF-8 continuation + // \x40 is the @ symbol which is valid. + str = "\x80\x40\x40"; + cbStr = (DWORD)strlen(str); + dwWritten = 0; + dwExpectedWritten = cbStr; + + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr)); + VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten); + + // \x80 is an invalid UTF-8 continuation + // \x40 is the @ symbol which is valid. + str = "\x80\x80\x80\x40"; + cbStr = (DWORD)strlen(str); + dwWritten = 0; + dwExpectedWritten = cbStr; + + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr)); + VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten); +} + +void FileTests::TestWriteFileRaw() +{ + // \x7 is bell + // \x8 is backspace + // \x9 is tab + // \xa is linefeed + // \xd is carriage return + // All should be ignored/printed in raw mode. + PCSTR strTest = "z\x7y\x8z\x9y\xaz\xdy"; + DWORD const cchTest = (DWORD)strlen(strTest); + String strReadBackExpected(strTest); + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 }; + csbiexBefore.cbSize = sizeof(csbiexBefore); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Retrieve screen buffer properties before writing."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, 0), L"Set raw write mode."); + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexBefore.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + DWORD dwWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, strTest, cchTest, &dwWritten, nullptr), L"Write text into buffer using WriteFile"); + VERIFY_ARE_EQUAL(cchTest, dwWritten); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; + csbiexAfter.cbSize = sizeof(csbiexAfter); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Retrieve screen buffer properties after writing."); + + csbiexBefore.dwCursorPosition.X += (SHORT)cchTest; + VERIFY_ARE_EQUAL(csbiexBefore.dwCursorPosition, csbiexAfter.dwCursorPosition, L"Verify cursor moved expected number of squares for the write length."); + + DWORD const cbReadBackBuffer = cchTest + 2; // +1 so we can read back a "space" that should be after what we wrote. +1 more so this can be null terminated for String class comparison. + wistd::unique_ptr strReadBack = wil::make_unique_failfast(cbReadBackBuffer); + ZeroMemory(strReadBack.get(), cbReadBackBuffer * sizeof(char)); + + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, strReadBack.get(), cchTest + 1, coordZero, &dwRead), L"Read back the data in the buffer."); + // +1 to read back the space that should be after the text we wrote + + strReadBackExpected += " "; // add in the space that should appear after the written text (buffer should be space filled when empty) + + VERIFY_ARE_EQUAL(strReadBackExpected, String(strReadBack.get()), L"Ensure that the buffer contents match what we expected based on what we wrote."); +} + +void WriteFileHelper(HANDLE hOut, + CONSOLE_SCREEN_BUFFER_INFOEX& csbiexBefore, + CONSOLE_SCREEN_BUFFER_INFOEX& csbiexAfter, + PCSTR psTest, + DWORD cchTest) +{ + csbiexBefore.cbSize = sizeof(csbiexBefore); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Retrieve screen buffer properties before writing."); + + DWORD dwWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, psTest, cchTest, &dwWritten, nullptr), L"Write text into buffer using WriteFile"); + VERIFY_ARE_EQUAL(cchTest, dwWritten, L"Verify all characters were written."); + + csbiexAfter.cbSize = sizeof(csbiexAfter); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Retrieve screen buffer properties after writing."); +} + +void ReadBackHelper(HANDLE hOut, + COORD coordReadBackPos, + DWORD dwReadBackLength, + wistd::unique_ptr& pszReadBack) +{ + // Add one so it can be zero terminated. + DWORD cbBuffer = dwReadBackLength + 1; + wistd::unique_ptr pszRead = wil::make_unique_failfast(cbBuffer); + ZeroMemory(pszRead.get(), cbBuffer * sizeof(char)); + + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, pszRead.get(), dwReadBackLength, coordReadBackPos, &dwRead), L"Read back data in the buffer."); + VERIFY_ARE_EQUAL(dwReadBackLength, dwRead, L"Verify API reports we read back the number of characters we asked for."); + + pszReadBack.swap(pszRead); +} + +void FileTests::TestWriteFileProcessed() +{ + // \x7 is bell + // \x8 is backspace + // \x9 is tab + // \xa is linefeed + // \xd is carriage return + // All should cause activity in processed mode. + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; + csbiexOriginal.cbSize = sizeof(csbiexOriginal); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, ENABLE_PROCESSED_OUTPUT), L"Set processed write mode."); + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + // Declare variables needed for each character test. + CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 }; + CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; + COORD coordExpected = { 0 }; + PCSTR pszTest; + DWORD cchTest; + PCSTR pszReadBackExpected; + DWORD cchReadBack; + wistd::unique_ptr pszReadBack; + + // 1. Test bell (\x7) + { + pszTest = "z\x7"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = "z "; + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write z and a bell. Cursor should move once as bell should have made audible noise (can't really test) and not moved or printed anything. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += 1; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved once for printable character and not for bell."); + + // Read back written data. + ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } + + + // 2. Test backspace (\x8) + { + pszTest = "yx\x8"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = "yx "; + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write two characters and a backspace. Cursor should move only one forward as the backspace should have moved the cursor back one after printing the second character. + // The backspace character itself is typically non-destructive so it should only affect the cursor, not the buffer contents. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += 1; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved twice forward for printable characters and once backward for backspace."); + + // Read back written data. + ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } + + // 3. Test tab (\x9) + { + // The tab character will space pad out the buffer to the next multiple-of-8 boundary. + // NOTE: This is dependent on the previous tests running first. + pszTest = "\x9"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = " "; + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write tab character. Cursor should move out to the next multiple-of-8 and leave space characters in its wake. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X = 8; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved forward to position 8 for tab."); + + // Read back written data. + ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } + + // 4. Test linefeed (\xa) + { + // The line feed character should move us down to the next line. + pszTest = "\xaQ"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = "Q "; + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write line feed character. Cursor should move down a line and then the Q from our string should be printed. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected.X = 1; + coordExpected.Y = 1; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved down a line and then one character over for linefeed + Q."); + + // Read back written data from the 2nd line. + COORD coordRead; + coordRead.Y = 1; + coordRead.X = 0; + ReadBackHelper(hOut, coordRead, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } + + // 5. Test carriage return (\xd) + { + // The carriage return character should move us to the front of the line. + pszTest = "J\xd"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = "QJ "; // J written, then move to beginning of line. + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write text and carriage return character. Cursor should end up at the beginning of this line. The J should have been printed in the line before we moved. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X = 0; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved to beginning of line for carriage return character."); + + // Read back text written from the 2nd line. + ReadBackHelper(hOut, csbiexAfter.dwCursorPosition, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } + + // 6. Print a character over the top of the existing + { + // After the carriage return, try typing on top of the Q with a K + pszTest = "K"; + cchTest = (DWORD)strlen(pszTest); + pszReadBackExpected = "KJ "; // NOTE: This is based on the previous test(s). + cchReadBack = (DWORD)strlen(pszReadBackExpected); + + // Write text. Cursor should end up on top of the J. + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += 1; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved over one for printing character."); + + // Read back text written from the 2nd line. + ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer."); + } +} + +void FileTests::TestWriteFileWrapEOL() +{ + bool fFlagOn; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fFlagOn", fFlagOn)); + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; + csbiexOriginal.cbSize = sizeof(csbiexOriginal); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test."); + + if (fFlagOn) + { + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, ENABLE_WRAP_AT_EOL_OUTPUT), L"Set wrap at EOL."); + } + else + { + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, 0), L"Make sure wrap at EOL is off."); + } + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + // Fill first row of the buffer with Z characters until 1 away from the end. + for (SHORT i = 0; i < csbiexOriginal.dwSize.X - 1; i++) + { + WriteFile(hOut, "Z", 1, nullptr, nullptr); + } + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 }; + csbiexBefore.cbSize = sizeof(csbiexBefore); + CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; + csbiexAfter.cbSize = sizeof(csbiexAfter); + COORD coordExpected = { 0 }; + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Get cursor position information before attempting to wrap at end of line."); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "Y", 1, nullptr, nullptr), L"Write of final character in line succeeded."); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Get cursor position information after attempting to wrap at end of line."); + + if (fFlagOn) + { + Log::Comment(L"Cursor should go down a row if we tried to print at end of line."); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.Y++; + coordExpected.X = 0; + } + else + { + Log::Comment(L"Cursor shouldn't move when printing at end of line."); + coordExpected = csbiexBefore.dwCursorPosition; + } + + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved as expected based on flag state."); +} + +void FileTests::TestWriteFileVTProcessing() +{ + bool fVtOn; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fVtOn", fVtOn)); + + bool fProcessedOn; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fProcessedOn", fProcessedOn)); + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; + csbiexOriginal.cbSize = sizeof(csbiexOriginal); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test."); + + DWORD dwFlags = 0; + WI_SetFlagIf(dwFlags, ENABLE_VIRTUAL_TERMINAL_PROCESSING, fVtOn); + WI_SetFlagIf(dwFlags, ENABLE_PROCESSED_OUTPUT, fProcessedOn); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwFlags), L"Turn on relevant flags for test."); + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + PCSTR pszTestString = "\x1b" "[14m"; + DWORD const cchTest = (DWORD)strlen(pszTestString); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 }; + csbiexBefore.cbSize = sizeof(csbiexBefore); + CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; + csbiexAfter.cbSize = sizeof(csbiexAfter); + + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTestString, cchTest); + + // We only expect characters to be processed and not printed if both processed mode and VT mode are on. + bool const fProcessedNotPrinted = fProcessedOn && fVtOn; + + if (fProcessedNotPrinted) + { + PCSTR pszReadBackExpected = " "; + DWORD const cchReadBackExpected = (DWORD)strlen(pszReadBackExpected); + + VERIFY_ARE_EQUAL(csbiexBefore.dwCursorPosition, csbiexAfter.dwCursorPosition, L"Verify cursor didn't move because the VT sequence was processed instead of printed."); + + wistd::unique_ptr pszReadBack; + ReadBackHelper(hOut, coordZero, cchReadBackExpected, pszReadBack); + VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify that nothing was printed into the buffer."); + } + else + { + COORD coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += (SHORT)cchTest; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved as characters should have been emitted, not consumed."); + + wistd::unique_ptr pszReadBack; + ReadBackHelper(hOut, coordZero, cchTest, pszReadBack); + VERIFY_ARE_EQUAL(String(pszTestString), String(pszReadBack.get()), L"Verify that original test string was printed into the buffer."); + } +} + +void FileTests::TestWriteFileDisableNewlineAutoReturn() +{ + bool fDisableAutoReturn; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fDisableAutoReturn", fDisableAutoReturn)); + + bool fProcessedOn; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fProcessedOn", fProcessedOn)); + + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; + csbiexOriginal.cbSize = sizeof(csbiexOriginal); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test."); + + DWORD dwMode = 0; + WI_SetFlagIf(dwMode, DISABLE_NEWLINE_AUTO_RETURN, fDisableAutoReturn); + WI_SetFlagIf(dwMode, ENABLE_PROCESSED_OUTPUT, fProcessedOn); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwMode), L"Set console mode for test."); + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 }; + csbiexBefore.cbSize = sizeof(csbiexBefore); + CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; + csbiexAfter.cbSize = sizeof(csbiexAfter); + COORD coordExpected = { 0 }; + + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, "abc", 3); + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += 3; + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Cursor should have moved right to the end of the text written."); + + WriteFileHelper(hOut, csbiexBefore, csbiexAfter, "\n", 1); + + if (fProcessedOn) + { + if (fDisableAutoReturn) + { + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.Y += 1; + } + else + { + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.Y += 1; + coordExpected.X = 0; + } + } + else + { + coordExpected = csbiexBefore.dwCursorPosition; + coordExpected.X += 1; + } + + VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Cursor should move to expected position."); +} + +void SendKeyHelper(HANDLE hIn, WORD vk) +{ + INPUT_RECORD irPause = { 0 }; + irPause.EventType = KEY_EVENT; + irPause.Event.KeyEvent.bKeyDown = TRUE; + irPause.Event.KeyEvent.wVirtualKeyCode = vk; + + DWORD dwWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputW(hIn, &irPause, 1u, &dwWritten), L"Key event sent."); +} + +void PauseHelper(HANDLE hIn) +{ + SendKeyHelper(hIn, VK_PAUSE); +} + +void UnpauseHelper(HANDLE hIn) +{ + SendKeyHelper(hIn, VK_ESCAPE); +} + +void FileTests::TestWriteFileSuspended() +{ + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); + + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; + csbiexOriginal.cbSize = sizeof(csbiexOriginal); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test."); + + DWORD dwMode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwMode), L"Set console mode for test."); + + COORD const coordZero = { 0 }; + VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer."); + + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "abc", 3, nullptr, nullptr), L"Test first write success."); + PauseHelper(hIn); + + auto BlockedWrite = std::async([&] { + Log::Comment(L"Background WriteFile scheduled."); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "def", 3, nullptr, nullptr), L"Test second write success."); + }); + + UnpauseHelper(hIn); + + BlockedWrite.wait(); +} + +void SendFullKeyStrokeHelper(HANDLE hIn, char ch) +{ + INPUT_RECORD ir[2]; + ZeroMemory(ir, ARRAYSIZE(ir) * sizeof(INPUT_RECORD)); + ir[0].EventType = KEY_EVENT; + ir[0].Event.KeyEvent.bKeyDown = TRUE; + ir[0].Event.KeyEvent.dwControlKeyState = ch < 0x20 ? LEFT_CTRL_PRESSED : 0; // set left_ctrl_pressed for control keys. + ir[0].Event.KeyEvent.uChar.AsciiChar = ch; + ir[0].Event.KeyEvent.wVirtualKeyCode = VkKeyScanA(ir[0].Event.KeyEvent.uChar.AsciiChar); + ir[0].Event.KeyEvent.wVirtualScanCode = (WORD)MapVirtualKeyA(ir[0].Event.KeyEvent.wVirtualKeyCode, MAPVK_VK_TO_VSC); + ir[0].Event.KeyEvent.wRepeatCount = 1; + ir[1] = ir[0]; + ir[1].Event.KeyEvent.bKeyDown = FALSE; + + DWORD dwWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(hIn, ir, (DWORD)ARRAYSIZE(ir), &dwWritten), L"Writing key stroke."); + VERIFY_ARE_EQUAL((DWORD)ARRAYSIZE(ir), dwWritten, L"Written matches expected."); +} + +void FileTests::TestReadFileBasic() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + DWORD dwMode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); + + char ch = '\0'; + Log::Comment(L"Queue background blocking read file operation."); + auto BackgroundRead = std::async([&] { + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful."); + VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character."); + }); + + char const chExpected = 'a'; + Log::Comment(L"Send a key into the console."); + SendFullKeyStrokeHelper(hIn, chExpected); + + Log::Comment(L"Wait for background to unblock."); + BackgroundRead.wait(); + VERIFY_ARE_EQUAL(chExpected, ch); +} + +void FileTests::TestReadFileBasicSync() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + DWORD dwMode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); + + char const chExpected = 'a'; + Log::Comment(L"Send a key into the console."); + SendFullKeyStrokeHelper(hIn, chExpected); + + char ch = '\0'; + Log::Comment(L"Read with synchronous blocking read."); + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful."); + VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character."); + + VERIFY_ARE_EQUAL(chExpected, ch); +} + +void FileTests::TestReadFileBasicEmpty() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + DWORD dwMode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); + + char ch = '\0'; + Log::Comment(L"Queue background blocking read file operation."); + auto BackgroundRead = std::async([&] { + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful."); + VERIFY_ARE_EQUAL(0u, dwRead, L"We should have read nothing back. It should just return from Ctrl+Z"); + }); + + char const chExpected = '\x1a'; // ctrl+z character + Log::Comment(L"Send a key into the console."); + SendFullKeyStrokeHelper(hIn, chExpected); + + Log::Comment(L"Wait for background to unblock."); + BackgroundRead.wait(); + VERIFY_ARE_EQUAL('\0', ch); +} + +void FileTests::TestReadFileLine() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + DWORD dwMode = ENABLE_LINE_INPUT; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); + + char ch = '\0'; + Log::Comment(L"Queue background blocking read file operation."); + auto BackgroundRead = std::async([&] { + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful."); + VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character."); + }); + + char const chExpected = 'a'; + Log::Comment(L"Send a key into the console."); + SendFullKeyStrokeHelper(hIn, chExpected); + + auto status = BackgroundRead.wait_for(std::chrono::milliseconds(250)); + VERIFY_ARE_EQUAL(std::future_status::timeout, status, L"We should still be waiting for a result."); + VERIFY_ARE_EQUAL('\0', ch, L"Character shouldn't be filled by background read yet."); + + Log::Comment(L"Send a line feed character, we should stay blocked."); + SendFullKeyStrokeHelper(hIn, '\n'); + status = BackgroundRead.wait_for(std::chrono::milliseconds(250)); + VERIFY_ARE_EQUAL(std::future_status::timeout, status, L"We should still be waiting for a result."); + VERIFY_ARE_EQUAL('\0', ch, L"Character shouldn't be filled by background read yet."); + + Log::Comment(L"Now send a carriage return into the console to signify the end of the input line."); + SendFullKeyStrokeHelper(hIn, '\r'); + + Log::Comment(L"Wait for background thread to unblock."); + BackgroundRead.wait(); + VERIFY_ARE_EQUAL(chExpected, ch); +} + +void FileTests::TestReadFileLineSync() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); + + DWORD dwMode = ENABLE_LINE_INPUT; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); + + char const chExpected = 'a'; + Log::Comment(L"Send a key into the console followed by a carriage return."); + SendFullKeyStrokeHelper(hIn, chExpected); + SendFullKeyStrokeHelper(hIn, '\r'); + + char ch = '\0'; + Log::Comment(L"Read back the input with a synchronous blocking read."); + DWORD dwRead = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, nullptr, nullptr), L"Read file was successful."); + VERIFY_ARE_EQUAL(0u, dwRead, L"Verify we read 0 characters."); + + VERIFY_ARE_EQUAL(chExpected, ch); +} + +//void FileTests::TestReadFileEcho() +//{ +// bool fUseBlockedRead; +// VERIFY_SUCCEEDED(TestData::TryGetValue(L"fUseBlockedRead", fUseBlockedRead)); +// +// HANDLE const hOut = GetStdOutputHandle(); +// VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle."); +// +// HANDLE const hIn = GetStdInputHandle(); +// VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle."); +// +// DWORD dwMode = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT; +// VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test."); +// +// VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test."); +// +// CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 }; +// csbiexOriginal.cbSize = sizeof(csbiexOriginal); +// VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve output screen buffer information."); +// +// COORD const coordZero = { 0 }; +// VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"We expect the cursor to be at 0,0 for the start of this test."); +// +// char ch = '\0'; +// std::future BackgroundRead; +// if (fUseBlockedRead) +// { +// Log::Comment(L"Queue background blocking read file operation."); +// BackgroundRead = std::async([&] { +// OVERLAPPED overlapped = { 0 }; +// wil::unique_event evt; +// evt.create(); +// overlapped.hEvent = evt.get(); +// +// DWORD dwRead = 0; +// VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, nullptr, &overlapped), L"Read file was dispatched successfully."); +// +// std::array handles; +// handles[0] = _cancellationEvent; +// handles[1] = overlapped.hEvent; +// +// WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE); +// Log::Comment(L"Wait complete."); +// +// VERIFY_ARE_EQUAL(0u, dwRead, L"Verify we read 0 characters."); +// }); +// } +// +// Log::Comment(L"Read back the first line of the buffer to see that it is empty."); +// wistd::unique_ptr pszBefore; +// PCSTR pszBeforeExpected = " "; +// DWORD const cchBeforeExpected = (DWORD)strlen(pszBeforeExpected); +// ReadBackHelper(hOut, coordZero, cchBeforeExpected, pszBefore); +// VERIFY_ARE_EQUAL(String(pszBeforeExpected), String(pszBefore.get()), L"Verify the first few characters of the buffer are empty (spaces)"); +// +// PCSTR pszAfterExpected = "qzmp "; +// COORD coordCursorAfter = { 0 }; +// DWORD const cchAfterExpected = (DWORD)strlen(pszAfterExpected); +// +// Log::Comment(L"Now write in a few input characters to the buffer."); +// for (DWORD i = 0; i < cchAfterExpected - 1; i++) +// { +// SendFullKeyStrokeHelper(hIn, pszAfterExpected[i]); +// coordCursorAfter.X++; +// } +// +// Log::Comment(L"Read back the first line of the buffer to see if we've echoed characters."); +// wistd::unique_ptr pszAfter; +// ReadBackHelper(hOut, coordZero, cchAfterExpected, pszAfter); +// +// if (fUseBlockedRead) +// { +// VERIFY_ARE_EQUAL(String(pszAfterExpected), String(pszAfter.get()), L"Verify the characters written were echoed into the buffer."); +// } +// else +// { +// VERIFY_ARE_EQUAL(String(pszBeforeExpected), String(pszAfter.get()), L"Verify nothing should have been printed while no one was waiting on a read."); +// } +// +// CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 }; +// csbiexAfter.cbSize = sizeof(csbiexAfter); +// VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Get the cursor position after the writes."); +// +// if (fUseBlockedRead) +// { +// VERIFY_ARE_EQUAL(coordCursorAfter, csbiexAfter.dwCursorPosition, L"Cursor should have moved with the writes."); +// } +// else +// { +// VERIFY_ARE_EQUAL(coordZero, csbiexAfter.dwCursorPosition, L"Cursor shouldn't move if no one is waiting with a read."); +// } +// +// if (fUseBlockedRead) +// { +// Log::Comment(L"Send newline to unblock the read."); +// SendFullKeyStrokeHelper(hIn, '\r'); +// BackgroundRead.wait(); +// } +//} diff --git a/src/host/ft_host/API_FillOutputTests.cpp b/src/host/ft_host/API_FillOutputTests.cpp new file mode 100644 index 000000000..680f24eb3 --- /dev/null +++ b/src/host/ft_host/API_FillOutputTests.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#define CP_USA 437 + +class FillOutputTests +{ + BEGIN_TEST_CLASS(FillOutputTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + + TEST_METHOD(WriteNarrowGlyphAscii) + { + HANDLE hConsole = GetStdOutputHandle(); + DWORD charsWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterA(hConsole, + 'a', + 1, + { 0, 0 }, + &charsWritten)); + VERIFY_ARE_EQUAL(1u, charsWritten); + + // test a box drawing character + const UINT previousCodepage = GetConsoleOutputCP(); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(CP_USA)); + + charsWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterA(hConsole, + '\xCE', // U+256C box drawing double vertical and horizontal + 1, + { 0, 0 }, + &charsWritten)); + VERIFY_ARE_EQUAL(1u, charsWritten); + VERIFY_SUCCEEDED(SetConsoleOutputCP(previousCodepage)); + } + + TEST_METHOD(WriteNarrowGlyphUnicode) + { + HANDLE hConsole = GetStdOutputHandle(); + DWORD charsWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(hConsole, + L'a', + 1, + { 0, 0 }, + &charsWritten)); + VERIFY_ARE_EQUAL(1u, charsWritten); + } + + TEST_METHOD(WriteWideGlyphUnicode) + { + HANDLE hConsole = GetStdOutputHandle(); + DWORD charsWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(hConsole, + L'\x304F', + 1, + { 0, 0 }, + &charsWritten)); + VERIFY_ARE_EQUAL(1u, charsWritten); + } +}; diff --git a/src/host/ft_host/API_FontTests.cpp b/src/host/ft_host/API_FontTests.cpp new file mode 100644 index 000000000..876d22644 --- /dev/null +++ b/src/host/ft_host/API_FontTests.cpp @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +static const COORD c_coordZero = {0,0}; + +static const PCWSTR pwszLongFontPath = L"%WINDIR%\\Fonts\\ltype.ttf"; + +class FontTests +{ + BEGIN_TEST_CLASS(FontTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl3.h") + END_TEST_CLASS() + + TEST_METHOD_SETUP(TestSetup); + TEST_METHOD_CLEANUP(TestCleanup); + + BEGIN_TEST_METHOD(TestCurrentFontAPIsInvalid) + TEST_METHOD_PROPERTY(L"Data:dwConsoleOutput", L"{0, 1, 0xFFFFFFFF}") + TEST_METHOD_PROPERTY(L"Data:bMaximumWindow", L"{TRUE, FALSE}") + TEST_METHOD_PROPERTY(L"Data:strOperation", L"{Get, GetEx, SetEx}") + END_TEST_METHOD(); + + BEGIN_TEST_METHOD(TestGetFontSizeInvalid) + TEST_METHOD_PROPERTY(L"Data:dwConsoleOutput", L"{0, 0xFFFFFFFF}") + END_TEST_METHOD(); + + TEST_METHOD(TestGetFontSizeLargeIndexInvalid); + TEST_METHOD(TestSetConsoleFontNegativeSize); + + TEST_METHOD(TestFontScenario); + TEST_METHOD(TestLongFontNameScenario); + + TEST_METHOD(TestSetFontAdjustsWindow); +}; + +bool FontTests::TestSetup() +{ + SetVerifyOutput verifySettings(VerifyOutputSettings::LogOnlyFailures); + return true; +} + +bool FontTests::TestCleanup() +{ + SetVerifyOutput verifySettings(VerifyOutputSettings::LogOnlyFailures); + return true; +} + +void FontTests::TestCurrentFontAPIsInvalid() +{ + DWORD dwConsoleOutput; + bool bMaximumWindow; + String strOperation; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwConsoleOutput", dwConsoleOutput), L"Get output handle value"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"bMaximumWindow", bMaximumWindow), L"Get maximized window value"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"strOperation", strOperation), L"Get operation value"); + + const bool bUseValidOutputHandle = (dwConsoleOutput == 1); + HANDLE hConsoleOutput; + if (bUseValidOutputHandle) + { + hConsoleOutput = GetStdOutputHandle(); + } + else + { + hConsoleOutput = (HANDLE)dwConsoleOutput; + } + + if (strOperation == L"Get") + { + CONSOLE_FONT_INFO cfi = {0}; + + if (bUseValidOutputHandle) + { + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFont(hConsoleOutput, (BOOL)bMaximumWindow, &cfi)); + } + else + { + VERIFY_WIN32_BOOL_FAILED(OneCoreDelay::GetCurrentConsoleFont(hConsoleOutput, (BOOL)bMaximumWindow, &cfi)); + } + } + else if (strOperation == L"GetEx") + { + CONSOLE_FONT_INFOEX cfie = {0}; + VERIFY_WIN32_BOOL_FAILED(OneCoreDelay::GetCurrentConsoleFontEx(hConsoleOutput, (BOOL)bMaximumWindow, &cfie)); + } + else if (strOperation == L"SetEx") + { + CONSOLE_FONT_INFOEX cfie = {0}; + VERIFY_WIN32_BOOL_FAILED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, (BOOL)bMaximumWindow, &cfie)); + } + else + { + VERIFY_FAIL(L"Unrecognized operation"); + } +} + +void FontTests::TestGetFontSizeInvalid() +{ + DWORD dwConsoleOutput; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwConsoleOutput", dwConsoleOutput), L"Get input handle value"); + + // Need to make sure that last error is cleared so that we can verify that lasterror was set by GetConsoleFontSize + SetLastError(0); + + COORD coordFontSize = OneCoreDelay::GetConsoleFontSize((HANDLE)dwConsoleOutput, 0); + VERIFY_ARE_EQUAL(coordFontSize, c_coordZero, L"Ensure (0,0) coord returned to indicate failure"); + VERIFY_ARE_EQUAL(GetLastError(), (DWORD)ERROR_INVALID_HANDLE, L"Ensure last error was set appropriately"); +} + +void FontTests::TestGetFontSizeLargeIndexInvalid() +{ + SetLastError(0); + COORD coordFontSize = OneCoreDelay::GetConsoleFontSize(GetStdOutputHandle(), 0xFFFFFFFF); + VERIFY_ARE_EQUAL(coordFontSize, c_coordZero, L"Ensure (0,0) coord returned to indicate failure"); + VERIFY_ARE_EQUAL(GetLastError(), (DWORD)ERROR_INVALID_PARAMETER, L"Ensure last error was set appropriately"); +} + +void FontTests::TestSetConsoleFontNegativeSize() +{ + const HANDLE hConsoleOutput = GetStdOutputHandle(); + CONSOLE_FONT_INFOEX cfie = {0}; + cfie.cbSize = sizeof(cfie); + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfie)); + cfie.dwFontSize.X = -4; + cfie.dwFontSize.Y = -12; + + // as strange as it sounds, we don't filter out negative font sizes. under the hood, this call ends up in + // FindCreateFont, which runs through our list of loaded fonts, fails to find, takes the absolute value of Y, and + // then performs a GDI font enumeration for fonts that match. we should hold on to this behavior until we can + // establish that it's no longer necessary. + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfie)); +} + +void FontTests::TestFontScenario() +{ + const HANDLE hConsoleOutput = GetStdOutputHandle(); + + Log::Comment(L"1. Ensure that the various GET APIs for font information align with each other."); + CONSOLE_FONT_INFOEX cfie = {0}; + cfie.cbSize = sizeof(cfie); + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfie)); + + CONSOLE_FONT_INFO cfi = {0}; + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFont(hConsoleOutput, FALSE, &cfi)); + + VERIFY_ARE_EQUAL(cfi.nFont, cfie.nFont, L"Ensure regular and Ex APIs return same nFont"); + VERIFY_ARE_NOT_EQUAL(cfi.dwFontSize, c_coordZero, L"Ensure non-zero font size"); + VERIFY_ARE_EQUAL(cfi.dwFontSize, cfie.dwFontSize, L"Ensure regular and Ex APIs return same dwFontSize"); + + const COORD coordCurrentFontSize = OneCoreDelay::GetConsoleFontSize(hConsoleOutput, cfi.nFont); + VERIFY_ARE_EQUAL(coordCurrentFontSize, cfi.dwFontSize, L"Ensure GetConsoleFontSize output matches GetCurrentConsoleFont"); + + // --------------------- + + Log::Comment(L"2. Ensure that our font settings round-trip appropriately through the Ex APIs"); + CONSOLE_FONT_INFOEX cfieSet = {0}; + cfieSet.cbSize = sizeof(cfieSet); + cfieSet.dwFontSize.Y = 12; + VERIFY_SUCCEEDED(StringCchCopy(cfieSet.FaceName, ARRAYSIZE(cfieSet.FaceName), L"Lucida Console")); + + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfieSet)); + + CONSOLE_FONT_INFOEX cfiePost = {0}; + cfiePost.cbSize = sizeof(cfiePost); + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfiePost)); + + // Ensure that the two values we attempted to set did accurately round-trip through the API. + // The other unspecified values may have been adjusted/updated by GDI. + if (0 != NoThrowString(cfieSet.FaceName).CompareNoCase(cfiePost.FaceName)) + { + Log::Comment(L"We cannot test changing fonts on systems that do not have alternatives available. Skipping test."); + Log::Result(WEX::Logging::TestResults::Result::Skipped); + return; + } + VERIFY_ARE_EQUAL(cfieSet.dwFontSize.Y, cfiePost.dwFontSize.Y); + + // Ensure that the entire structure we received matches what we expect to usually get for this Lucida Console Size 12 ask. + CONSOLE_FONT_INFOEX cfieFullExpected = { 0 }; + cfieFullExpected.cbSize = sizeof(cfieFullExpected); + wcscpy_s(cfieFullExpected.FaceName, L"Lucida Console"); + + if (!OneCoreDelay::IsIsWindowPresent()) + { + // On OneCore Windows without GDI, this is what we expect to get. + cfieFullExpected.dwFontSize.X = 8; + cfieFullExpected.dwFontSize.Y = 12; + cfieFullExpected.FontFamily = 4; + cfieFullExpected.FontWeight = 0; + } + else + { + // On client Windows with GDI, this is what we expect to get. + cfieFullExpected.dwFontSize.X = 7; + cfieFullExpected.dwFontSize.Y = 12; + cfieFullExpected.FontFamily = 54; + cfieFullExpected.FontWeight = 400; + } + + VERIFY_ARE_EQUAL(cfieFullExpected, cfiePost); +} + +void FontTests::TestLongFontNameScenario() +{ + wistd::unique_ptr expandedLongFontPath; + VERIFY_SUCCEEDED(ExpandPathToMutable(pwszLongFontPath, expandedLongFontPath)); + if (!CheckIfFileExists(expandedLongFontPath.get())) + { + Log::Comment(L"Lucida Sans Typewriter doesn't exist; skipping long font test."); + Log::Result(WEX::Logging::TestResults::Result::Skipped); + return; + } + + const HANDLE hConsoleOutput = GetStdOutputHandle(); + + CONSOLE_FONT_INFOEX cfieSetLong = { 0 }; + cfieSetLong.cbSize = sizeof(cfieSetLong); + cfieSetLong.FontFamily = 54; + cfieSetLong.dwFontSize.Y = 12; + VERIFY_SUCCEEDED(StringCchCopy(cfieSetLong.FaceName, ARRAYSIZE(cfieSetLong.FaceName), L"Lucida Sans Typewriter")); + + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfieSetLong)); + + CONSOLE_FONT_INFOEX cfiePostLong = { 0 }; + cfiePostLong.cbSize = sizeof(cfiePostLong); + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfiePostLong)); + + Log::Comment(NoThrowString().Format(L"%ls %ls", cfieSetLong.FaceName, cfiePostLong.FaceName)); + + VERIFY_ARE_EQUAL(0, NoThrowString(cfieSetLong.FaceName).CompareNoCase(cfiePostLong.FaceName)); +} + +void FontTests::TestSetFontAdjustsWindow() +{ + if (!OneCoreDelay::IsIsWindowPresent()) + { + Log::Comment(L"Adjusting window size by changing font scenario can't be checked on platform without classic window operations."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + const HANDLE hConsoleOutput = GetStdOutputHandle(); + const HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + RECT rc = { 0 }; + + CONSOLE_FONT_INFOEX cfiex = { 0 }; + cfiex.cbSize = sizeof(cfiex); + + Log::Comment(L"First set the console window to Consolas 16."); + wcscpy_s(cfiex.FaceName, L"Consolas"); + cfiex.dwFontSize.Y = 16; + + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfiex)); + Sleep(250); + VERIFY_WIN32_BOOL_SUCCEEDED(GetClientRect(hwnd, &rc), L"Retrieve client rectangle size for Consolas 16."); + SIZE szConsolas; + szConsolas.cx = rc.right - rc.left; + szConsolas.cy = rc.bottom - rc.top; + Log::Comment(NoThrowString().Format(L"Client rect size is (X: %d, Y: %d)", szConsolas.cx, szConsolas.cy)); + + Log::Comment(L"Adjust console window to Lucida Console 12."); + wcscpy_s(cfiex.FaceName, L"Lucida Console"); + cfiex.dwFontSize.Y = 12; + + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfiex)); + Sleep(250); + VERIFY_WIN32_BOOL_SUCCEEDED(GetClientRect(hwnd, &rc), L"Retrieve client rectangle size for Lucida Console 12."); + SIZE szLucida; + szLucida.cx = rc.right - rc.left; + szLucida.cy = rc.bottom - rc.top; + + Log::Comment(NoThrowString().Format(L"Client rect size is (X: %d, Y: %d)", szLucida.cx, szLucida.cy)); + Log::Comment(L"Window should shrink in size when going to Lucida 12 from Consolas 16."); + VERIFY_IS_LESS_THAN(szLucida.cx, szConsolas.cx); + VERIFY_IS_LESS_THAN(szLucida.cy, szConsolas.cy); + + Log::Comment(L"Adjust console window back to Consolas 16."); + wcscpy_s(cfiex.FaceName, L"Consolas"); + cfiex.dwFontSize.Y = 16; + + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::SetCurrentConsoleFontEx(hConsoleOutput, FALSE, &cfiex)); + Sleep(250); + VERIFY_WIN32_BOOL_SUCCEEDED(GetClientRect(hwnd, &rc), L"Retrieve client rectangle size for Consolas 16."); + szConsolas.cx = rc.right - rc.left; + szConsolas.cy = rc.bottom - rc.top; + + Log::Comment(NoThrowString().Format(L"Client rect size is (X: %d, Y: %d)", szConsolas.cx, szConsolas.cy)); + Log::Comment(L"Window should grow in size when going from Lucida 12 to Consolas 16."); + VERIFY_IS_LESS_THAN(szLucida.cx, szConsolas.cx); + VERIFY_IS_LESS_THAN(szLucida.cy, szConsolas.cy); +} diff --git a/src/host/ft_host/API_InputTests.cpp b/src/host/ft_host/API_InputTests.cpp new file mode 100644 index 000000000..636924e9a --- /dev/null +++ b/src/host/ft_host/API_InputTests.cpp @@ -0,0 +1,714 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +#include "..\..\interactivity\onecore\SystemConfigurationProvider.hpp" + +// some assumptions have been made on this value. only change it if you have a good reason to. +#define NUMBER_OF_SCENARIO_INPUTS 10 +#define READ_BATCH 3 + +// This class is intended to test: +// FlushConsoleInputBuffer +// PeekConsoleInput +// ReadConsoleInput +// WriteConsoleInput +// GetNumberOfConsoleInputEvents +// GetNumberOfConsoleMouseButtons +class InputTests +{ + BEGIN_TEST_CLASS(InputTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"api-ms-win-core-console-l2-2-0.lib") + END_TEST_CLASS() + + TEST_CLASS_SETUP(TestSetup); + TEST_CLASS_CLEANUP(TestCleanup); + + TEST_METHOD(TestGetMouseButtonsValid); // note: GetNumberOfConsoleMouseButtons crashes with nullptr, so there's no + // negative test + TEST_METHOD(TestInputScenario); + TEST_METHOD(TestFlushValid); + TEST_METHOD(TestFlushInvalid); + TEST_METHOD(TestPeekConsoleInvalid); + TEST_METHOD(TestReadConsoleInvalid); + TEST_METHOD(TestWriteConsoleInvalid); + + TEST_METHOD(TestReadWaitOnHandle); + + TEST_METHOD(TestReadConsolePasswordScenario); + + TEST_METHOD(TestMouseWheelReadConsoleMouseInput); + TEST_METHOD(TestMouseHorizWheelReadConsoleMouseInput); + TEST_METHOD(TestMouseWheelReadConsoleNoMouseInput); + TEST_METHOD(TestMouseHorizWheelReadConsoleNoMouseInput); + TEST_METHOD(TestMouseWheelReadConsoleInputQuickEdit); + TEST_METHOD(TestMouseHorizWheelReadConsoleInputQuickEdit); + TEST_METHOD(RawReadUnpacksCoalsescedInputRecords); + + BEGIN_TEST_METHOD(TestVtInputGeneration) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD(); +}; + +void VerifyNumberOfInputRecords(const HANDLE hConsoleInput, _In_ DWORD nInputs) +{ + SetVerifyOutput verifySettings(VerifyOutputSettings::LogOnlyFailures); + DWORD nInputEvents = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(hConsoleInput, &nInputEvents)); + VERIFY_ARE_EQUAL(nInputEvents, + nInputs, + L"Verify number of input events"); +} + +bool InputTests::TestSetup() +{ + const bool fRet = Common::TestBufferSetup(); + + HANDLE hConsoleInput = GetStdInputHandle(); + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hConsoleInput)); + VerifyNumberOfInputRecords(hConsoleInput, 0); + + return fRet; +} + +bool InputTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void InputTests::TestGetMouseButtonsValid() +{ + DWORD nMouseButtons = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(OneCoreDelay::GetNumberOfConsoleMouseButtons(&nMouseButtons)); + + DWORD dwButtonsExpected = (DWORD)-1; + if (OneCoreDelay::IsGetSystemMetricsPresent()) + { + dwButtonsExpected = (DWORD)GetSystemMetrics(SM_CMOUSEBUTTONS); + } + else + { + dwButtonsExpected = Microsoft::Console::Interactivity::OneCore::SystemConfigurationProvider::s_DefaultNumberOfMouseButtons; + } + + VERIFY_ARE_EQUAL(dwButtonsExpected, nMouseButtons); +} + +void GenerateAndWriteInputRecords(const HANDLE hConsoleInput, + const UINT cRecordsToGenerate, + _Out_writes_(cRecs) INPUT_RECORD *prgRecs, + const DWORD cRecs, + _Out_ PDWORD pdwWritten) +{ + Log::Comment(String().Format(L"Generating %d input events", cRecordsToGenerate)); + for (UINT iRecord = 0; iRecord < cRecs; iRecord++) + { + prgRecs[iRecord].EventType = KEY_EVENT; + prgRecs[iRecord].Event.KeyEvent.bKeyDown = FALSE; + prgRecs[iRecord].Event.KeyEvent.wRepeatCount = 1; + prgRecs[iRecord].Event.KeyEvent.wVirtualKeyCode = ('A' + (WORD)iRecord); + } + + Log::Comment(L"Writing events"); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hConsoleInput, prgRecs, cRecs, pdwWritten)); + VERIFY_ARE_EQUAL(*pdwWritten, + cRecs, + L"verify number written"); +} + +void InputTests::TestInputScenario() +{ + Log::Comment(L"Get input handle"); + HANDLE hConsoleInput = GetStdInputHandle(); + + DWORD nWrittenEvents = (DWORD)-1; + INPUT_RECORD rgInputRecords[NUMBER_OF_SCENARIO_INPUTS] = {0}; + GenerateAndWriteInputRecords(hConsoleInput, NUMBER_OF_SCENARIO_INPUTS, rgInputRecords, ARRAYSIZE(rgInputRecords), &nWrittenEvents); + + VerifyNumberOfInputRecords(hConsoleInput, ARRAYSIZE(rgInputRecords)); + + Log::Comment(L"Peeking events"); + INPUT_RECORD rgPeekedRecords[NUMBER_OF_SCENARIO_INPUTS] = {0}; + DWORD nPeekedEvents = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(PeekConsoleInput(hConsoleInput, rgPeekedRecords, ARRAYSIZE(rgPeekedRecords), &nPeekedEvents)); + VERIFY_ARE_EQUAL(nPeekedEvents, nWrittenEvents, L"We should be able to peek at all of the records we've written"); + for (UINT iPeekedRecord = 0; iPeekedRecord < nPeekedEvents; iPeekedRecord++) + { + VERIFY_ARE_EQUAL(rgPeekedRecords[iPeekedRecord], + rgInputRecords[iPeekedRecord], + L"make sure our peeked records match what we input"); + } + + // read inputs 3 at a time until we've read them all. since the number we're batching by doesn't match the number of + // total events, we need to account for the last incomplete read we'll perform. + const UINT cIterations = (NUMBER_OF_SCENARIO_INPUTS / READ_BATCH) + ((NUMBER_OF_SCENARIO_INPUTS % READ_BATCH > 0) ? 1 : 0); + for (UINT iIteration = 0; iIteration < cIterations; iIteration++) + { + const bool fIsLastIteration = (iIteration + 1) > (NUMBER_OF_SCENARIO_INPUTS / READ_BATCH); + Log::Comment(String().Format(L"Reading inputs (iteration %d/%d)%s", + iIteration+1, + cIterations, + fIsLastIteration ? L" (last one)" : L"")); + + INPUT_RECORD rgReadRecords[READ_BATCH] = {0}; + DWORD nReadEvents = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(hConsoleInput, rgReadRecords, ARRAYSIZE(rgReadRecords), &nReadEvents)); + + DWORD dwExpectedEventsRead = READ_BATCH; + if (fIsLastIteration) + { + // on the last iteration, we'll have an incomplete read. account for it here. + dwExpectedEventsRead = NUMBER_OF_SCENARIO_INPUTS % READ_BATCH; + } + + VERIFY_ARE_EQUAL(nReadEvents, dwExpectedEventsRead); + for (UINT iReadRecord = 0; iReadRecord < nReadEvents; iReadRecord++) + { + const UINT iInputRecord = iReadRecord+(iIteration*READ_BATCH); + VERIFY_ARE_EQUAL(rgReadRecords[iReadRecord], + rgInputRecords[iInputRecord], + String().Format(L"verify record %d", iInputRecord)); + } + + DWORD nInputEventsAfterRead = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(hConsoleInput, &nInputEventsAfterRead)); + + DWORD dwExpectedEventsAfterRead = (NUMBER_OF_SCENARIO_INPUTS - (READ_BATCH*(iIteration+1))); + if (fIsLastIteration) + { + dwExpectedEventsAfterRead = 0; + } + VERIFY_ARE_EQUAL(dwExpectedEventsAfterRead, + nInputEventsAfterRead, + L"verify number of remaining inputs"); + } +} + +void InputTests::TestFlushValid() +{ + Log::Comment(L"Get input handle"); + HANDLE hConsoleInput = GetStdInputHandle(); + + DWORD nWrittenEvents = (DWORD)-1; + INPUT_RECORD rgInputRecords[NUMBER_OF_SCENARIO_INPUTS] = {0}; + GenerateAndWriteInputRecords(hConsoleInput, NUMBER_OF_SCENARIO_INPUTS, rgInputRecords, ARRAYSIZE(rgInputRecords), &nWrittenEvents); + + VerifyNumberOfInputRecords(hConsoleInput, ARRAYSIZE(rgInputRecords)); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hConsoleInput)); + + VerifyNumberOfInputRecords(hConsoleInput, 0); +} + +void InputTests::TestFlushInvalid() +{ + // NOTE: FlushConsoleInputBuffer(nullptr) crashes, so we don't verify that here. + VERIFY_WIN32_BOOL_FAILED(FlushConsoleInputBuffer(INVALID_HANDLE_VALUE)); +} + +void InputTests::TestPeekConsoleInvalid() +{ + DWORD nPeeked = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(PeekConsoleInput(INVALID_HANDLE_VALUE, nullptr, 0, &nPeeked)); // NOTE: nPeeked is required + VERIFY_ARE_EQUAL(nPeeked, (DWORD)0); + + HANDLE hConsoleInput = GetStdInputHandle(); + + nPeeked = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(PeekConsoleInput(hConsoleInput, nullptr, 5, &nPeeked)); + VERIFY_ARE_EQUAL(nPeeked, (DWORD)0); + + DWORD nWritten = (DWORD)-1; + INPUT_RECORD ir = {0}; + GenerateAndWriteInputRecords(hConsoleInput, 1, &ir, 1, &nWritten); + + VerifyNumberOfInputRecords(hConsoleInput, 1); + + nPeeked = (DWORD)-1; + INPUT_RECORD irPeeked = {0}; + VERIFY_WIN32_BOOL_SUCCEEDED(PeekConsoleInput(hConsoleInput, &irPeeked, 0, &nPeeked)); + VERIFY_ARE_EQUAL(nPeeked, (DWORD)0, L"Verify that an empty array doesn't cause peeks to get written"); + + VerifyNumberOfInputRecords(hConsoleInput, 1); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hConsoleInput)); +} + +void InputTests::TestReadConsoleInvalid() +{ + DWORD nRead = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(ReadConsoleInput(0, nullptr, 0, &nRead)); + VERIFY_ARE_EQUAL(nRead, (DWORD)0); + + nRead = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(ReadConsoleInput(INVALID_HANDLE_VALUE, nullptr, 0, &nRead)); + VERIFY_ARE_EQUAL(nRead, (DWORD)0); + + // NOTE: ReadConsoleInput blocks until at least one input event is read, even if the operation would result in no + // records actually being read (e.g. valid handle, NULL lpBuffer) + + HANDLE hConsoleInput = GetStdInputHandle(); + + DWORD nWritten = (DWORD)-1; + INPUT_RECORD irWrite = {0}; + GenerateAndWriteInputRecords(hConsoleInput, 1, &irWrite, 1, &nWritten); + VerifyNumberOfInputRecords(hConsoleInput, 1); + + nRead = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(hConsoleInput, nullptr, 0, &nRead)); + VERIFY_ARE_EQUAL(nRead, (DWORD)0); + + INPUT_RECORD irRead = {0}; + nRead = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(hConsoleInput, &irRead, 0, &nRead)); + VERIFY_ARE_EQUAL(nRead, (DWORD)0); + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hConsoleInput)); +} + +void InputTests::TestWriteConsoleInvalid() +{ + DWORD nWrite = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(WriteConsoleInput(0, nullptr, 0, &nWrite)); + VERIFY_ARE_EQUAL(nWrite, (DWORD)0); + + // weird: WriteConsoleInput with INVALID_HANDLE_VALUE writes garbage to lpNumberOfEventsWritten, whereas + // [Read|Peek]ConsoleInput don't. This is a legacy behavior that we don't want to change. + nWrite = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(WriteConsoleInput(INVALID_HANDLE_VALUE, nullptr, 0, &nWrite)); + + HANDLE hConsoleInput = GetStdInputHandle(); + + nWrite = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hConsoleInput, nullptr, 0, &nWrite)); + VERIFY_ARE_EQUAL(nWrite, (DWORD)0); + + nWrite = (DWORD)-1; + INPUT_RECORD irWrite = {0}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hConsoleInput, &irWrite, 0, &nWrite)); + VERIFY_ARE_EQUAL(nWrite, (DWORD)0); +} + +void FillInputRecordHelper(_Inout_ INPUT_RECORD* const pir, _In_ wchar_t wch, _In_ bool fIsKeyDown) +{ + pir->EventType = KEY_EVENT; + pir->Event.KeyEvent.wRepeatCount = 1; + pir->Event.KeyEvent.dwControlKeyState = 0; + pir->Event.KeyEvent.bKeyDown = !!fIsKeyDown; + pir->Event.KeyEvent.uChar.UnicodeChar = wch; + + // This only holds true for capital letters from A-Z. + VERIFY_IS_TRUE(wch >= 'A' && wch <= 'Z'); + pir->Event.KeyEvent.wVirtualKeyCode = wch; + + pir->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(pir->Event.KeyEvent.wVirtualKeyCode, MAPVK_VK_TO_VSC)); +} + +void InputTests::TestReadConsolePasswordScenario() +{ + if (!OneCoreDelay::IsPostMessageWPresent()) + { + Log::Comment(L"Password scenario can't be checked on platform without window message queuing."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + // Scenario inspired by net use's password capture code. + HANDLE const hIn = GetStdHandle(STD_INPUT_HANDLE); + + // 1. Set up our mode to be raw input (mimicing method used by "net use") + DWORD mode = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_MOUSE_INPUT; + GetConsoleMode(hIn, &mode); + + SetConsoleMode(hIn, (~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT)) & mode); + + // 2. Flush and write some text into the input buffer (added for sake of test.) + PCWSTR pwszExpected = L"QUE"; + DWORD const cBuffer = static_cast(wcslen(pwszExpected) * 2); + wistd::unique_ptr irBuffer = wil::make_unique_nothrow(cBuffer); + FillInputRecordHelper(&irBuffer.get()[0], pwszExpected[0], true); + FillInputRecordHelper(&irBuffer.get()[1], pwszExpected[0], false); + FillInputRecordHelper(&irBuffer.get()[2], pwszExpected[1], true); + FillInputRecordHelper(&irBuffer.get()[3], pwszExpected[1], false); + FillInputRecordHelper(&irBuffer.get()[4], pwszExpected[2], true); + FillInputRecordHelper(&irBuffer.get()[5], pwszExpected[2], false); + + DWORD dwWritten; + FlushConsoleInputBuffer(hIn); + WriteConsoleInputW(hIn, irBuffer.get(), cBuffer, &dwWritten); + + + // Press "enter" key on the window to signify the user pressing enter at the end of the password. + + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(PostMessageW(GetConsoleWindow(), WM_KEYDOWN, VK_RETURN, 0)); + + // 3. Set up our read loop (mimicing password capture methodology from "net use" command.) + size_t const buflen = (cBuffer / 2) + 1; // key down and key up will be coalesced into one. + wistd::unique_ptr buf = wil::make_unique_nothrow(buflen); + size_t len = 0; + VERIFY_IS_NOT_NULL(buf); + wchar_t* bufPtr = buf.get(); + + while (true) + { + wchar_t ch; + DWORD c; + int err = ReadConsoleW(hIn, &ch, 1, &c, 0); + + if (!err || c != 1) + { + ch = 0xffff; // end of line + } + + if ((ch == 0xD) || (ch == 0xffff)) // CR or end of line + { + break; + } + + if (ch == 0x8) // backspace + { + if (bufPtr != buf.get()) + { + bufPtr--; + len--; + } + } + else + { + *bufPtr = ch; + if (len < buflen) + { + bufPtr++; + } + len++; + } + } + + // 4. Restore console mode and terminate string (mimics "net use" behavior) + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), mode); + + *bufPtr = L'\0'; // null terminate string + putchar('\n'); + + // 5. Verify our string got read back (added for sake of the test) + VERIFY_ARE_EQUAL(String(pwszExpected), String(buf.get())); + VERIFY_ARE_EQUAL(wcslen(pwszExpected), len); +} + +void TestMouseWheelReadConsoleInputHelper(const UINT msg, const DWORD dwEventFlagsExpected, const DWORD dwConsoleMode) +{ + if (!OneCoreDelay::IsIsWindowPresent()) + { + Log::Comment(L"Mouse wheel with respect to a window can't be checked on platform without classic window message queuing."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + HWND const hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd), L"Get console window handle to inject wheel messages."); + + HANDLE const hConsoleInput = GetStdInputHandle(); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hConsoleInput, dwConsoleMode), L"Apply the requested console mode"); + + // We don't generate mouse console event in QuickEditMode or if MouseInput is not enabled + DWORD dwExpectedEvents = 1; + if (dwConsoleMode & ENABLE_QUICK_EDIT_MODE || !(dwConsoleMode & ENABLE_MOUSE_INPUT)) + { + Log::Comment(L"QuickEditMode is set or MouseInput is not set, not expecting events"); + dwExpectedEvents = 0; + } + + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hConsoleInput), L"Flush input queue to make sure no one else is in the way."); + + // WM_MOUSEWHEEL params + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx + + // WPARAM is HIWORD the wheel delta and LOWORD the keystate (keys pressed with it) + // We want no keys pressed in the loword (0) and we want one tick of the wheel in the high word. + WPARAM wParam = 0; + short sKeyState = 0; + short sWheelDelta = -WHEEL_DELTA; // scroll down is negative, up is positive. + wParam = ((sWheelDelta << 16) | sKeyState) & 0xFFFFFFFF; // we only use the lower 32-bits (in case of 64-bit system) + + // LPARAM is positioning information. We don't care so we'll leave it 0x0 + LPARAM lParam = 0; + + Log::Comment(L"Send scroll down message into console window queue."); + SendMessageW(hwnd, msg, wParam, lParam); + + Sleep(250); // give message time to sink in + + DWORD dwAvailable = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(hConsoleInput, &dwAvailable), L"Retrieve number of events in queue."); + VERIFY_ARE_EQUAL(dwExpectedEvents, dwAvailable, + NoThrowString().Format(L"We expected %i event from our scroll message.", dwExpectedEvents)); + + INPUT_RECORD ir; + DWORD dwRead = 0; + if (dwExpectedEvents == 1) + { + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(hConsoleInput, &ir, 1, &dwRead), L"Read the event out."); + VERIFY_ARE_EQUAL(1u, dwRead); + + Log::Comment(L"Verify the event is what we expected. We only verify the fields relevant to this test."); + VERIFY_ARE_EQUAL(MOUSE_EVENT, ir.EventType); + VERIFY_ARE_EQUAL((DWORD)wParam, ir.Event.MouseEvent.dwButtonState); // hard cast OK. only using lower 32-bits (see above) + // Don't care about ctrl key state. Can be messed with by caps lock/numlock state. Not checking this. + VERIFY_ARE_EQUAL(dwEventFlagsExpected, ir.Event.MouseEvent.dwEventFlags); + // Don't care about mouse position for ensuring scroll message went through. + } +} + +void InputTests::TestMouseWheelReadConsoleMouseInput() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEWHEEL, MOUSE_WHEELED, dwInputMode); +} + +void InputTests::TestMouseHorizWheelReadConsoleMouseInput() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEHWHEEL, MOUSE_HWHEELED, dwInputMode); +} + +void InputTests::TestMouseWheelReadConsoleNoMouseInput() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_EXTENDED_FLAGS; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEWHEEL, MOUSE_WHEELED, dwInputMode); +} + +void InputTests::TestMouseHorizWheelReadConsoleNoMouseInput() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_EXTENDED_FLAGS; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEHWHEEL, MOUSE_HWHEELED, dwInputMode); +} + +void InputTests::TestMouseWheelReadConsoleInputQuickEdit() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEWHEEL, MOUSE_WHEELED, dwInputMode); +} + +void InputTests::TestMouseHorizWheelReadConsoleInputQuickEdit() +{ + const DWORD dwInputMode = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE; + TestMouseWheelReadConsoleInputHelper(WM_MOUSEHWHEEL, MOUSE_HWHEELED, dwInputMode); +} + +void InputTests::TestReadWaitOnHandle() +{ + HANDLE const hIn = GetStdInputHandle(); + VERIFY_IS_NOT_NULL(hIn, L"Check input handle is not null."); + + // Set up events and background thread to wait. + bool fAbortWait = false; + + // this will be signaled when we want the thread to start waiting on the input handle + // It is an auto-reset. + wil::unique_event doWait; + doWait.create(); + + // the thread will signal this when it is done waiting on the input handle. + // It is an auto-reset. + wil::unique_event doneWaiting; + doneWaiting.create(); + + std::thread bgThread([&] { + while (!fAbortWait) + { + doWait.wait(); + + if (fAbortWait) + { + break; + } + + HANDLE waits[2]; + waits[0] = doWait.get(); + waits[1] = hIn; + WaitForMultipleObjects(2, waits, FALSE, INFINITE); + + if (fAbortWait) + { + break; + } + + doneWaiting.SetEvent(); + } + }); + + auto onExit = wil::scope_exit([&] { + Log::Comment(L"Tell our background thread to abort waiting, signal it, then wait for it to exit before we finish the test."); + fAbortWait = true; + doWait.SetEvent(); + bgThread.join(); + }); + + Log::Comment(L"Test 1: Waiting for text to be appended to the buffer."); + // Empty the buffer and tell the thread to start waiting + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Ensure input buffer is empty."); + doWait.SetEvent(); + + // Send some input into the console. + INPUT_RECORD ir; + ir.EventType = MOUSE_EVENT; + ir.Event.MouseEvent.dwMousePosition.X = 1; + ir.Event.MouseEvent.dwMousePosition.Y = 1; + ir.Event.MouseEvent.dwButtonState = FROM_LEFT_1ST_BUTTON_PRESSED; + ir.Event.MouseEvent.dwControlKeyState = NUMLOCK_ON; + ir.Event.MouseEvent.dwEventFlags = 0; + + DWORD dwWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputW(hIn, &ir, 1, &dwWritten), L"Inject input event into queue."); + VERIFY_ARE_EQUAL(1u, dwWritten, L"Ensure 1 event was written."); + + VERIFY_IS_TRUE(doneWaiting.wait(5000), L"The input handle should have been signaled on our background thread within our 5 second timeout."); + + Log::Comment(L"Test 2: Trigger a VT response so the buffer will be prepended (things inserted at the front)."); + + HANDLE const hOut = GetStdOutputHandle(); + DWORD dwMode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(hOut, &dwMode), L"Get existing console mode."); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING), L"Ensure VT mode is on."); + + // Empty the buffer and tell the thread to start waiting + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Ensure input buffer is empty."); + doWait.SetEvent(); + + // Send a VT command that will trigger a response. + PCWSTR pwszDeviceAttributeRequest = L"\x1b[c"; + DWORD cchDeviceAttributeRequest = static_cast(wcslen(pwszDeviceAttributeRequest)); + dwWritten = 0; + WriteConsoleW(hOut, pwszDeviceAttributeRequest, cchDeviceAttributeRequest, &dwWritten, nullptr); + VERIFY_ARE_EQUAL(cchDeviceAttributeRequest, dwWritten, L"Verify string was written"); + + VERIFY_IS_TRUE(doneWaiting.wait(5000), L"The input handle should have been signaled on our background thread within our 5 second timeout."); +} + +void InputTests::TestVtInputGeneration() +{ + Log::Comment(L"Get input handle"); + HANDLE hIn = GetStdInputHandle(); + + DWORD dwMode; + GetConsoleMode(hIn, &dwMode); + + DWORD dwWritten = (DWORD)-1; + DWORD dwRead = (DWORD)-1; + INPUT_RECORD rgInputRecords[64] = {0}; + + Log::Comment(L"First make sure that an arrow keydown is not translated in not-VT mode"); + + dwMode = WI_ClearFlag(dwMode, ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(hIn, dwMode); + GetConsoleMode(hIn, &dwMode); + VERIFY_IS_FALSE(WI_IsFlagSet(dwMode, ENABLE_VIRTUAL_TERMINAL_INPUT)); + + rgInputRecords[0].EventType = KEY_EVENT; + rgInputRecords[0].Event.KeyEvent.bKeyDown = TRUE; + rgInputRecords[0].Event.KeyEvent.wRepeatCount = 1; + rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode = VK_UP; + + Log::Comment(L"Writing events"); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hIn, rgInputRecords, 1, &dwWritten)); + VERIFY_ARE_EQUAL(dwWritten, (DWORD)1); + + Log::Comment(L"Reading events"); + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(hIn, rgInputRecords, ARRAYSIZE(rgInputRecords), &dwRead)); + VERIFY_ARE_EQUAL(dwRead, (DWORD)1); + VERIFY_ARE_EQUAL(rgInputRecords[0].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(rgInputRecords[0].Event.KeyEvent.bKeyDown, TRUE); + VERIFY_ARE_EQUAL(rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode, VK_UP); + + + Log::Comment(L"Now, enable VT Input and make sure that a vt sequence comes out the other side."); + + dwMode = WI_SetFlag(dwMode, ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(hIn, dwMode); + GetConsoleMode(hIn, &dwMode); + VERIFY_IS_TRUE(WI_IsFlagSet(dwMode, ENABLE_VIRTUAL_TERMINAL_INPUT)); + + Log::Comment(L"Flushing"); + VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn)); + + rgInputRecords[0].EventType = KEY_EVENT; + rgInputRecords[0].Event.KeyEvent.bKeyDown = TRUE; + rgInputRecords[0].Event.KeyEvent.wRepeatCount = 1; + rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode = VK_UP; + + Log::Comment(L"Writing events"); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hIn, rgInputRecords, 1, &dwWritten)); + VERIFY_ARE_EQUAL(dwWritten, (DWORD)1); + + Log::Comment(L"Reading events"); + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(hIn, rgInputRecords, ARRAYSIZE(rgInputRecords), &dwRead)); + VERIFY_ARE_EQUAL(dwRead, (DWORD)3); + VERIFY_ARE_EQUAL(rgInputRecords[0].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(rgInputRecords[0].Event.KeyEvent.bKeyDown, TRUE); + VERIFY_ARE_EQUAL(rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode, 0); + VERIFY_ARE_EQUAL(rgInputRecords[0].Event.KeyEvent.uChar.UnicodeChar, L'\x1b'); + + VERIFY_ARE_EQUAL(rgInputRecords[1].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(rgInputRecords[1].Event.KeyEvent.bKeyDown, TRUE); + VERIFY_ARE_EQUAL(rgInputRecords[1].Event.KeyEvent.wVirtualKeyCode, 0); + VERIFY_ARE_EQUAL(rgInputRecords[1].Event.KeyEvent.uChar.UnicodeChar, L'['); + + VERIFY_ARE_EQUAL(rgInputRecords[2].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(rgInputRecords[2].Event.KeyEvent.bKeyDown, TRUE); + VERIFY_ARE_EQUAL(rgInputRecords[2].Event.KeyEvent.wVirtualKeyCode, 0); + VERIFY_ARE_EQUAL(rgInputRecords[2].Event.KeyEvent.uChar.UnicodeChar, L'A'); +} + +void InputTests::RawReadUnpacksCoalsescedInputRecords() +{ + DWORD mode = 0; + HANDLE hIn = GetStdInputHandle(); + const wchar_t writeWch = L'a'; + const auto repeatCount = 5; + + // turn on raw mode + GetConsoleMode(hIn, &mode); + WI_ClearFlag(mode, ENABLE_LINE_INPUT); + SetConsoleMode(hIn, mode); + + INPUT_RECORD record; + record.EventType = KEY_EVENT; + record.Event.KeyEvent.bKeyDown = TRUE; + record.Event.KeyEvent.wRepeatCount = repeatCount; + record.Event.KeyEvent.wVirtualKeyCode = writeWch; + record.Event.KeyEvent.uChar.UnicodeChar = writeWch; + + // write an event with a repeat count + DWORD writtenAmount = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInput(hIn, &record, 1, &writtenAmount)); + VERIFY_ARE_EQUAL(writtenAmount, static_cast(1)); + + // stream read the events out one at a time + DWORD eventCount = 0; + for (int i = 0; i < repeatCount; ++i) + { + eventCount = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(hIn, &eventCount)); + VERIFY_IS_TRUE(eventCount > 0); + + wchar_t wch; + DWORD readAmount; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsole(hIn, &wch, 1, &readAmount, NULL)); + VERIFY_ARE_EQUAL(readAmount, static_cast(1)); + VERIFY_ARE_EQUAL(wch, writeWch); + } + + // input buffer should now be empty + eventCount = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(hIn, &eventCount)); + VERIFY_ARE_EQUAL(eventCount, static_cast(0)); +} diff --git a/src/host/ft_host/API_ModeTests.cpp b/src/host/ft_host/API_ModeTests.cpp new file mode 100644 index 000000000..89f4bbc66 --- /dev/null +++ b/src/host/ft_host/API_ModeTests.cpp @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test: +// GetConsoleMode +// SetConsoleMode +class ModeTests +{ + BEGIN_TEST_CLASS(ModeTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + END_TEST_CLASS() + + TEST_METHOD_SETUP(TestSetup); + TEST_METHOD_CLEANUP(TestCleanup); + + TEST_METHOD(TestGetConsoleModeInvalid); + TEST_METHOD(TestSetConsoleModeInvalid); + + TEST_METHOD(TestConsoleModeInputScenario); + TEST_METHOD(TestConsoleModeScreenBufferScenario); + + TEST_METHOD(TestGetConsoleDisplayMode); + + TEST_METHOD(TestGetConsoleProcessList); +}; + +bool ModeTests::TestSetup() +{ + return Common::TestBufferSetup(); +} + +bool ModeTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void ModeTests::TestGetConsoleModeInvalid() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsPerfTest", L"true") + END_TEST_METHOD_PROPERTIES() + + DWORD dwConsoleMode = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(GetConsoleMode(INVALID_HANDLE_VALUE, &dwConsoleMode)); + VERIFY_ARE_EQUAL(dwConsoleMode, (DWORD)-1); + + dwConsoleMode = (DWORD)-1; + VERIFY_WIN32_BOOL_FAILED(GetConsoleMode(nullptr, &dwConsoleMode)); + VERIFY_ARE_EQUAL(dwConsoleMode, (DWORD)-1); +} + +void ModeTests::TestSetConsoleModeInvalid() +{ + VERIFY_WIN32_BOOL_FAILED(SetConsoleMode(INVALID_HANDLE_VALUE, 0)); + VERIFY_WIN32_BOOL_FAILED(SetConsoleMode(nullptr, 0)); + + HANDLE hConsoleInput = GetStdInputHandle(); + VERIFY_WIN32_BOOL_FAILED(SetConsoleMode(hConsoleInput, 0xFFFFFFFF), L"Can't set invalid input flags"); + VERIFY_WIN32_BOOL_FAILED(SetConsoleMode(hConsoleInput, ENABLE_ECHO_INPUT), + L"Can't set ENABLE_ECHO_INPUT without ENABLE_LINE_INPUT on input handle"); + + VERIFY_WIN32_BOOL_FAILED(SetConsoleMode(Common::_hConsole, 0xFFFFFFFF), L"Can't set invalid output flags"); +} + +void ModeTests::TestConsoleModeInputScenario() +{ + HANDLE hConsoleInput = GetStdInputHandle(); + + const DWORD dwInputModeToSet = ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_WINDOW_INPUT; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hConsoleInput, dwInputModeToSet), L"Set valid flags for input"); + + DWORD dwInputMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(hConsoleInput, &dwInputMode), L"Get recently set flags for input"); + VERIFY_ARE_EQUAL(dwInputMode, dwInputModeToSet, L"Make sure SetConsoleMode worked for input"); +} + +void ModeTests::TestConsoleModeScreenBufferScenario() +{ + const DWORD dwOutputModeToSet = ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(Common::_hConsole, dwOutputModeToSet), + L"Set initial output flags"); + + DWORD dwOutputMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(Common::_hConsole, &dwOutputMode), L"Get new output flags"); + VERIFY_ARE_EQUAL(dwOutputMode, dwOutputModeToSet, L"Make sure output flags applied appropriately"); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(Common::_hConsole, 0), L"Set zero output flags"); + + dwOutputMode = (DWORD)-1; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(Common::_hConsole, &dwOutputMode), L"Get zero output flags"); + VERIFY_ARE_EQUAL(dwOutputMode, (DWORD)0, L"Verify able to set zero output flags"); +} + +void ModeTests::TestGetConsoleDisplayMode() +{ + DWORD dwMode = 0; + SetLastError(0); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleDisplayMode(&dwMode)); + VERIFY_ARE_EQUAL(0u, GetLastError()); +} + +void ModeTests::TestGetConsoleProcessList() +{ + // Set last error to 0. + Log::Comment(L"Test null and 0"); + { + SetLastError(0); + VERIFY_ARE_EQUAL(0ul, GetConsoleProcessList(nullptr, 0), L"Return value should be 0"); + VERIFY_ARE_EQUAL(static_cast(ERROR_INVALID_PARAMETER), GetLastError(), L"Last error should be invalid parameter."); + } + + Log::Comment(L"Test null and a valid length"); + { + SetLastError(0); + VERIFY_ARE_EQUAL(0ul, GetConsoleProcessList(nullptr, 10), L"Return value should be 0"); + VERIFY_ARE_EQUAL(static_cast(ERROR_INVALID_PARAMETER), GetLastError(), L"Last error should be invalid parameter."); + } + + Log::Comment(L"Test valid buffer and a zero length"); + { + DWORD one = 0; + const DWORD oneOriginal = one; + SetLastError(0); + VERIFY_ARE_EQUAL(0ul, GetConsoleProcessList(&one, 0), L"Return value should be 0"); + VERIFY_ARE_EQUAL(static_cast(ERROR_INVALID_PARAMETER), GetLastError(), L"Last error should be invalid parameter."); + VERIFY_ARE_EQUAL(oneOriginal, one, L"Buffer should not have been touched."); + } + + Log::Comment(L"Test a valid buffer of length 1"); + { + DWORD one = 0; + const DWORD oneOriginal = one; + SetLastError(0); + VERIFY_ARE_EQUAL(2ul, GetConsoleProcessList(&one, 1), L"Return value should be 2 because there are at least two processes attached during tests."); + VERIFY_ARE_EQUAL(static_cast(ERROR_SUCCESS), GetLastError(), L"Last error should be success."); + VERIFY_ARE_EQUAL(oneOriginal, one, L"Buffer should not have been touched."); + } + + Log::Comment(L"Test a valid buffer of length 2"); + { + DWORD two[2] = { 0 }; + + SetLastError(0); + VERIFY_ARE_EQUAL(2ul, GetConsoleProcessList(two, 2), L"Return value should be 2 because there are at least two processes attached during tests."); + VERIFY_ARE_EQUAL(static_cast(ERROR_SUCCESS), GetLastError(), L"Last error should be success."); + VERIFY_ARE_NOT_EQUAL(0ul, two[0], L"Slot 0 was filled"); + VERIFY_ARE_NOT_EQUAL(0ul, two[1], L"Slot 1 was filled."); + } + + Log::Comment(L"Test a valid buffer of length 5"); + { + DWORD five[5] = { 0 }; + + SetLastError(0); + VERIFY_ARE_EQUAL(2ul, GetConsoleProcessList(five, 2), L"Return value should be 2 because there are at least two processes attached during tests."); + VERIFY_ARE_EQUAL(static_cast(ERROR_SUCCESS), GetLastError(), L"Last error should be success."); + VERIFY_ARE_NOT_EQUAL(0ul, five[0], L"Slot 0 was filled"); + VERIFY_ARE_NOT_EQUAL(0ul, five[1], L"Slot 1 was filled."); + VERIFY_ARE_EQUAL(0ul, five[2], L"Slot 2 is still empty."); + VERIFY_ARE_EQUAL(0ul, five[3], L"Slot 3 is still empty."); + VERIFY_ARE_EQUAL(0ul, five[4], L"Slot 4 is still empty."); + } +} diff --git a/src/host/ft_host/API_OutputTests.cpp b/src/host/ft_host/API_OutputTests.cpp new file mode 100644 index 000000000..e1253b697 --- /dev/null +++ b/src/host/ft_host/API_OutputTests.cpp @@ -0,0 +1,1062 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\types\inc\viewport.hpp" + +#include +#include +#include + +using namespace Microsoft::Console::Types; + +class OutputTests +{ + BEGIN_TEST_CLASS(OutputTests) + TEST_CLASS_PROPERTY(L"IsolationLevel", L"Method") + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + + TEST_CLASS_SETUP(TestSetup); + TEST_CLASS_CLEANUP(TestCleanup); + + TEST_METHOD(BasicReadConsoleOutputATest); + TEST_METHOD(BasicReadConsoleOutputWTest); + + TEST_METHOD(BasicWriteConsoleOutputWTest); + TEST_METHOD(BasicWriteConsoleOutputATest); + + TEST_METHOD(WriteConsoleOutputWOutsideBuffer); + TEST_METHOD(WriteConsoleOutputWWithClipping); + TEST_METHOD(WriteConsoleOutputWNegativePositions); + + TEST_METHOD(ReadConsoleOutputWOutsideBuffer); + TEST_METHOD(ReadConsoleOutputWWithClipping); + TEST_METHOD(ReadConsoleOutputWNegativePositions); + TEST_METHOD(ReadConsoleOutputWPartialUserBuffer); + + TEST_METHOD(WriteConsoleOutputCharacterWRunoff); + + TEST_METHOD(WriteConsoleOutputAttributeSimpleTest); + TEST_METHOD(WriteConsoleOutputAttributeCheckerTest); + + TEST_METHOD(WriteBackspaceTest); + + TEST_METHOD(WinPtyWrite); +}; + +bool OutputTests::TestSetup() +{ + return Common::TestBufferSetup(); +} + +bool OutputTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void OutputTests::BasicWriteConsoleOutputWTest() +{ + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + // Establish a writing region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Make a test value and fill an array (via a vector) full of it. + CHAR_INFO testValue; + testValue.Attributes = 0x3e; + testValue.Char.UnicodeChar = L' '; + + std::vector buffer(regionSize, testValue); + + // Call the API and confirm results. + SMALL_RECT affected = region; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(region, affected); +} + +void OutputTests::BasicWriteConsoleOutputATest() +{ + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + // Establish a writing region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Make a test value and fill an array (via a vector) full of it. + CHAR_INFO testValue; + testValue.Attributes = 0x3e; + testValue.Char.AsciiChar = ' '; + + std::vector buffer(regionSize, testValue); + + // Call the API and confirm results. + SMALL_RECT affected = region; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputA(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(region, affected); +} + +void OutputTests::WriteConsoleOutputWOutsideBuffer() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Make a test value and fill an array (via a vector) full of it. + CHAR_INFO testValue; + testValue.Attributes = 0x3e; + testValue.Char.UnicodeChar = L'A'; + + std::vector buffer(regionSize, testValue); + + // Call the API and confirm results. + + // move outside in X and Y directions + auto shiftedRegion = region; + shiftedRegion.Left += bufferSize.X; + shiftedRegion.Right += bufferSize.X; + shiftedRegion.Top += bufferSize.Y; + shiftedRegion.Bottom += bufferSize.Y; + + auto affected = shiftedRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(shiftedRegion, affected); + + // Read the entire buffer back and validate that we didn't write anything anywhere + const auto readBack = std::make_unique(sbiex.dwSize.X * sbiex.dwSize.Y); + SMALL_RECT readRegion = { 0 }; + readRegion.Bottom = sbiex.dwSize.Y - 1; + readRegion.Right = sbiex.dwSize.X - 1; + const auto readBefore = readRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, readBack.get(), sbiex.dwSize, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(readBefore, readRegion); + + for (auto row = 0; row < sbiex.dwSize.Y; row++) + { + for (auto col = 0; col < sbiex.dwSize.X; col++) + { + CHAR_INFO readItem = *(readBack.get() + (row * sbiex.dwSize.X) + col); + + CHAR_INFO expectedItem; + expectedItem.Char.UnicodeChar = L' '; + expectedItem.Attributes = sbiex.wAttributes; + + VERIFY_ARE_EQUAL(expectedItem, readItem); + } + } +} + +void OutputTests::WriteConsoleOutputWWithClipping() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Make a test value and fill an array (via a vector) full of it. + CHAR_INFO testValue; + testValue.Attributes = 0x3e; + testValue.Char.UnicodeChar = L'A'; + + std::vector buffer(regionSize, testValue); + + // Move the write region to get clipped in the X and the Y dimension. + auto adjustedRegion = region; + adjustedRegion.Left += 5; + adjustedRegion.Right += 5; + adjustedRegion.Top += bufferSize.Y / 2; + adjustedRegion.Bottom += bufferSize.Y / 2; + + auto expectedRegion = adjustedRegion; + expectedRegion.Left = max(0, adjustedRegion.Left); + expectedRegion.Top = max(0, adjustedRegion.Top); + expectedRegion.Right = min(bufferSize.X - 1, adjustedRegion.Right); + expectedRegion.Bottom = min(bufferSize.Y - 1, adjustedRegion.Bottom); + + // Call the API and confirm results. + auto affected = adjustedRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expectedRegion, affected); + + // Read the entire buffer back and validate that we only wrote where we expected to write + const auto readBack = std::make_unique(sbiex.dwSize.X * sbiex.dwSize.Y); + SMALL_RECT readRegion = { 0 }; + readRegion.Bottom = sbiex.dwSize.Y - 1; + readRegion.Right = sbiex.dwSize.X - 1; + const auto readBefore = readRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, readBack.get(), sbiex.dwSize, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(readBefore, readRegion); + + for (auto row = 0; row < sbiex.dwSize.Y; row++) + { + for (auto col = 0; col < sbiex.dwSize.X; col++) + { + CHAR_INFO readItem = *(readBack.get() + (row * sbiex.dwSize.X) + col); + + CHAR_INFO expectedItem; + if (affected.Top <= row && affected.Bottom >= row && affected.Left <= col && affected.Right >= col) + { + expectedItem = testValue; + } + else + { + expectedItem.Char.UnicodeChar = L' '; + expectedItem.Attributes = sbiex.wAttributes; + } + + VERIFY_ARE_EQUAL(expectedItem, readItem); + } + } +} + +void OutputTests::WriteConsoleOutputWNegativePositions() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Make a test value and fill an array (via a vector) full of it. + CHAR_INFO testValue; + testValue.Attributes = 0x3e; + testValue.Char.UnicodeChar = L'A'; + + std::vector buffer(regionSize, testValue); + + // Call the API and confirm results. + + // Move the write region to negative values in the X and Y dimension + auto adjustedRegion = region; + adjustedRegion.Left -= 3; + adjustedRegion.Right -= 3; + adjustedRegion.Top -= 10; + adjustedRegion.Bottom -= 10; + + auto expectedRegion = adjustedRegion; + expectedRegion.Left = max(0, adjustedRegion.Left); + expectedRegion.Top = max(0, adjustedRegion.Top); + expectedRegion.Right = min(bufferSize.X - 1, adjustedRegion.Right); + expectedRegion.Bottom = min(bufferSize.Y - 1, adjustedRegion.Bottom); + + // Call the API and confirm results. + auto affected = adjustedRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expectedRegion, affected); + + // Read the entire buffer back and validate that we only wrote where we expected to write + const auto readBack = std::make_unique(sbiex.dwSize.X * sbiex.dwSize.Y); + SMALL_RECT readRegion = { 0 }; + readRegion.Bottom = sbiex.dwSize.Y - 1; + readRegion.Right = sbiex.dwSize.X - 1; + const auto readBefore = readRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, readBack.get(), sbiex.dwSize, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(readBefore, readRegion); + + for (auto row = 0; row < sbiex.dwSize.Y; row++) + { + for (auto col = 0; col < sbiex.dwSize.X; col++) + { + CHAR_INFO readItem = *(readBack.get() + (row * sbiex.dwSize.X) + col); + + CHAR_INFO expectedItem; + if (affected.Top <= row && affected.Bottom >= row && affected.Left <= col && affected.Right >= col) + { + expectedItem = testValue; + } + else + { + expectedItem.Char.UnicodeChar = L' '; + expectedItem.Attributes = sbiex.wAttributes; + } + + VERIFY_ARE_EQUAL(expectedItem, readItem); + } + } + + // Set the region so the left will end up past the right + adjustedRegion = region; + adjustedRegion.Left = -(adjustedRegion.Right + 1); + affected = adjustedRegion; + VERIFY_WIN32_BOOL_FAILED(WriteConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); +} + +void OutputTests::WriteConsoleOutputCharacterWRunoff() +{ + // writes text that will not all fit on the screen to verify reported size is correct + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + COORD target{ bufferSize.X - 1, bufferSize.Y - 1}; + + const std::wstring text = L"hello"; + DWORD charsWritten = 0; + VERIFY_SUCCEEDED(WriteConsoleOutputCharacterW(consoleOutputHandle, + text.c_str(), + gsl::narrow(text.size()), + target, + &charsWritten)); + VERIFY_ARE_EQUAL(charsWritten, 1u); +} + +void OutputTests::WriteConsoleOutputAttributeSimpleTest() +{ + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + const DWORD size = 500; + const WORD setAttr = FOREGROUND_BLUE | BACKGROUND_RED; + const COORD coord{ 0, 0 }; + DWORD attrsWritten = 0; + WORD attributes[size]; + std::fill_n(attributes, size, setAttr); + + // write some attribute changes + VERIFY_SUCCEEDED(WriteConsoleOutputAttribute(consoleOutputHandle, attributes, size, coord, &attrsWritten)); + VERIFY_ARE_EQUAL(attrsWritten, size); + + // confirm change happened + WORD resultAttrs[size]; + DWORD attrsRead = 0; + VERIFY_SUCCEEDED(ReadConsoleOutputAttribute(consoleOutputHandle, resultAttrs, size, coord, &attrsRead)); + VERIFY_ARE_EQUAL(attrsRead, size); + + for (size_t i = 0; i < size; ++i) + { + VERIFY_ARE_EQUAL(attributes[i], resultAttrs[i]); + } +} + +void OutputTests::WriteConsoleOutputAttributeCheckerTest() +{ + // writes a red/green checkerboard pattern on top of some text and makes sure that the text and color attr + // changes roundtrip properly through the API + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + + const WORD red = BACKGROUND_RED; + const WORD green = BACKGROUND_GREEN; + + const DWORD height = 8; + const DWORD width = bufferSize.X; + // todo verify less than or equal to buffer size ^^^ + const DWORD size = width * height; + std::unique_ptr attrs = std::make_unique(size); + + std::generate(attrs.get(), attrs.get() + size, [=]() + { + static int i = 0; + return i++ % 2 == 0 ? red : green; + }); + + // write text + const COORD coord{ 0, 0 }; + DWORD charsWritten = 0; + std::unique_ptr wchs = std::make_unique(size); + std::fill_n(wchs.get(), size, L'*'); + VERIFY_SUCCEEDED(WriteConsoleOutputCharacter(consoleOutputHandle, wchs.get(), size, coord, &charsWritten)); + VERIFY_ARE_EQUAL(charsWritten, size); + + // write attribute changes + DWORD attrsWritten = 0; + VERIFY_SUCCEEDED(WriteConsoleOutputAttribute(consoleOutputHandle, attrs.get(), size, coord, &attrsWritten)); + VERIFY_ARE_EQUAL(attrsWritten, size); + + // get changed attributes + std::unique_ptr resultAttrs = std::make_unique(size); + DWORD attrsRead = 0; + VERIFY_SUCCEEDED(ReadConsoleOutputAttribute(consoleOutputHandle, resultAttrs.get(), size, coord, &attrsRead)); + VERIFY_ARE_EQUAL(attrsRead, size); + + // get text + std::unique_ptr resultWchs = std::make_unique(size); + DWORD charsRead = 0; + VERIFY_SUCCEEDED(ReadConsoleOutputCharacter(consoleOutputHandle, resultWchs.get(), size, coord, &charsRead)); + VERIFY_ARE_EQUAL(charsRead, size); + + // confirm that attributes were set without affecting text + for (size_t i = 0; i < size; ++i) + { + VERIFY_ARE_EQUAL(attrs[i], resultAttrs[i]); + VERIFY_ARE_EQUAL(wchs[i], resultWchs[i]); + } +} + +void OutputTests::WriteBackspaceTest() +{ + // Get output buffer information. + const auto hOut = GetStdOutputHandle(); + Log::Comment(NoThrowString().Format( + L"Outputing \"\\b \\b\" should behave the same as \"\b\", \" \", \"\b\" in seperate WriteConsoleW calls." + )); + + DWORD n = 0; + CONSOLE_SCREEN_BUFFER_INFO csbi = {0}; + COORD c = {0, 0}; + VERIFY_SUCCEEDED(SetConsoleCursorPosition(hOut, c)); + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"GoodX", 5, &n, nullptr)); + + VERIFY_SUCCEEDED(GetConsoleScreenBufferInfo(hOut, &csbi)); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.X, 5); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.Y, 0); + + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"\b", 1, &n, nullptr)); + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L" ", 1, &n, nullptr)); + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"\b", 1, &n, nullptr)); + + VERIFY_SUCCEEDED(GetConsoleScreenBufferInfo(hOut, &csbi)); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.X, 4); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.Y, 0); + + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"\n", 1, &n, nullptr)); + + VERIFY_SUCCEEDED(GetConsoleScreenBufferInfo(hOut, &csbi)); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.X, 0); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.Y, 1); + + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"badX", 4, &n, nullptr)); + + VERIFY_SUCCEEDED(GetConsoleScreenBufferInfo(hOut, &csbi)); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.X, 4); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.Y, 1); + + VERIFY_SUCCEEDED(WriteConsoleW(hOut, L"\b \b", 3, &n, nullptr)); + + VERIFY_SUCCEEDED(GetConsoleScreenBufferInfo(hOut, &csbi)); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.X, 3); + VERIFY_ARE_EQUAL(csbi.dwCursorPosition.Y, 1); + +} + +void OutputTests::BasicReadConsoleOutputATest() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.AsciiChar = 'A'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterA(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make an array that can hold the output + std::vector buffer(regionSize); + + // Call the API and confirm results + SMALL_RECT affected = region; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputA(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(region, affected); + + // Verify that all the data read matches what was expected. + for (const auto& ci : buffer) + { + VERIFY_ARE_EQUAL(ciFill, ci); + } +} + +void OutputTests::BasicReadConsoleOutputWTest() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.UnicodeChar = L'Z'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make an array that can hold the output + std::vector buffer(regionSize); + + // Call the API and confirm results + SMALL_RECT affected = region; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(region, affected); + + // Verify that all the data read matches what was expected. + for (const auto& ci : buffer) + { + VERIFY_ARE_EQUAL(ciFill, ci); + } +} + +void OutputTests::ReadConsoleOutputWOutsideBuffer() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.UnicodeChar = L'Z'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make a buffer to hold the read data + const CHAR_INFO ciEmpty = { 0 }; + std::vector buffer(regionSize, ciEmpty); + + // Try to read completely outside the buffer. + auto shiftedRegion = region; + shiftedRegion.Left += bufferSize.X; + shiftedRegion.Right += bufferSize.X; + shiftedRegion.Top += bufferSize.Y; + shiftedRegion.Bottom += bufferSize.Y; + + auto expectedRegion = shiftedRegion; + expectedRegion.Right = expectedRegion.Left - 1; + expectedRegion.Bottom = expectedRegion.Top - 1; + + auto affected = shiftedRegion; + VERIFY_WIN32_BOOL_FAILED(ReadConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expectedRegion, affected); + + // Verify that all the data read matches what was expected. + for (const auto& ci : buffer) + { + VERIFY_ARE_EQUAL(ciEmpty, ci); + } +} + +void OutputTests::ReadConsoleOutputWWithClipping() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.UnicodeChar = L'Z'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make a buffer to hold the read data + CHAR_INFO ciEmpty; + ciEmpty.Char.UnicodeChar = L'A'; + ciEmpty.Attributes = BACKGROUND_BLUE; + std::vector buffer(regionSize, ciEmpty); + + // Move the write region to get clipped in the X and the Y dimension. + auto adjustedRegion = region; + adjustedRegion.Left += 5; + adjustedRegion.Right += 5; + adjustedRegion.Top += bufferSize.Y / 2; + adjustedRegion.Bottom += bufferSize.Y / 2; + + auto expectedRegion = adjustedRegion; + expectedRegion.Left = max(0, adjustedRegion.Left); + expectedRegion.Top = max(0, adjustedRegion.Top); + expectedRegion.Right = min(bufferSize.X - 1, adjustedRegion.Right); + expectedRegion.Bottom = min(bufferSize.Y - 1, adjustedRegion.Bottom); + + // Call the API and confirm results. + // NOTE: We expect this to be broken for v1. It's always been wrong there (returning a clipped count of bytes instead of the whole rectangle). + auto affected = adjustedRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expectedRegion, affected); + + const auto affectedViewport = Viewport::FromInclusive(affected); + const auto filledBuffer = Viewport::FromDimensions({ 0, 0 }, affectedViewport.Dimensions()); + + for (SHORT row = 0; row < regionDimensions.Y; row++) + { + for (SHORT col = 0; col < regionDimensions.X; col++) + { + CHAR_INFO bufferItem = *(buffer.begin() + (row * regionDimensions.X) + col); + + CHAR_INFO expectedItem; + if (filledBuffer.IsInBounds({ col, row })) + { + expectedItem = ciFill; + } + else + { + expectedItem = ciEmpty; + } + + VERIFY_ARE_EQUAL(expectedItem, bufferItem); + } + } +} + +void OutputTests::ReadConsoleOutputWNegativePositions() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + const COORD regionOrigin{ 0, 0 }; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.UnicodeChar = L'Z'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make a buffer to hold the read data + CHAR_INFO ciEmpty; + ciEmpty.Char.UnicodeChar = L'A'; + ciEmpty.Attributes = BACKGROUND_BLUE; + std::vector buffer(regionSize, ciEmpty); + + // Move the read region to negative values in the X and Y dimension + auto adjustedRegion = region; + adjustedRegion.Left -= 3; + adjustedRegion.Right -= 3; + adjustedRegion.Top -= 10; + adjustedRegion.Bottom -= 10; + + auto expectedRegion = adjustedRegion; + expectedRegion.Left = max(0, adjustedRegion.Left); + expectedRegion.Top = max(0, adjustedRegion.Top); + expectedRegion.Right = min(bufferSize.X - 1, adjustedRegion.Right); + expectedRegion.Bottom = min(bufferSize.Y - 1, adjustedRegion.Bottom); + + // Call the API + // NOTE: Due to the same reason as the ReadConsoleOutputWWithClipping test (the v1 buffer told the driver the wrong return buffer byte length) + // we expect the test to fail on the v1 console. V2 reports the correct buffer byte length to the driver for the return payload. + auto affected = adjustedRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expectedRegion, affected); + + // Verify the data read affected only the expected area + const auto affectedViewport = Viewport::FromInclusive(affected); + + // Because of the negative origin, the API will report that it filled starting at the 0 coordinate, but it believed + // the original buffer's origin was at -3, -10. This means we have to read at that offset into the buffer we provided + // for the data we requested. + const auto filledBuffer = Viewport::FromDimensions({ 0, 0 }, affectedViewport.Dimensions()); + auto adjustedBuffer = Viewport::Offset(filledBuffer, { -adjustedRegion.Left, -adjustedRegion.Top }); + + for (SHORT row = 0; row < regionDimensions.Y; row++) + { + for (SHORT col = 0; col < regionDimensions.X; col++) + { + CHAR_INFO bufferItem = *(buffer.begin() + (row * regionDimensions.X) + col); + + CHAR_INFO expectedItem; + if (adjustedBuffer.IsInBounds({ col, row })) + { + expectedItem = ciFill; + } + else + { + expectedItem = ciEmpty; + } + + VERIFY_ARE_EQUAL(expectedItem, bufferItem); + } + } +} + +void OutputTests::ReadConsoleOutputWPartialUserBuffer() +{ + SetVerifyOutput vf(VerifyOutputSettings::LogOnlyFailures); + + // Get output buffer information. + const auto consoleOutputHandle = GetStdOutputHandle(); + SetConsoleActiveScreenBuffer(consoleOutputHandle); + + // OneCore systems can't adjust the window/buffer size, so we'll skip making it smaller. + // On Desktop systems, make it smaller so the test runs faster. + if (OneCoreDelay::IsIsWindowPresent()) + { + SMALL_RECT window = { 0 }; + window.Right = 5; + window.Bottom = 5; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleWindowInfo(consoleOutputHandle, true, &window)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleScreenBufferSize(consoleOutputHandle, { 20, 20 })); + } + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex{ 0 }; + sbiex.cbSize = sizeof(sbiex); + + // Get buffer information + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(consoleOutputHandle, &sbiex)); + const auto bufferSize = sbiex.dwSize; + const auto bufferLength = bufferSize.X * bufferSize.Y; + + // Establish a reading region that is the width of the buffer and half the height. + const SMALL_RECT region{ 0, 0, bufferSize.X - 1, bufferSize.Y / 2 }; + const COORD regionDimensions{ region.Right - region.Left + 1, region.Bottom - region.Top + 1 }; + const auto regionSize = regionDimensions.X * regionDimensions.Y; + + // Fill buffer with some data to read back. + CHAR_INFO ciFill = { 0 }; + ciFill.Char.UnicodeChar = L'Z'; + ciFill.Attributes = FOREGROUND_RED; + + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputCharacterW(consoleOutputHandle, ciFill.Char.AsciiChar, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(FillConsoleOutputAttribute(consoleOutputHandle, ciFill.Attributes, bufferLength, { 0, 0 }, &written)); + VERIFY_ARE_EQUAL(static_cast(bufferLength), written); + + // Make an array that can hold the output prefilled with some data so we can confirm it is untouched + CHAR_INFO ciEmpty; + ciEmpty.Char.UnicodeChar = L'A'; + ciEmpty.Attributes = BACKGROUND_BLUE; + std::vector buffer(regionSize, ciEmpty); + + // Only fill up a small portion of the region we allocated. + // We're going to set the origin to the middle and say we only want to read into/out of the bottom right corner. + const COORD regionOrigin{ regionDimensions.X / 2, regionDimensions.Y / 2 }; + + // Create the area that we expect to be filled with data. + SMALL_RECT expected; + expected.Left = regionOrigin.X; + expected.Right = regionDimensions.X - 1; + expected.Top = regionOrigin.Y; + expected.Bottom = regionDimensions.Y - 1; + + const auto filledExpected = Viewport::FromInclusive(expected); + + // translate the expected region into the origin at 0,0 because that's what the API will report. + expected.Right -= expected.Left; + expected.Left -= expected.Left; + expected.Bottom -= expected.Top; + expected.Top -= expected.Top; + + // Call the API and confirm results + SMALL_RECT affected = region; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(consoleOutputHandle, buffer.data(), regionDimensions, regionOrigin, &affected)); + VERIFY_ARE_EQUAL(expected, affected); + + // Verify that all the data read matches what was expected. + for (SHORT row = 0; row < regionDimensions.Y; row++) + { + for (SHORT col = 0; col < regionDimensions.X; col++) + { + CHAR_INFO bufferItem = *(buffer.begin() + (row * regionDimensions.X) + col); + + CHAR_INFO expectedItem; + if (filledExpected.IsInBounds({ col, row })) + { + expectedItem = ciFill; + } + else + { + expectedItem = ciEmpty; + } + + VERIFY_ARE_EQUAL(expectedItem, bufferItem); + } + } +} + +// Send "Select All", then spawn a thread to hit ESC a moment later. +static void WinPtyTestStartSelection() { + const HWND hwnd = GetConsoleWindow(); + const int SC_CONSOLE_SELECT_ALL = 0xFFF5; + SendMessage(hwnd, WM_SYSCOMMAND, SC_CONSOLE_SELECT_ALL, 0); + auto press_escape = std::thread([=]() { + Sleep(500); + SendMessage(hwnd, WM_CHAR, 27, 0x00010001); // 0x00010001 is the repeat count (1) and scan code (1) + }); + press_escape.detach(); +} + +template +static void WinPtyDoWriteTest( + const wchar_t *api_name, + T *api_ptr, + bool use_selection) { + if (use_selection) + WinPtyTestStartSelection(); + char buf[] = "1234567890567890567890567890\n"; + DWORD actual = 0; + const BOOL ret = api_ptr( + GetStdHandle(STD_OUTPUT_HANDLE), + buf, static_cast(strlen(buf)), &actual, NULL); + const DWORD last_error = GetLastError(); + VERIFY_IS_TRUE(ret && actual == strlen(buf), String().Format(L"%s: %s returned %d: actual=%u LastError=%u (%s)\n", + ((ret && actual == strlen(buf)) ? L"SUCCESS" : L"ERROR"), + api_name, ret, actual, last_error, + use_selection ? L"select" : L"no-select")); +} + +void OutputTests::WinPtyWrite() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:method", L"{0, 1}") + TEST_METHOD_PROPERTY(L"Data:selection", L"{true, false}") + END_TEST_METHOD_PROPERTIES(); + + if (!OneCoreDelay::IsIsWindowPresent()) + { + Log::Comment(L"Scenario requiring window message triggers can't be checked on platform without classic window operations."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + DWORD method; + bool selection; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"method", method), L"Get which function mode we should use"); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"selection", selection), L"Get whether we should use selection."); + + switch (method) + { + case 0: + WinPtyDoWriteTest(L"WriteConsoleA", WriteConsoleA, selection); + break; + case 1: + WinPtyDoWriteTest(L"WriteFile", WriteConsoleA, selection); + break; + default: + VERIFY_FAIL(L"Unknown test type."); + break; + } + +} diff --git a/src/host/ft_host/API_PolicyTests.cpp b/src/host/ft_host/API_PolicyTests.cpp new file mode 100644 index 000000000..d4382ee09 --- /dev/null +++ b/src/host/ft_host/API_PolicyTests.cpp @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test restrictions placed on APIs from within a UWP application context +class PolicyTests +{ + BEGIN_TEST_CLASS(PolicyTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + +// UAP test type doesn't work quite right in VSO, skip. We'll get it in the RI-TP internally. +#ifdef __INSIDE_WINDOWS + BEGIN_TEST_METHOD(WrongWayVerbsUAP) + TEST_METHOD_PROPERTY(L"RunAs", L"UAP") + END_TEST_METHOD(); +#endif + + BEGIN_TEST_METHOD(WrongWayVerbsUser) + TEST_METHOD_PROPERTY(L"RunAs", L"User") + END_TEST_METHOD(); +}; + +void DoWrongWayVerbTest(_In_ BOOL bResultExpected, _In_ DWORD dwStatusExpected) +{ + DWORD dwResult; + BOOL bResultActual; + + // Try to read the output buffer + { + Log::Comment(L"Read the output buffer using string commands."); + { + wchar_t pwsz[50]; + char psz[50]; + COORD coord = { 0 }; + + SetLastError(0); + bResultActual = ReadConsoleOutputCharacterW(GetStdOutputHandle(), pwsz, ARRAYSIZE(pwsz), coord, &dwResult); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + + SetLastError(0); + bResultActual = ReadConsoleOutputCharacterA(GetStdOutputHandle(), psz, ARRAYSIZE(psz), coord, &dwResult); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + + WORD attrs[50]; + SetLastError(0); + bResultActual = ReadConsoleOutputAttribute(GetStdOutputHandle(), static_cast(attrs), ARRAYSIZE(attrs), coord, &dwResult); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + } + + Log::Comment(L"Read the output buffer using CHAR_INFO commands."); + { + CHAR_INFO pci[50]; + COORD coordPos = { 0 }; + COORD coordPci; + coordPci.X = 50; + coordPci.Y = 1; + SMALL_RECT srPci; + srPci.Top = 1; + srPci.Bottom = 1; + srPci.Left = 1; + srPci.Right = 50; + + SetLastError(0); + bResultActual = ReadConsoleOutputW(GetStdOutputHandle(), pci, coordPci, coordPos, &srPci); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + + SetLastError(0); + bResultActual = ReadConsoleOutputA(GetStdOutputHandle(), pci, coordPci, coordPos, &srPci); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + } + } + + // Try to write the input buffer + Log::Comment(L"Write the input buffer using INPUT_RECORD commands."); + { + INPUT_RECORD ir[2]; + ir[0].EventType = KEY_EVENT; + ir[0].Event.KeyEvent.bKeyDown = TRUE; + ir[0].Event.KeyEvent.dwControlKeyState = 0; + ir[0].Event.KeyEvent.uChar.UnicodeChar = L'@'; + ir[0].Event.KeyEvent.wRepeatCount = 1; + ir[0].Event.KeyEvent.wVirtualKeyCode = ir[0].Event.KeyEvent.uChar.UnicodeChar; + ir[0].Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(ir[0].Event.KeyEvent.wVirtualKeyCode, MAPVK_VK_TO_VSC)); + ir[1] = ir[0]; + ir[1].Event.KeyEvent.bKeyDown = FALSE; + + SetLastError(0); + bResultActual = WriteConsoleInputW(GetStdInputHandle(), ir, ARRAYSIZE(ir), &dwResult); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + + SetLastError(0); + bResultActual = WriteConsoleInputA(GetStdInputHandle(), ir, ARRAYSIZE(ir), &dwResult); + VERIFY_ARE_EQUAL(bResultExpected, bResultActual); + VERIFY_ARE_EQUAL(dwStatusExpected, GetLastError()); + } +} + +#ifdef __INSIDE_WINDOWS +void PolicyTests::WrongWayVerbsUAP() +{ + Log::Comment(L"From the UAP environment, these functions should be access denied."); + DoWrongWayVerbTest(FALSE, ERROR_ACCESS_DENIED); +} +#endif + +void PolicyTests::WrongWayVerbsUser() +{ + Log::Comment(L"From the classic uer environment, these functions should return with a normal status code."); + DoWrongWayVerbTest(TRUE, ERROR_SUCCESS); +} diff --git a/src/host/ft_host/API_RgbColorTests.cpp b/src/host/ft_host/API_RgbColorTests.cpp new file mode 100644 index 000000000..4304c39f4 --- /dev/null +++ b/src/host/ft_host/API_RgbColorTests.cpp @@ -0,0 +1,449 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +HANDLE g_hOut = INVALID_HANDLE_VALUE; +CONSOLE_SCREEN_BUFFER_INFOEX g_sbiex_backup = { 0 }; +COORD g_cWriteSize = {16, 16}; + +const int LEGACY_MODE = 0; +const int VT_SIMPLE_MODE = 1; +const int VT_256_MODE = 2; +const int VT_RGB_MODE = 3; +const int VT_256_GRID_MODE = 4; + +// This class is intended to test boundary conditions for: +// SetConsoleActiveScreenBuffer +class RgbColorTests +{ + BEGIN_TEST_CLASS(RgbColorTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + END_TEST_CLASS() + + TEST_METHOD_SETUP(MethodSetup) + { + g_hOut = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD outMode; + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(g_hOut, &outMode)); + outMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT; + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(g_hOut, outMode)); + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + + BOOL fSuccess = GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + if (fSuccess) + { + sbiex.srWindow.Bottom++; // hack because the API is not good at roundtrip + + g_sbiex_backup = sbiex; + + // Set the Color table to a known color table + sbiex.ColorTable[0] = RGB(0x0000, 0x0000, 0x0000); + sbiex.ColorTable[1] = RGB(0x0000, 0x0000, 0x0080); + sbiex.ColorTable[2] = RGB(0x0000, 0x0080, 0x0000); + sbiex.ColorTable[3] = RGB(0x0000, 0x0080, 0x0080); + sbiex.ColorTable[4] = RGB(0x0080, 0x0000, 0x0000); + sbiex.ColorTable[5] = RGB(0x0080, 0x0000, 0x0080); + sbiex.ColorTable[6] = RGB(0x0080, 0x0080, 0x0000); + sbiex.ColorTable[7] = RGB(0x00C0, 0x00C0, 0x00C0); + sbiex.ColorTable[8] = RGB(0x0080, 0x0080, 0x0080); + sbiex.ColorTable[9] = RGB(0x0000, 0x0000, 0x00FF); + sbiex.ColorTable[10] = RGB(0x0000, 0x00FF, 0x0000); + sbiex.ColorTable[11] = RGB(0x0000, 0x00FF, 0x00FF); + sbiex.ColorTable[12] = RGB(0x00FF, 0x0000, 0x0000); + sbiex.ColorTable[13] = RGB(0x00FF, 0x0000, 0x00FF); + sbiex.ColorTable[14] = RGB(0x00FF, 0x00FF, 0x0000); + sbiex.ColorTable[15] = RGB(0x00FF, 0x00FF, 0x00FF); + + sbiex.dwCursorPosition.X = 0; + sbiex.dwCursorPosition.Y = 0; + + SetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + } + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + SetConsoleScreenBufferInfoEx(g_hOut, &g_sbiex_backup); + + return true; + } + TEST_METHOD(TestPureLegacy); + TEST_METHOD(TestVTSimpleToLegacy); + TEST_METHOD(TestVT256ToLegacy); + TEST_METHOD(TestVTRGBToLegacy); + TEST_METHOD(TestVT256Grid); +}; + +// Takes windows 16 color table index, and returns a int for the equivalent SGR sequence +WORD WinToVTColor(int winColor, bool isForeground) +{ + bool red = (winColor & FOREGROUND_RED) > 0; + bool green = (winColor & FOREGROUND_GREEN) > 0; + bool blue = (winColor & FOREGROUND_BLUE) > 0; + bool bright = (winColor & FOREGROUND_INTENSITY) > 0; + + WORD result = isForeground? 30 : 40; + result += bright? 60 : 0; + result += red? 1 : 0; + result += green? 2 : 0; + result += blue? 4 : 0; + + return result; +} + +WORD MakeAttribute(int fg, int bg) +{ + return (WORD)((bg << 4) | (fg)); +} + +// Takes a windows 16 color table index, and returns the equivalent xterm table index +// (also in [0,15]) +int WinToXtermIndex(int iWinColor) +{ + bool fRed = (iWinColor & 0x04) > 0; + bool fGreen = (iWinColor & 0x02) > 0; + bool fBlue = (iWinColor & 0x01) > 0; + bool fBright = (iWinColor & 0x08) > 0; + int iXtermTableEntry = (fRed? 0x1:0x0) | (fGreen? 0x2:0x0) | (fBlue? 0x4:0x0) | (fBright? 0x8:0x0); + return iXtermTableEntry; +} + +int WriteLegacyColorTestChars(int fg, int bg) +{ + DWORD numWritten = 0; + SetConsoleTextAttribute(g_hOut, MakeAttribute(fg, bg)); + WriteConsole(g_hOut, L"*", 1, &numWritten, nullptr); + return numWritten; +} + +int WriteVTSimpleTestChars(int fg, int bg) +{ + DWORD numWritten = 0; + wprintf(L"\x1b[%d;%dm", WinToVTColor(fg, true), WinToVTColor(bg, false)); + WriteConsole(g_hOut, L"*", 1, &numWritten, nullptr); + return numWritten; +} + +int WriteVT256TestChars(int fg, int bg) +{ + DWORD numWritten = 0; + wprintf(L"\x1b[38;5;%d;48;5;%dm", WinToXtermIndex(fg), WinToXtermIndex(bg)); + WriteConsole(g_hOut, L"*", 1, &numWritten, nullptr); + return numWritten; +} + +int WriteVT256GridTestChars(int fg, int bg) +{ + DWORD numWritten = 0; + WORD index = MakeAttribute(fg, bg); + wprintf(L"\x1b[38;5;%d;48;5;%dm", index, index); + WriteConsole(g_hOut, L"*", 1, &numWritten, nullptr); + return numWritten; +} + +int WriteVTRGBTestChars(int fg, int bg) +{ + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COLORREF fgColor = sbiex.ColorTable[fg]; + COLORREF bgColor = sbiex.ColorTable[bg]; + + int fgRed = GetRValue(fgColor); + int fgBlue = GetBValue(fgColor); + int fgGreen = GetGValue(fgColor); + + int bgRed = GetRValue(bgColor); + int bgBlue = GetBValue(bgColor); + int bgGreen = GetGValue(bgColor); + + DWORD numWritten = 0; + wprintf(L"\x1b[38;2;%d;%d;%d;48;2;%d;%d;%dm", fgRed, fgGreen, fgBlue, bgRed, bgGreen, bgBlue); + WriteConsole(g_hOut, L"*", 1, &numWritten, nullptr); + return numWritten; +} + +BOOL CreateColorGrid(int iColorMode) +{ + COORD coordCursor = { 0 }; + BOOL fSuccess = SetConsoleCursorPosition(g_hOut, coordCursor); + + if (fSuccess) + { + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + if (fSuccess) + { + DWORD totalWritten = 0; + COORD writeSize = g_cWriteSize; + COORD cursorPosInitial = sbiex.dwCursorPosition; + for (int fg = 0; fg < writeSize.Y; fg++) + { + DWORD numWritten = 0; + for (int bg = 0; bg < writeSize.X; bg++) + { + switch (iColorMode) + { + case LEGACY_MODE: + numWritten = WriteLegacyColorTestChars(fg, bg); + totalWritten += numWritten; + break; + case VT_SIMPLE_MODE: + numWritten = WriteVTSimpleTestChars(fg, bg); + totalWritten += numWritten; + break; + case VT_256_MODE: + numWritten = WriteVT256TestChars(fg, bg); + totalWritten += numWritten; + break; + case VT_RGB_MODE: + numWritten = WriteVTRGBTestChars(fg, bg); + totalWritten += numWritten; + break; + case VT_256_GRID_MODE: + numWritten = WriteVT256GridTestChars(fg, bg); + totalWritten += numWritten; + break; + default: + VERIFY_ARE_EQUAL(true, false, L"Did not provide a valid color mode"); + + } + } + switch (iColorMode) + { + case LEGACY_MODE: + SetConsoleTextAttribute(g_hOut, sbiex.wAttributes); + WriteConsole(g_hOut, L"\n", 1, &numWritten, nullptr); + break; + case VT_SIMPLE_MODE: + case VT_256_MODE: + case VT_RGB_MODE: + case VT_256_GRID_MODE: + wprintf(L"\x1b[0m\n"); + break; + default: + VERIFY_ARE_EQUAL(true, false, L"Did not provide a valid color mode"); + + } + } + fSuccess = totalWritten == (1 * 16 * 16); + } + } + + return fSuccess; + +} + +BOOL CreateLegacyColorGrid() +{ + return CreateColorGrid(LEGACY_MODE); +} + +WORD GetGridAttrs(int x, int y, CHAR_INFO* pBuffer, COORD cGridSize) +{ + return (pBuffer[(cGridSize.X * y) + x]).Attributes; +} + +BOOL ValidateLegacyColorGrid(COORD cursorPosInitial) +{ + COORD actualWriteSize; + actualWriteSize.X = 16; + actualWriteSize.Y = 16; + + CHAR_INFO* rOutputBuffer = new CHAR_INFO[actualWriteSize.X * actualWriteSize.Y]; + + SMALL_RECT srReadRegion = {0}; + srReadRegion.Top = cursorPosInitial.Y; + srReadRegion.Left = cursorPosInitial.X; + srReadRegion.Right = srReadRegion.Left + actualWriteSize.X; + srReadRegion.Bottom = srReadRegion.Top + actualWriteSize.Y; + + BOOL fSuccess = ReadConsoleOutput(g_hOut, rOutputBuffer, actualWriteSize, {0}, &srReadRegion); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Read the output back"); + if (fSuccess) + { + CHAR_INFO* pInfo = rOutputBuffer; + for (int fg = 0; fg < g_cWriteSize.Y; fg++) + { + for (int bg = 0; bg < g_cWriteSize.X; bg++) + { + WORD wExpected = MakeAttribute(fg, bg); + VERIFY_ARE_EQUAL(pInfo->Attributes, wExpected, NoThrowString().Format(L"fg, bg = (%d,%d)", fg, bg)); + fSuccess &= pInfo->Attributes == wExpected; + pInfo+=1;// We wrote one character each time + } + } + } + delete[] rOutputBuffer; + + return fSuccess; +} + +BOOL Validate256GridToLegacy(COORD cursorPosInitial) +{ + COORD actualWriteSize; + actualWriteSize.X = 16; + actualWriteSize.Y = 16; + + CHAR_INFO* rOutputBuffer = new CHAR_INFO[actualWriteSize.X * actualWriteSize.Y]; + + SMALL_RECT srReadRegion = {0}; + srReadRegion.Top = cursorPosInitial.Y; + srReadRegion.Left = cursorPosInitial.X; + srReadRegion.Right = srReadRegion.Left + actualWriteSize.X; + srReadRegion.Bottom = srReadRegion.Top + actualWriteSize.Y; + + BOOL fSuccess = ReadConsoleOutput(g_hOut, rOutputBuffer, actualWriteSize, {0}, &srReadRegion); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Read the output back"); + if (fSuccess) + { + // Validate some locations on the grid with what we know they'll be translated to. + // First column has the 16 colors from the table, in VT order + VERIFY_ARE_EQUAL(GetGridAttrs(0, 0, rOutputBuffer, actualWriteSize), MakeAttribute(0x0, 0x0)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 1, rOutputBuffer, actualWriteSize), MakeAttribute(0x4, 0x4)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 2, rOutputBuffer, actualWriteSize), MakeAttribute(0x2, 0x2)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 3, rOutputBuffer, actualWriteSize), MakeAttribute(0x6, 0x6)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 4, rOutputBuffer, actualWriteSize), MakeAttribute(0x1, 0x1)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 5, rOutputBuffer, actualWriteSize), MakeAttribute(0x5, 0x5)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 6, rOutputBuffer, actualWriteSize), MakeAttribute(0x3, 0x3)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 7, rOutputBuffer, actualWriteSize), MakeAttribute(0x7, 0x7)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 8, rOutputBuffer, actualWriteSize), MakeAttribute(0x8, 0x8)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 9, rOutputBuffer, actualWriteSize), MakeAttribute(0xC, 0xC)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 10, rOutputBuffer, actualWriteSize), MakeAttribute(0xA, 0xA)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 11, rOutputBuffer, actualWriteSize), MakeAttribute(0xE, 0xE)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 12, rOutputBuffer, actualWriteSize), MakeAttribute(0x9, 0x9)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 13, rOutputBuffer, actualWriteSize), MakeAttribute(0xD, 0xD)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 14, rOutputBuffer, actualWriteSize), MakeAttribute(0xB, 0xB)); + VERIFY_ARE_EQUAL(GetGridAttrs(0, 15, rOutputBuffer, actualWriteSize), MakeAttribute(0xF, 0xF)); + + // Verify some other locations in the table, that will be RGB->Legacy conversions. + VERIFY_ARE_EQUAL(GetGridAttrs(1, 1, rOutputBuffer, actualWriteSize), MakeAttribute(0x1, 0x1)); + VERIFY_ARE_EQUAL(GetGridAttrs(2, 1, rOutputBuffer, actualWriteSize), MakeAttribute(0xB, 0xB)); + VERIFY_ARE_EQUAL(GetGridAttrs(2, 2, rOutputBuffer, actualWriteSize), MakeAttribute(0x2, 0x2)); + VERIFY_ARE_EQUAL(GetGridAttrs(2, 3, rOutputBuffer, actualWriteSize), MakeAttribute(0x3, 0x3)); + VERIFY_ARE_EQUAL(GetGridAttrs(3, 4, rOutputBuffer, actualWriteSize), MakeAttribute(0x4, 0x4)); + VERIFY_ARE_EQUAL(GetGridAttrs(3, 5, rOutputBuffer, actualWriteSize), MakeAttribute(0x5, 0x5)); + VERIFY_ARE_EQUAL(GetGridAttrs(4, 5, rOutputBuffer, actualWriteSize), MakeAttribute(0x9, 0x9)); + VERIFY_ARE_EQUAL(GetGridAttrs(4, 6, rOutputBuffer, actualWriteSize), MakeAttribute(0x6, 0x6)); + VERIFY_ARE_EQUAL(GetGridAttrs(4, 7, rOutputBuffer, actualWriteSize), MakeAttribute(0x7, 0x7)); + VERIFY_ARE_EQUAL(GetGridAttrs(3, 11, rOutputBuffer, actualWriteSize), MakeAttribute(0x8, 0x8)); + VERIFY_ARE_EQUAL(GetGridAttrs(3, 12, rOutputBuffer, actualWriteSize), MakeAttribute(0x1, 0x1)); + VERIFY_ARE_EQUAL(GetGridAttrs(4, 12, rOutputBuffer, actualWriteSize), MakeAttribute(0xA, 0xA)); + VERIFY_ARE_EQUAL(GetGridAttrs(5, 12, rOutputBuffer, actualWriteSize), MakeAttribute(0xD, 0xD)); + VERIFY_ARE_EQUAL(GetGridAttrs(10, 12, rOutputBuffer, actualWriteSize), MakeAttribute(0xE, 0xE)); + VERIFY_ARE_EQUAL(GetGridAttrs(10, 13, rOutputBuffer, actualWriteSize), MakeAttribute(0xC, 0xC)); + VERIFY_ARE_EQUAL(GetGridAttrs(11, 13, rOutputBuffer, actualWriteSize), MakeAttribute(0xF, 0xF)); + + // Greyscale ramp + VERIFY_ARE_EQUAL(GetGridAttrs(14, 8, rOutputBuffer, actualWriteSize), MakeAttribute(0x0, 0x0)); + VERIFY_ARE_EQUAL(GetGridAttrs(14, 9, rOutputBuffer, actualWriteSize), MakeAttribute(0x0, 0x0)); + + VERIFY_ARE_EQUAL(GetGridAttrs(14, 14, rOutputBuffer, actualWriteSize), MakeAttribute(0x8, 0x8)); + VERIFY_ARE_EQUAL(GetGridAttrs(14, 15, rOutputBuffer, actualWriteSize), MakeAttribute(0x8, 0x8)); + VERIFY_ARE_EQUAL(GetGridAttrs(15, 0, rOutputBuffer, actualWriteSize), MakeAttribute(0x8, 0x8)); + + VERIFY_ARE_EQUAL(GetGridAttrs(15, 8, rOutputBuffer, actualWriteSize), MakeAttribute(0x7, 0x7)); + VERIFY_ARE_EQUAL(GetGridAttrs(15, 9, rOutputBuffer, actualWriteSize), MakeAttribute(0x7, 0x7)); + + VERIFY_ARE_EQUAL(GetGridAttrs(15, 14, rOutputBuffer, actualWriteSize), MakeAttribute(0xF, 0xF)); + VERIFY_ARE_EQUAL(GetGridAttrs(15, 15, rOutputBuffer, actualWriteSize), MakeAttribute(0xF, 0xF)); + + + } + delete[] rOutputBuffer; + return fSuccess; +} + +void RgbColorTests::TestPureLegacy() +{ + BOOL fSuccess; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = CreateLegacyColorGrid(); + if (fSuccess) + { + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COORD actualPos = sbiex.dwCursorPosition; + // Subtract the size of the grid to get back to the top of it. + actualPos.Y -= g_cWriteSize.Y; + fSuccess = ValidateLegacyColorGrid(actualPos); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Validated Legacy Color Grid"); + } +} + +void RgbColorTests::TestVTSimpleToLegacy() +{ + BOOL fSuccess; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = CreateColorGrid(VT_SIMPLE_MODE); + if (fSuccess) + { + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COORD actualPos = sbiex.dwCursorPosition; + // Subtract the size of the grid to get back to the top of it. + actualPos.Y -= g_cWriteSize.Y; + fSuccess = ValidateLegacyColorGrid(actualPos); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Validated Simple VT Color Grid"); + } +} + +void RgbColorTests::TestVT256ToLegacy() +{ + BOOL fSuccess; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = CreateColorGrid(VT_256_MODE); + if (fSuccess) + { + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COORD actualPos = sbiex.dwCursorPosition; + // Subtract the size of the grid to get back to the top of it. + actualPos.Y -= g_cWriteSize.Y; + fSuccess = ValidateLegacyColorGrid(actualPos); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Validated 256 Table VT Color Grid"); + } +} + +void RgbColorTests::TestVTRGBToLegacy() +{ + BOOL fSuccess; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = CreateColorGrid(VT_RGB_MODE); + if (fSuccess) + { + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COORD actualPos = sbiex.dwCursorPosition; + // Subtract the size of the grid to get back to the top of it. + actualPos.Y -= g_cWriteSize.Y; + fSuccess = ValidateLegacyColorGrid(actualPos); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Validated RGB VT Color Grid"); + } +} + +void RgbColorTests::TestVT256Grid() +{ + BOOL fSuccess; + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + fSuccess = CreateColorGrid(VT_256_GRID_MODE); + if (fSuccess) + { + GetConsoleScreenBufferInfoEx(g_hOut, &sbiex); + COORD actualPos = sbiex.dwCursorPosition; + // Subtract the size of the grid to get back to the top of it. + actualPos.Y -= g_cWriteSize.Y; + fSuccess = Validate256GridToLegacy(actualPos); + VERIFY_WIN32_BOOL_SUCCEEDED(fSuccess, L"Validated VT 256 Color Grid to Legacy Attributes"); + } +} diff --git a/src/host/ft_host/API_TitleTests.cpp b/src/host/ft_host/API_TitleTests.cpp new file mode 100644 index 000000000..08f748f18 --- /dev/null +++ b/src/host/ft_host/API_TitleTests.cpp @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to test: +// GetConsoleTitle +class TitleTests +{ + BEGIN_TEST_CLASS(TitleTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"api-ms-win-core-console-ansi-l2-1-0.lib") + END_TEST_CLASS() + + TEST_METHOD_SETUP(TestSetup); + TEST_METHOD_CLEANUP(TestCleanup); + + TEST_METHOD(TestGetConsoleTitleA); + TEST_METHOD(TestGetConsoleTitleW); +}; + +bool TitleTests::TestSetup() +{ + return Common::TestBufferSetup(); +} + +bool TitleTests::TestCleanup() +{ + return Common::TestBufferCleanup(); +} + +void TestGetConsoleTitleAFillHelper(_Out_writes_all_(cchBuffer) char* const chBuffer, + const size_t cchBuffer, + const char chFill) +{ + for (size_t i = 0; i < cchBuffer; i++) + { + chBuffer[i] = chFill; + } +} + +void TestGetConsoleTitleWFillHelper(_Out_writes_all_(cchBuffer) wchar_t* const wchBuffer, + const size_t cchBuffer, + const wchar_t wchFill) +{ + for (size_t i = 0; i < cchBuffer; i++) + { + wchBuffer[i] = wchFill; + } +} + +void TestGetConsoleTitleAPrepExpectedHelper(_In_reads_(cchTitle) const char* const chTitle, + const size_t cchTitle, + _Inout_updates_all_(cchReadBuffer) char* const chReadBuffer, + const size_t cchReadBuffer, + _Inout_updates_all_(cchReadExpected) char* const chReadExpected, + const size_t cchReadExpected, + const size_t cchTryToRead) +{ + // Fill our read buffer and expected with all Zs to start + TestGetConsoleTitleAFillHelper(chReadBuffer, cchReadBuffer, 'Z'); + TestGetConsoleTitleAFillHelper(chReadExpected, cchReadExpected, 'Z'); + + // Prep expected data + if (cchTryToRead >= cchTitle - 1) + { + VERIFY_SUCCEEDED(StringCchCopyNA(chReadExpected, cchReadExpected, chTitle, cchTryToRead)); // Copy as much room as we said we had leaving space for null terminator + + if (cchTryToRead == cchTitle - 1) + { + chReadExpected[cchTryToRead] = 'Z'; + } + } + else + { + chReadExpected[0] = '\0'; + } + +} + +void TestGetConsoleTitleWPrepExpectedHelper(_In_reads_(cchTitle) const wchar_t* const wchTitle, + const size_t cchTitle, + _Inout_updates_all_(cchReadBuffer) wchar_t* const wchReadBuffer, + const size_t cchReadBuffer, + _Inout_updates_all_(cchReadExpected) wchar_t* const wchReadExpected, + const size_t cchReadExpected, + const size_t cchTryToRead) +{ + // Fill our read buffer and expected with all Zs to start + TestGetConsoleTitleWFillHelper(wchReadBuffer, cchReadBuffer, L'Z'); + TestGetConsoleTitleWFillHelper(wchReadExpected, cchReadExpected, L'Z'); + + // Prep expected data + size_t const cchCopy = min(cchTitle, cchTryToRead); + VERIFY_SUCCEEDED(StringCchCopyNW(wchReadExpected, cchReadBuffer, wchTitle, cchCopy - 1)); // Copy as much room as we said we had leaving space for null terminator +} + +void TestGetConsoleTitleAVerifyHelper(_Inout_updates_(cchReadBuffer) char* const chReadBuffer, + const size_t cchReadBuffer, + const size_t cchTryToRead, + const DWORD dwExpectedRetVal, + const DWORD dwExpectedLastError, + _In_reads_(cchExpected) const char* const chReadExpected, + const size_t cchExpected) +{ + VERIFY_ARE_EQUAL(cchExpected, cchReadBuffer); + + SetLastError(0); + DWORD const dwRetVal = GetConsoleTitleA(chReadBuffer, (DWORD)cchTryToRead); + DWORD const dwLastError = GetLastError(); + + VERIFY_ARE_EQUAL(dwExpectedRetVal, dwRetVal); + VERIFY_ARE_EQUAL(dwExpectedLastError, dwLastError); + + if (chReadExpected != nullptr) + { + for (size_t i = 0; i < cchExpected; i++) + { + wchar_t const wchExpectedVis = chReadExpected[i] < 0x30 ? (wchar_t)chReadExpected[i] + 0x2400 : chReadExpected[i]; + wchar_t const wchBufferVis = chReadBuffer[i] < 0x30 ? (wchar_t)chReadBuffer[i] + 0x2400 : chReadBuffer[i]; + + // We must verify every individual character, not as a string, because we might be expecting a null + // in the middle and need to verify it then keep going and read what's past that. + VERIFY_ARE_EQUAL(chReadExpected[i], chReadBuffer[i], NoThrowString().Format(L"%c (0x%02x) == %c (0x%02x)", wchExpectedVis, chReadExpected[i], wchBufferVis, chReadBuffer[i])); + } + } + else + { + VERIFY_ARE_EQUAL(chReadExpected, chReadBuffer); + VERIFY_ARE_EQUAL(0u, cchTryToRead); + } +} + +void TestGetConsoleTitleWVerifyHelper(_Inout_updates_(cchReadBuffer) wchar_t* const wchReadBuffer, + const size_t cchReadBuffer, + const size_t cchTryToRead, + const DWORD dwExpectedRetVal, + const DWORD dwExpectedLastError, + _In_reads_(cchExpected) const wchar_t* const wchReadExpected, + const size_t cchExpected) +{ + VERIFY_ARE_EQUAL(cchExpected, cchReadBuffer); + + SetLastError(0); + DWORD const dwRetVal = GetConsoleTitleW(wchReadBuffer, (DWORD)cchTryToRead); + DWORD const dwLastError = GetLastError(); + + VERIFY_ARE_EQUAL(dwExpectedRetVal, dwRetVal); + VERIFY_ARE_EQUAL(dwExpectedLastError, dwLastError); + + if (wchReadExpected != nullptr) + { + for (size_t i = 0; i < cchExpected; i++) + { + wchar_t const wchExpectedVis = wchReadExpected[i] < 0x30 ? wchReadExpected[i] + 0x2400 : wchReadExpected[i]; + wchar_t const wchBufferVis = wchReadBuffer[i] < 0x30 ? wchReadBuffer[i] + 0x2400 : wchReadBuffer[i]; + + // We must verify every individual character, not as a string, because we might be expecting a null + // in the middle and need to verify it then keep going and read what's past that. + VERIFY_ARE_EQUAL(wchReadExpected[i], wchReadBuffer[i], NoThrowString().Format(L"%c (0x%02x) == %c (0x%02x)", wchExpectedVis, wchReadExpected[i], wchBufferVis, wchReadBuffer[i])); + } + } + else + { + VERIFY_ARE_EQUAL(wchReadExpected, wchReadBuffer); + VERIFY_ARE_EQUAL(0u, cchTryToRead); + } +} + +void TitleTests::TestGetConsoleTitleA() +{ + const char* const szTestTitle = "TestTitle"; + size_t const cchTestTitle = strlen(szTestTitle); + + Log::Comment(NoThrowString().Format(L"Set up the initial console title to '%S'.", szTestTitle)); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleTitleA(szTestTitle)); + + size_t cchReadBuffer = cchTestTitle + 1 + 4; // string length + null terminator + 4 bonus spots to check overruns/extra length. + wistd::unique_ptr chReadBuffer = wil::make_unique_nothrow(cchReadBuffer); + VERIFY_IS_NOT_NULL(chReadBuffer.get()); + + wistd::unique_ptr chReadExpected = wil::make_unique_nothrow(cchReadBuffer); + VERIFY_IS_NOT_NULL(chReadExpected.get()); + + size_t cchTryToRead = 0; + + Log::Comment(L"Test 1: Say we have half the buffer size necessary."); + cchTryToRead = cchTestTitle / 2; + + // Prepare the buffers and expected data + TestGetConsoleTitleAPrepExpectedHelper(szTestTitle, + cchTestTitle + 1, + chReadBuffer.get(), + cchReadBuffer, + chReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleAVerifyHelper(chReadBuffer.get(), cchReadBuffer, cchTryToRead, 0, S_OK, chReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 2: Say we have have exactly the string length with no null space."); + cchTryToRead = cchTestTitle; + + // Prepare the buffers and expected data + TestGetConsoleTitleAPrepExpectedHelper(szTestTitle, + cchTestTitle + 1, + chReadBuffer.get(), + cchReadBuffer, + chReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleAVerifyHelper(chReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, chReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 3: Say we have have the string length plus one null space."); + cchTryToRead = cchTestTitle + 1; + + // Prepare the buffers and expected data + TestGetConsoleTitleAPrepExpectedHelper(szTestTitle, + cchTestTitle + 1, + chReadBuffer.get(), + cchReadBuffer, + chReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleAVerifyHelper(chReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, chReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 4: Say we have the string length with a null space and an extra space."); + cchTryToRead = cchTestTitle + 1 + 1; + + // Prepare the buffers and expected data + TestGetConsoleTitleAPrepExpectedHelper(szTestTitle, + cchTestTitle + 1, + chReadBuffer.get(), + cchReadBuffer, + chReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleAVerifyHelper(chReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, chReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 5: Say we have no buffer."); + cchTryToRead = cchTestTitle; + + // Run the call and test it out. + TestGetConsoleTitleAVerifyHelper(nullptr, 0, 0, 0, S_OK, nullptr, 0); +} + + +void TitleTests::TestGetConsoleTitleW() +{ + const wchar_t* const wszTestTitle = L"TestTitle"; + size_t const cchTestTitle = wcslen(wszTestTitle); + + Log::Comment(NoThrowString().Format(L"Set up the initial console title to '%s'.", wszTestTitle)); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleTitleW(wszTestTitle)); + + size_t cchReadBuffer = cchTestTitle + 1 + 4; // string length + null terminator + 4 bonus spots to check overruns/extra length. + wistd::unique_ptr wchReadBuffer = wil::make_unique_nothrow(cchReadBuffer); + VERIFY_IS_NOT_NULL(wchReadBuffer.get()); + + wistd::unique_ptr wchReadExpected = wil::make_unique_nothrow(cchReadBuffer); + VERIFY_IS_NOT_NULL(wchReadExpected.get()); + + size_t cchTryToRead = 0; + + Log::Comment(L"Test 1: Say we have half the buffer size necessary."); + cchTryToRead = cchTestTitle / 2; + + // Prepare the buffers and expected data + TestGetConsoleTitleWPrepExpectedHelper(wszTestTitle, + cchTestTitle + 1, + wchReadBuffer.get(), + cchReadBuffer, + wchReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleWVerifyHelper(wchReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, wchReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 2: Say we have have exactly the string length with no null space."); + cchTryToRead = cchTestTitle; + + // Prepare the buffers and expected data + TestGetConsoleTitleWPrepExpectedHelper(wszTestTitle, + cchTestTitle + 1, + wchReadBuffer.get(), + cchReadBuffer, + wchReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleWVerifyHelper(wchReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, wchReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 3: Say we have have the string length plus one null space."); + cchTryToRead = cchTestTitle + 1; + + // Prepare the buffers and expected data + TestGetConsoleTitleWPrepExpectedHelper(wszTestTitle, + cchTestTitle + 1, + wchReadBuffer.get(), + cchReadBuffer, + wchReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleWVerifyHelper(wchReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, wchReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 4: Say we have the string length with a null space and an extra space."); + cchTryToRead = cchTestTitle + 1 + 1; + + // Prepare the buffers and expected data + TestGetConsoleTitleWPrepExpectedHelper(wszTestTitle, + cchTestTitle + 1, + wchReadBuffer.get(), + cchReadBuffer, + wchReadExpected.get(), + cchReadBuffer, + cchTryToRead); + + // Run the call and test it out. + TestGetConsoleTitleWVerifyHelper(wchReadBuffer.get(), cchReadBuffer, cchTryToRead, (DWORD)cchTestTitle, S_OK, wchReadExpected.get(), cchReadBuffer); + + Log::Comment(L"Test 5: Say we have no buffer."); + cchTryToRead = cchTestTitle; + + // Run the call and test it out. + TestGetConsoleTitleWVerifyHelper(nullptr, 0, 0, 0, S_OK, nullptr, 0); +} diff --git a/src/host/ft_host/CJK_DbcsTests.cpp b/src/host/ft_host/CJK_DbcsTests.cpp new file mode 100644 index 000000000..36f7dad4e --- /dev/null +++ b/src/host/ft_host/CJK_DbcsTests.cpp @@ -0,0 +1,2542 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include +#include +#include + +#define ENGLISH_US_CP 437u +#define JAPANESE_CP 932u + +namespace DbcsWriteRead +{ + enum WriteMode + { + CrtWrite = 0, + WriteConsoleOutputFunc = 1, + WriteConsoleOutputCharacterFunc = 2, + WriteConsoleFunc = 3 + }; + + enum ReadMode + { + ReadConsoleOutputFunc = 0, + ReadConsoleOutputCharacterFunc = 1 + }; + + void TestRunner(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + _In_opt_ WORD* const pwAttrOverride, + const bool fUseTrueType, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteInUnicode, + const DbcsWriteRead::ReadMode ReadMode, + const bool fReadWithUnicode); + + bool Setup(_In_ unsigned int uiCodePage, + _In_ bool fIsTrueType, + _Out_ HANDLE* const phOut, + _Out_ WORD* const pwAttributes); + + void SendOutput(const HANDLE hOut, _In_ unsigned int const uiCodePage, + const WriteMode WriteMode, const bool fIsUnicode, + _In_ PCSTR pszTestString, const WORD wAttr); + + void RetrieveOutput(const HANDLE hOut, + const DbcsWriteRead::ReadMode ReadMode, const bool fReadUnicode, + _Out_writes_(cChars) CHAR_INFO* const rgChars, const SHORT cChars); + + void Verify(_In_reads_(cExpected) CHAR_INFO* const rgExpected, const size_t cExpected, + _In_reads_(cExpected) CHAR_INFO* const rgActual); + + void PrepExpected(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const DbcsWriteRead::ReadMode ReadMode, + const bool fReadWithUnicode, + _Outptr_result_buffer_(*pcExpected) CHAR_INFO** const ppciExpected, + _Out_ size_t* const pcExpected); + + void PrepReadConsoleOutput(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const bool fReadWithUnicode, + _Inout_updates_all_(cExpectedNeeded) CHAR_INFO* const rgciExpected, + const size_t cExpectedNeeded); + + void PrepReadConsoleOutputCharacter(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const bool fReadWithUnicode, + _Inout_updates_all_(cExpectedNeeded) CHAR_INFO* const rgciExpected, + const size_t cExpectedNeeded); + + + namespace PrepPattern + { + // There are 14 different patterns that result from the various combinations of our APIs. + // These patterns are simply recognized based on the existing v1 console behavior and generated + // here as a black box test to maintain compatibility based on the variations in API usage. + // It can be assumed that calling this pattern means that the combinations of APIs used for the test + // resulted in output that looks like this pattern on the v1 console. + // + // All patterns will be documented with their sample before and afters above the comment. + // We will use *KI* to represent a Japanese Hiragana character that is romanized and + // no * to represent US ASCII text. + // + // We don't store the Hiragana directly in this file because Visual Studio and Git fight over the + // proper encoding of UTF-8. + + // 1 + void SpacePaddedDedupeW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 2 + void SpacePaddedDedupeTruncatedW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 3 + void NullPaddedDedupeW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 4 + void DoubledWNegativeOneTrailing(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 5 + void DoubledW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 6 + void A(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 7 + void AStompsWNegativeOnePatternTruncateSpacePadded(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 8 + void AOnDoubledWNegativeOneTrailing(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 9 + void AOnDoubledW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 10 + void WNullCoverAChar(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 11 + void WSpaceFill(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 12 + void ACoverAttrSpacePaddedDedupeTruncatedW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 13 + void SpacePaddedDedupeA(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + + // 14 + void TrueTypeCharANullWithAttrs(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected); + }; +}; + +class DbcsTests +{ + BEGIN_TEST_CLASS(DbcsTests) + TEST_CLASS_PROPERTY(L"IsolationLevel", L"Class") + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl2.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"api-ms-win-core-console-l1-2-1.lib") + END_TEST_CLASS(); + + TEST_METHOD_SETUP(DbcsTestSetup); + + // This test must come before ones that launch another process as launching another process can tamper with the codepage + // in ways that this test is not expecting. + TEST_METHOD(TestMultibyteInputRetrieval); + + BEGIN_TEST_METHOD(TestDbcsWriteRead) + TEST_METHOD_PROPERTY(L"Data:uiCodePage", L"{437, 932}") + TEST_METHOD_PROPERTY(L"Data:fUseTrueTypeFont", L"{true, false}") + TEST_METHOD_PROPERTY(L"Data:WriteMode", L"{0, 1, 2, 3}") + TEST_METHOD_PROPERTY(L"Data:fWriteInUnicode", L"{true, false}") + TEST_METHOD_PROPERTY(L"Data:ReadMode", L"{0, 1}") + TEST_METHOD_PROPERTY(L"Data:fReadInUnicode", L"{true, false}") + END_TEST_METHOD() + + TEST_METHOD(TestDbcsBisect); + + BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsBeginW) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsEndW) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsBeginA) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsBisectWriteCellsEndA) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsOneByOne) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsTrailLead) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestDbcsStdCoutScenario) + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD() +}; + +HANDLE hScreen = INVALID_HANDLE_VALUE; + +bool DbcsTests::DbcsTestSetup() +{ + return true; +} + +bool DbcsWriteRead::Setup(_In_ unsigned int uiCodePage, + _In_ bool fIsTrueType, + _Out_ HANDLE* const phOut, + _Out_ WORD* const pwAttributes) +{ + HANDLE const hOut = GetStdOutputHandle(); + + // Ensure that the console is set into the appropriate codepage for the test + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleCP(uiCodePage)); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(SetConsoleOutputCP(uiCodePage)); + + // Now set up the font. Many of these APIs are oddly dependent on font, so set as appropriate. + CONSOLE_FONT_INFOEX cfiex = { 0 }; + cfiex.cbSize = sizeof(cfiex); + if (!fIsTrueType) + { + // We use Terminal as the raster font name always. + wcscpy_s(cfiex.FaceName, L"Terminal"); + + // Use default raster font size from Japanese system. + cfiex.dwFontSize.X = 8; + cfiex.dwFontSize.Y = 18; + } + else + { + switch (uiCodePage) + { + case JAPANESE_CP: + wcscpy_s(cfiex.FaceName, L"MS Gothic"); + break; + case ENGLISH_US_CP: + wcscpy_s(cfiex.FaceName, L"Consolas"); + break; + } + + cfiex.dwFontSize.Y = 16; + } + + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(OneCoreDelay::SetCurrentConsoleFontEx(hOut, FALSE, &cfiex)); + + // Ensure that we set the font we expected to set + CONSOLE_FONT_INFOEX cfiexGet = { 0 }; + cfiexGet.cbSize = sizeof(cfiexGet); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(OneCoreDelay::GetCurrentConsoleFontEx(hOut, FALSE, &cfiexGet)); + + if (0 != NoThrowString(cfiex.FaceName).CompareNoCase(cfiexGet.FaceName)) + { + Log::Comment(L"Could not change font. This system doesn't have the fonts we need to perform this test. Skipping."); + Log::Result(WEX::Logging::TestResults::Result::Skipped); + return false; + } + + // Retrieve some of the information about the preferences/settings for the console buffer including + // the size of the buffer and the default colors (attributes) to use. + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(sbiex); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(GetConsoleScreenBufferInfoEx(hOut, &sbiex)); + + // ensure first line of console is cleared out with spaces so nothing interferes with the text these tests will be writing. + COORD coordZero = { 0 }; + DWORD dwWritten; + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FillConsoleOutputCharacterW(hOut, L'\x20', sbiex.dwSize.X, coordZero, &dwWritten)); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FillConsoleOutputAttribute(hOut, sbiex.wAttributes, sbiex.dwSize.X, coordZero, &dwWritten)); + + // Move the cursor to the 0,0 position into our empty line so the tests can write (important for the CRT tests that specify no location) + if (!SetConsoleCursorPosition(GetStdOutputHandle(), coordZero)) + { + VERIFY_FAIL(L"Failed to set cursor position"); + } + + // Give back the output handle and the default attributes so tests can verify attributes didn't change on roundtrip + *phOut = hOut; + *pwAttributes = sbiex.wAttributes; + + return true; +} + +void DbcsWriteRead::SendOutput(const HANDLE hOut, _In_ unsigned int const uiCodePage, + const DbcsWriteRead::WriteMode WriteMode, const bool fIsUnicode, + _In_ PCSTR pszTestString, const WORD wAttr) +{ + + // DBCS is very dependent on knowing the byte length in the original codepage of the input text. + // Save off the original length of the string so we know what its A length was. + SHORT const cTestString = (SHORT)strlen(pszTestString); + + // If we're in Unicode mode, we will need to translate the test string to Unicode before passing into the console + PWSTR pwszTestString = nullptr; + if (fIsUnicode) + { + // Use double-call pattern to find space to allocate, allocate it, then convert. + int const icchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestString, -1, nullptr, 0); + + pwszTestString = new WCHAR[icchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestString); + + int const iRes = MultiByteToWideChar(uiCodePage, 0, pszTestString, -1, pwszTestString, icchNeeded); + CheckLastErrorZeroFail(iRes, L"MultiByteToWideChar"); + } + + // Calculate the number of cells/characters/calls we will need to fill with our input depending on the mode. + SHORT cChars = 0; + if (fIsUnicode) + { + cChars = (SHORT)wcslen(pwszTestString); + } + else + { + cChars = cTestString; + } + + // These parameters will be used to print out the written rectangle if we used the console APIs (not the CRT APIs) + // This information will be stored and printed out at the very end after we move the cursor off of the text we just printed. + // The cursor auto-moves for CRT, but we have to manually move it for some of the Console APIs. + bool fUseRectWritten = false; + SMALL_RECT srWrittenExpected = { 0 }; + SMALL_RECT srWritten = { 0 }; + + bool fUseDwordWritten = false; + DWORD dwWritten = 0; + + switch (WriteMode) + { + case DbcsWriteRead::WriteMode::CrtWrite: + { + // Align the CRT's mode with the text we're about to write. + // If you call a W function on the CRT while the mode is still set to A, + // the CRT will helpfully back-convert your text from W to A before sending it to the driver. + if (fIsUnicode) + { + _setmode(_fileno(stdout), _O_WTEXT); + } + else + { + _setmode(_fileno(stdout), _O_TEXT); + } + + // Write each character in the string individually out through the CRT + if (fIsUnicode) + { + for (SHORT i = 0; i < cChars; i++) + { + putwchar(pwszTestString[i]); + } + } + else + { + for (SHORT i = 0; i < cChars; i++) + { + putchar(pszTestString[i]); + } + } + break; + } + case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc: + { + // If we're going to be using WriteConsoleOutput, we need to create up a nice + // CHAR_INFO buffer to pass into the method containing the string and possibly attributes + CHAR_INFO* rgChars = new CHAR_INFO[cChars]; + VERIFY_IS_NOT_NULL(rgChars); + + for (SHORT i = 0; i < cChars; i++) + { + rgChars[i].Attributes = wAttr; + + if (fIsUnicode) + { + rgChars[i].Char.UnicodeChar = pwszTestString[i]; + } + else + { + // Ensure the top half of the union is filled with 0 for comparison purposes later. + rgChars[i].Char.UnicodeChar = 0; + rgChars[i].Char.AsciiChar = pszTestString[i]; + } + } + + // This is the stated size of the buffer we're passing. + // This console API can treat the buffer as a 2D array. We're only doing 1 dimension so the Y is 1 and the X is the number of CHAR_INFO charcters. + COORD coordBufferSize = { 0 }; + coordBufferSize.Y = 1; + coordBufferSize.X = cChars; + + // We want to write to the coordinate 0,0 of the buffer. The test setup function has blanked out that line. + COORD coordBufferTarget = { 0 }; + + // inclusive rectangle (bottom and right are INSIDE the read area. usually are exclusive.) + SMALL_RECT srWriteRegion = { 0 }; + + // Since we could have full-width characters, we have to "allow" the console to write up to the entire A string length (up to double the W length) + srWriteRegion.Right = cTestString - 1; + + // Save the expected written rectangle for comparison after the call + srWrittenExpected = { 0 }; + srWrittenExpected.Right = cChars - 1; // we expect that the written report will be the number of characters inserted, not the size of buffer consumed + + // NOTE: Don't VERIFY these calls or we will overwrite the text in the buffer with the log message. + if (fIsUnicode) + { + WriteConsoleOutputW(hOut, rgChars, coordBufferSize, coordBufferTarget, &srWriteRegion); + } + else + { + WriteConsoleOutputA(hOut, rgChars, coordBufferSize, coordBufferTarget, &srWriteRegion); + } + + // Save write region so we can print it out after we move the cursor out of the way + srWritten = srWriteRegion; + fUseRectWritten = true; + + delete[] rgChars; + break; + } + case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc: + { + COORD coordBufferTarget = { 0 }; + + if (fIsUnicode) + { + WriteConsoleOutputCharacterW(hOut, pwszTestString, cChars, coordBufferTarget, &dwWritten); + } + else + { + WriteConsoleOutputCharacterA(hOut, pszTestString, cChars, coordBufferTarget, &dwWritten); + } + + fUseDwordWritten = true; + break; + } + case DbcsWriteRead::WriteMode::WriteConsoleFunc: + { + if (fIsUnicode) + { + WriteConsoleW(hOut, pwszTestString, cChars, &dwWritten, nullptr); + } + else + { + WriteConsoleA(hOut, pszTestString, cChars, &dwWritten, nullptr); + } + + fUseDwordWritten = true; + break; + } + default: + VERIFY_FAIL(L"Unsupported write mode."); + } + + // Free memory if appropriate (if we had to convert A to W) + if (nullptr != pwszTestString) + { + delete[] pwszTestString; + } + + // Move the cursor down a line in case log info prints out. + COORD coordSetCursor = { 0 }; + coordSetCursor.Y = 1; + SetConsoleCursorPosition(hOut, coordSetCursor); + + // If we had log info to print, print it now that it's safe (cursor out of the test data we printed) + // This only matters for when the test is run in the same window as the runner and could print log information. + if (fUseRectWritten) + { + Log::Comment(NoThrowString().Format(L"WriteRegion T: %d L: %d B: %d R: %d", srWritten.Top, srWritten.Left, srWritten.Bottom, srWritten.Right)); + VERIFY_ARE_EQUAL(srWrittenExpected, srWritten); + } + else if (fUseDwordWritten) + { + Log::Comment(NoThrowString().Format(L"Chars Written: %d", dwWritten)); + VERIFY_ARE_EQUAL((DWORD)cChars, dwWritten); + } +} + +// 3 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x029 | 0x3044 (0x44) | Hiragana I +// 0x029 | 0x304B (0x4B) | Hiragana KA +// 0x029 | 0x306A (0x6A) | Hiragana NA +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x029 | 0x306B (0x6B) | Hiragana NI +// 0x000 | 0x0000 (0x00) | +// 0x000 | 0x0000 (0x00) | +// 0x000 | 0x0000 (0x00) | +// 0x000 | 0x0000 (0x00) | +// ... +// "Null Padded" means any unused data in the buffer will be filled with null and null attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::NullPaddedDedupeW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD /*wAttrOriginal*/, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 3"); + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + + size_t const cWideTestData = wcslen(pwszTestData); + VERIFY_IS_GREATER_THAN_OR_EQUAL(cExpected, cWideTestData); + + for (size_t i = 0; i < cWideTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + wchar_t const wch = pwszTestData[i]; + + pciCurrent->Attributes = wAttrWritten; + pciCurrent->Char.UnicodeChar = wch; + } + + delete[] pwszTestData; +} + +// 1 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x029 | 0x3044 (0x44) | Hiragana I +// 0x029 | 0x304B (0x4B) | Hiragana KA +// 0x029 | 0x306A (0x6A) | Hiragana NA +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x029 | 0x306B (0x6B) | Hiragana NI +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// ... +// "Space Padded" means any unused data in the buffer will be filled with spaces and the default attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::SpacePaddedDedupeW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 1"); + DbcsWriteRead::PrepPattern::NullPaddedDedupeW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + for (size_t i = 0; i < cExpected; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + + if (0 == pciCurrent->Attributes && 0 == pciCurrent->Char.UnicodeChar) + { + pciCurrent->Attributes = wAttrOriginal; + pciCurrent->Char.UnicodeChar = L'\x20'; + } + } +} + +// 2 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x029 | 0x3044 (0x44) | Hiragana I +// 0x029 | 0x304B (0x4B) | Hiragana KA +// 0x029 | 0x306A (0x6A) | Hiragana NA +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x000 | 0x0000 (0x00) | +// 0x000 | 0x0000 (0x00) | +// 0x000 | 0x0000 (0x00) | +// ... +// "Space Padded" means most of the unused data in the buffer will be filled with spaces and the default attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +// "Truncated" means that this pattern trims off some of the end of the buffer with NULLs. +void DbcsWriteRead::PrepPattern::SpacePaddedDedupeTruncatedW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 2"); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + + size_t const cWideData = wcslen(pwszTestData); + + // The maximum number of columns the console will consume is the number of wide characters there are in the string. + // This is whether or not the characters themselves are halfwidth or fullwidth (1 col or 2 col respectively.) + // This means that for 4 wide characters that are halfwidth (1 col), the console will copy out all 4 of them. + // For 4 wide characters that are fullwidth (2 col each), the console will copy out 2 of them (because it will count each fullwidth as 2 when filling) + // For a mixed string that is something like half, full, half (4 columns, 3 wchars), we will receive half, full (3 columns worth) and truncate the last half. + + size_t const cMaxColumns = cWideData; + size_t iColumnsConsumed = 0; + + size_t iNarrow = 0; + size_t iWide = 0; + size_t iExpected = 0; + + size_t iNulls = 0; + + while (iColumnsConsumed < cMaxColumns) + { + CHAR_INFO* const pciCurrent = &pciExpected[iExpected]; + char const chCurrent = pszTestData[iWide]; + wchar_t const wchCurrent = pwszTestData[iWide]; + + pciCurrent->Attributes = wAttrWritten; + pciCurrent->Char.UnicodeChar = wchCurrent; + + if (IsDBCSLeadByteEx(uiCodePage, chCurrent)) + { + iColumnsConsumed += 2; + iNarrow += 2; + iNulls++; + } + else + { + iColumnsConsumed++; + iNarrow++; + } + + iWide++; + iExpected++; + } + + // Fill remaining with spaces and original attribute + while (iExpected < cExpected - iNulls) + { + CHAR_INFO* const pciCurrent = &pciExpected[iExpected]; + pciCurrent->Attributes = wAttrOriginal; + pciCurrent->Char.UnicodeChar = L'\x20'; + + iExpected++; + } + + delete[] pwszTestData; +} + +// 13 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x0082 (0x82) | Hiragana I Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00A2 (0xA2) | Hiragana I Shift-JIS Codepage 932 Trail Byte +// 0x129 | 0x0082 (0x82) | Hiragana KA Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00A9 (0xA9) | Hiragana KA Shift-JIS Codepage 932 Trail Byte +// 0x129 | 0x0082 (0x82) | Hiragana NA Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00C8 (0xC8) | Hiragana NA Shift-JIS Codepage 932 Trail Byte +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// ... +// "Space Padded" means most of the unused data in the buffer will be filled with spaces and the default attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "A" means that we intend in-codepage (char) data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::SpacePaddedDedupeA(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 13"); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + + size_t const cWideData = wcslen(pwszTestData); + + // The maximum number of columns the console will consume is the number of wide characters there are in the string. + // This is whether or not the characters themselves are halfwidth or fullwidth (1 col or 2 col respectively.) + // This means that for 4 wide characters that are halfwidth (1 col), the console will copy out all 4 of them. + // For 4 wide characters that are fullwidth (2 col each), the console will copy out 2 of them (because it will count each fullwidth as 2 when filling) + // For a mixed string that is something like half, full, half (4 columns, 3 wchars), we will receive half, full (3 columns worth) and truncate the last half. + + size_t const cMaxColumns = cWideData; + + bool fIsNextTrailing = false; + size_t i = 0; + for (; i < cMaxColumns; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + char const chCurrent = pszTestData[i]; + + pciCurrent->Attributes = wAttrWritten; + pciCurrent->Char.AsciiChar = chCurrent; + + if (IsDBCSLeadByteEx(uiCodePage, chCurrent)) + { + pciCurrent->Attributes |= COMMON_LVB_LEADING_BYTE; + fIsNextTrailing = true; + } + else if (fIsNextTrailing) + { + pciCurrent->Attributes |= COMMON_LVB_TRAILING_BYTE; + fIsNextTrailing = false; + } + } + + // Fill remaining with spaces and original attribute + while (i < cExpected) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + pciCurrent->Attributes = wAttrOriginal; + pciCurrent->Char.UnicodeChar = L'\x20'; + + i++; + } + + delete[] pwszTestData; +} + +// 5 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3044 (0x44) | Hiragana I +// 0x229 | 0x3044 (0x44) | Hiragana I +// 0x129 | 0x304B (0x4B) | Hiragana KA +// 0x229 | 0x304B (0x4B) | Hiragana KA +// 0x129 | 0x306A (0x6A) | Hiragana NA +// 0x229 | 0x306A (0x6A) | Hiragana NA +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x129 | 0x306B (0x6B) | Hiragana NI +// 0x229 | 0x306B (0x6B) | Hiragana NI +// ... +// "Doubled" means that any full-width characters in the buffer are returned twice with a leading and trailing byte marker. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::DoubledW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD /*wAttrOriginal*/, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 5"); + size_t const cTestData = strlen(pszTestData); + VERIFY_IS_GREATER_THAN_OR_EQUAL(cExpected, cTestData); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + + size_t iWide = 0; + wchar_t wchRepeat = L'\0'; + bool fIsNextTrailing = false; + for (size_t i = 0; i < cTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + char const chTest = pszTestData[i]; + wchar_t const wchCopy = pwszTestData[iWide]; + + pciCurrent->Attributes = wAttrWritten; + + if (IsDBCSLeadByteEx(uiCodePage, chTest)) + { + pciCurrent->Char.UnicodeChar = wchCopy; + iWide++; + + pciCurrent->Attributes |= COMMON_LVB_LEADING_BYTE; + + wchRepeat = wchCopy; + fIsNextTrailing = true; + } + else if (fIsNextTrailing) + { + pciCurrent->Char.UnicodeChar = wchRepeat; + + pciCurrent->Attributes |= COMMON_LVB_TRAILING_BYTE; + + fIsNextTrailing = false; + } + else + { + pciCurrent->Char.UnicodeChar = wchCopy; + iWide++; + } + } + + delete[] pwszTestData; +} + +// 4 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3044 (0x44) | Hiragana I +// 0x229 | 0xFFFF (0xFF) | Invalid Unicode Character +// 0x129 | 0x304B (0x4B) | Hiragana KA +// 0x229 | 0xFFFF (0xFF) | Invalid Unicode Character +// 0x129 | 0x306A (0x6A) | Hiragana NA +// 0x229 | 0xFFFF (0xFF) | Invalid Unicode Character +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x129 | 0x306B (0x6B) | Hiragana NI +// 0x229 | 0xFFFF (0xFF) | Invalid Unicode Character +// ... +// "Doubled" means that any full-width characters in the buffer are returned twice with a leading and trailing byte marker. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +// "NegativeOneTrailing" means that all trailing bytes have their character replaced with the value -1 or 0xFFFF +void DbcsWriteRead::PrepPattern::DoubledWNegativeOneTrailing(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 4"); + DbcsWriteRead::PrepPattern::DoubledW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + for (size_t i = 0; i < cExpected; i++) + { + CHAR_INFO* pciCurrent = &pciExpected[i]; + + if (WI_IsFlagSet(pciCurrent->Attributes, COMMON_LVB_TRAILING_BYTE)) + { + pciCurrent->Char.UnicodeChar = 0xFFFF; + } + } +} + +// 7 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3082 (0x82) | Hiragana I Unicode 0x3044 with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFA2 (0xA2) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA2 +// 0x129 | 0x3082 (0x82) | Hiragana KA Unicode 0x304B with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFA9 (0xA9) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA9 +// 0x129 | 0x3082 (0x82) | Hiragana NA 0x306A with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFC8 (0xC8) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xC8 +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// ... +// "AStompsW" means that the Unicode characters were fit into the result buffer first, then the Multibyte conversion +// was written over the top of the lower byte. This makes an invalid Unicode character, but can be understood +// as in-codepage from the char portion of the union. +// "NegativeOnePattern" means that every trailing byte started as -1 or 0xFFFF +// "TruncateSpacePadded" means that we only allowed ourselves to return as many characters as is in the unicode length +// of the string and then filled the rest of the buffer after that with spaces. +void DbcsWriteRead::PrepPattern::AStompsWNegativeOnePatternTruncateSpacePadded(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 7"); + DbcsWriteRead::PrepPattern::DoubledWNegativeOneTrailing(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + // Stomp all A portions of the structure from the existing pattern with the A characters + size_t const cTestData = strlen(pszTestData); + for (size_t i = 0; i < cTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + pciCurrent->Char.AsciiChar = pszTestData[i]; + } + + // Now truncate down and space fill the space based on the max column count. + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + + size_t const cWideData = wcslen(pwszTestData); + + // The maximum number of columns the console will consume is the number of wide characters there are in the string. + // This is whether or not the characters themselves are halfwidth or fullwidth (1 col or 2 col respectively.) + // This means that for 4 wide characters that are halfwidth (1 col), the console will copy out all 4 of them. + // For 4 wide characters that are fullwidth (2 col each), the console will copy out 2 of them (because it will count each fullwidth as 2 when filling) + // For a mixed string that is something like half, full, half (4 columns, 3 wchars), we will receive half, full (3 columns worth) and truncate the last half. + + size_t const cMaxColumns = cWideData; + + for (size_t i = cMaxColumns; i < cExpected; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + pciCurrent->Char.UnicodeChar = L'\x20'; + pciCurrent->Attributes = wAttrOriginal; + } + + delete[] pwszTestData; +} + +// 6 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x0082 (0x82) | Hiragana I Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00A2 (0xA2) | Hiragana I Shift-JIS Codepage 932 Trail Byte +// 0x129 | 0x0082 (0x82) | Hiragana KA Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00A9 (0xA9) | Hiragana KA Shift-JIS Codepage 932 Trail Byte +// 0x129 | 0x0082 (0x82) | Hiragana NA Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00C8 (0xC8) | Hiragana NA Shift-JIS Codepage 932 Trail Byte +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x129 | 0x0082 (0x82) | Hiragana NI Shift-JIS Codepage 932 Lead Byte +// 0x229 | 0x00C9 (0xC9) | Hiragana NI Shift-JIS Codepage 932 Trail Byte +// ... +// "A" means that we intend in-codepage (char) data to be browsed in the resulting struct. +// This one returns pretty much exactly as expected. +void DbcsWriteRead::PrepPattern::A(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD /*wAttrOriginal*/, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 6"); + size_t const cTestData = strlen(pszTestData); + VERIFY_IS_GREATER_THAN_OR_EQUAL(cExpected, cTestData); + + bool fIsNextTrailing = false; + for (size_t i = 0; i < cTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + char const ch = pszTestData[i]; + + pciCurrent->Attributes = wAttrWritten; + pciCurrent->Char.AsciiChar = ch; + + if (IsDBCSLeadByteEx(uiCodePage, ch)) + { + pciCurrent->Attributes |= COMMON_LVB_LEADING_BYTE; + fIsNextTrailing = true; + } + else if (fIsNextTrailing) + { + pciCurrent->Attributes |= COMMON_LVB_TRAILING_BYTE; + fIsNextTrailing = false; + } + } +} + +// 10 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3044 (0x44) | Hiragana I +// 0x229 | 0x304B (0x4B) | Hiragana KA +// 0x129 | 0x306A (0x6A) | Hiragana NA +// 0x229 | 0x005A (0x5A) | Z +// 0x129 | 0x0059 (0x59) | Y +// 0x229 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x029 | 0x306B (0x6B) | Hiragana NI +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x129 | 0x0000 (0x00) | +// 0x229 | 0x0000 (0x00) | +// ... +// "Null" means any unused data in the buffer will be filled with null and null attribute. +// "CoverAChar" means that the attributes belong to the A version of the call, but we've placed de-duped W characters over the top. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::WNullCoverAChar(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 10"); + DbcsWriteRead::PrepPattern::A(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + size_t const cWideData = wcslen(pwszTestData); + + size_t i = 0; + for (; i < cWideData; i++) + { + pciExpected[i].Char.UnicodeChar = pwszTestData[i]; + } + + for (; i < cExpected; i++) + { + pciExpected[i].Char.UnicodeChar = L'\0'; + } + + delete[] pwszTestData; +} + +// 11 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x029 | 0x3044 (0x44) | Hiragana I +// 0x029 | 0x304B (0x4B) | Hiragana KA +// 0x029 | 0x306A (0x6A) | Hiragana NA +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x029 | 0x306B (0x6B) | Hiragana NI +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// ... +// "Space fill" means any unused data in the buffer will be filled with space and default attribute +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +void DbcsWriteRead::PrepPattern::WSpaceFill(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 11"); + DbcsWriteRead::PrepPattern::WNullCoverAChar(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + size_t const cWideData = wcslen(pwszTestData); + + size_t i = 0; + for (; i < cWideData; i++) + { + pciExpected[i].Attributes = wAttrWritten; + } + + for (; i < cExpected; i++) + { + pciExpected[i].Char.UnicodeChar = L'\x20'; + pciExpected[i].Attributes = wAttrOriginal; + } + + delete[] pwszTestData; +} + +//8 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3082 (0x82) | Hiragana I Unicode 0x3044 with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFA2 (0xA2) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA2 +// 0x129 | 0x3082 (0x82) | Hiragana KA Unicode 0x304B with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFA9 (0xA9) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA9 +// 0x129 | 0x3082 (0x82) | Hiragana NA 0x306A with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFC8 (0xC8) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xC8 +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x129 | 0x3082 (0x30) | Hiragana NI 0x306B with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0xFFC9 (0xC9) | Invalid Unicode Character 0xFFFF with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xC9 +// ... +// "AOn" means that the Unicode characters were fit into the result buffer first, then the Multibyte conversion +// was written over the top of the lower byte. This makes an invalid Unicode character, but can be understood +// as in-codepage from the char portion of the union. +// "DoubledW" means that the full-width Unicode characters were inserted twice into the buffer (and marked lead/trailing) +// "NegativeOneTrailing" means that every trailing byte started as -1 or 0xFFFF +void DbcsWriteRead::PrepPattern::AOnDoubledWNegativeOneTrailing(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 8"); + + DbcsWriteRead::PrepPattern::DoubledWNegativeOneTrailing(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + // Stomp all A portions of the structure from the existing pattern with the A characters + size_t const cTestData = strlen(pszTestData); + VERIFY_IS_GREATER_THAN_OR_EQUAL(cExpected, cTestData); + for (size_t i = 0; i < cTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + pciCurrent->Char.AsciiChar = pszTestData[i]; + } +} + +// 9 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3082 (0x82) | Hiragana I Unicode 0x3044 with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0x30A2 (0xA2) | Hiragana I Unicode 0x3044 with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA2 +// 0x129 | 0x3082 (0x82) | Hiragana KA Unicode 0x304B with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0x30A9 (0xA9) | Hiragana KA Unicode 0x304B with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xA9 +// 0x129 | 0x3082 (0x82) | Hiragana NA 0x306A with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0x39C8 (0xC8) | Hiragana NA 0x306A with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xC8 +// 0x029 | 0x005A (0x5A) | Z +// 0x029 | 0x0059 (0x59) | Y +// 0x029 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0055 (0x55) | U +// 0x029 | 0x0054 (0x54) | T +// 0x129 | 0x3082 (0x30) | Hiragana NI 0x306B with the lower byte covered by Shift-JIS Codepage 932 Lead Byte 0x82. +// 0x229 | 0x30C9 (0xC9) | Hiragana NI 0x306B with the lower byte covered by Shift-JIS Codepage 932 Trail Byte 0xC9 +// ... +// "AOn" means that the Unicode characters were fit into the result buffer first, then the Multibyte conversion +// was written over the top of the lower byte. This makes an invalid Unicode character, but can be understood +// as in-codepage from the char portion of the union. +// "DoubledW" means that the full-width Unicode characters were inserted twice into the buffer (and marked lead/trailing) +// "NegativeOneTrailing" means that every trailing byte started as -1 or 0xFFFF +void DbcsWriteRead::PrepPattern::AOnDoubledW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 9"); + + DbcsWriteRead::PrepPattern::DoubledW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + // Stomp all A portions of the structure from the existing pattern with the A characters + size_t const cTestData = strlen(pszTestData); + VERIFY_IS_GREATER_THAN_OR_EQUAL(cExpected, cTestData); + for (size_t i = 0; i < cTestData; i++) + { + CHAR_INFO* const pciCurrent = &pciExpected[i]; + pciCurrent->Char.AsciiChar = pszTestData[i]; + } +} + +// 12 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0051 (0x51) | Q +// 0x129 | 0x3044 (0x44) | Hiragana I +// 0x229 | 0x304B (0x4B) | Hiragana KA +// 0x129 | 0x306A (0x6A) | Hiragana NA +// 0x229 | 0x005A (0x5A) | Z +// 0x129 | 0x0059 (0x59) | Y +// 0x229 | 0x0058 (0x58) | X +// 0x029 | 0x0057 (0x57) | W +// 0x029 | 0x0056 (0x56) | V +// 0x029 | 0x0020 (0x20) | +// 0x029 | 0x0020 (0x20) | +// 0x029 | 0x0020 (0x20) | +// 0x007 | 0x0020 (0x20) | +// 0x007 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// ... +// "Space Padded" means most of the unused data in the buffer will be filled with spaces and the default attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +// "Truncated" means that this pattern trims off some of the end of the buffer with NULLs. +// "A Cover Attr" means that after all the other operations, we will finally run through and cover up the attributes +// again with what they would have been for multi-byte data (leading and trailing flags) +void DbcsWriteRead::PrepPattern::ACoverAttrSpacePaddedDedupeTruncatedW(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 12"); + DbcsWriteRead::PrepPattern::SpacePaddedDedupeTruncatedW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, pciExpected, cExpected); + + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + size_t const cWideData = wcslen(pwszTestData); + + size_t i = 0; + bool fIsNextTrailing = false; + for (; i < cWideData; i++) + { + pciExpected[i].Attributes = wAttrWritten; + + if (IsDBCSLeadByteEx(uiCodePage, pszTestData[i])) + { + pciExpected[i].Attributes |= COMMON_LVB_LEADING_BYTE; + fIsNextTrailing = true; + } + else if (fIsNextTrailing) + { + pciExpected[i].Attributes |= COMMON_LVB_TRAILING_BYTE; + fIsNextTrailing = false; + } + + } + + for (; i < cExpected; i++) + { + pciExpected[i].Attributes = wAttrOriginal; + } + + delete[] pwszTestData; +} + +// 14 +// From Input String: "Q(Hiragana I)(Hiragana KA)(Hiragana NA)ZYXWVUT(Hiragana NI) +// With Default Attribute 0x7 (before writing) and Applied Attribute 0x29 (written with text) +// ... +// Receive Output Table: +// attr | wchar (char) | symbol +// ------------------------------------ +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x029 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// 0x007 | 0x0000 (0x00) | +// ... +// "Space Padded" means most of the unused data in the buffer will be filled with spaces and the default attribute. +// "Dedupe" means that any full-width characters in the buffer (despite being stored doubled inside the buffer) +// will be returned as single copies. +// "W" means that we intend Unicode data to be browsed in the resulting struct (even though wchar and char are unioned.) +// "Truncated" means that this pattern trims off some of the end of the buffer with NULLs. +// "A Cover Attr" means that after all the other operations, we will finally run through and cover up the attributes +// again with what they would have been for multi-byte data (leading and trailing flags) +void DbcsWriteRead::PrepPattern::TrueTypeCharANullWithAttrs(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + _Inout_updates_all_(cExpected) CHAR_INFO* const pciExpected, + const size_t cExpected) +{ + Log::Comment(L"Pattern 14"); + int const iwchNeeded = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, nullptr, 0); + PWSTR pwszTestData = new wchar_t[iwchNeeded]; + VERIFY_IS_NOT_NULL(pwszTestData); + int const iSuccess = MultiByteToWideChar(uiCodePage, 0, pszTestData, -1, pwszTestData, iwchNeeded); + CheckLastErrorZeroFail(iSuccess, L"MultiByteToWideChar"); + size_t const cWideData = wcslen(pwszTestData); + + // Fill the number of columns worth of wide characters with the write attribute. The rest get the original attribute. + size_t i; + for (i = 0; i < cWideData; i++) + { + pciExpected[i].Attributes = wAttrWritten; + } + + for (; i < cExpected; i++) + { + pciExpected[i].Attributes = wAttrOriginal; + } + + // For characters, if the string contained NO double-byte characters, it will return. Otherwise, it won't return due to + // a long standing bug in the console's way it calls RtlUnicodeToOemN + size_t const cTestData = strlen(pszTestData); + if (cWideData == cTestData) + { + for (i = 0; i < cTestData; i++) + { + pciExpected[i].Char.AsciiChar = pszTestData[i]; + } + } + + delete[] pwszTestData; +} + +void DbcsWriteRead::PrepReadConsoleOutput(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const bool fReadWithUnicode, + _Inout_updates_all_(cExpectedNeeded) CHAR_INFO* const rgciExpected, + const size_t cExpectedNeeded) +{ + switch (WriteMode) + { + case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc: + { + // If we wrote with WriteConsoleOutput*, things are going to be munged depending on the font and the A/W status of both the write and the read. + if (!fReadWithUnicode) + { + // If we read it back with the A functions, the font might matter. + // We will get different results dependent on whether the original text was written with the W or A method. + if (fWriteWithUnicode) + { + if (fIsTrueTypeFont) + { + // When written with WriteConsoleOutputW and read back with ReadConsoleOutputA under TT font, we will get a deduplicated + // set of Unicode characters (YES. Unicode characters despite calling the A API to read back) that is space padded out + // There will be no lead/trailing markings. + DbcsWriteRead::PrepPattern::SpacePaddedDedupeW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + // When written with WriteConsoleOutputW and read back with ReadConsoleOutputA under Raster font, we will get the + // double-byte sequences stomped on top of a Unicode filled CHAR_INFO structure that used -1 for trailing bytes. + DbcsWriteRead::PrepPattern::AStompsWNegativeOnePatternTruncateSpacePadded(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + else + { + // When written with WriteConsoleOutputA and read back with ReadConsoleOutputA, + // we will get back the double-byte sequences appropriately labeled with leading/trailing bytes. + //DbcsWriteRead::PrepPattern::A(pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + DbcsWriteRead::PrepPattern::AOnDoubledWNegativeOneTrailing(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + else + { + // If we read it back with the W functions, both the font and the original write mode (A vs. W) matter + if (fIsTrueTypeFont) + { + if (fWriteWithUnicode) + { + // When written with WriteConsoleOutputW and read back with ReadConsoleOutputW when the font is TrueType, + // we will get a deduplicated set of Unicode characters with no lead/trailing markings and space padded at the end. + DbcsWriteRead::PrepPattern::SpacePaddedDedupeW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + // When written with WriteConsoleOutputW and read back with ReadConsoleOutputA when the font is TrueType, + // we will get back Unicode characters doubled up and marked with leading and trailing bytes... + // ... except all the trailing bytes character values will be -1. + DbcsWriteRead::PrepPattern::DoubledWNegativeOneTrailing(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + else + { + if (fWriteWithUnicode) + { + // When written with WriteConsoleOutputW and read back with ReadConsoleOutputW when the font is Raster, + // we will get a deduplicated set of Unicode characters with no lead/trailing markings and space padded at the end... + // ... except something weird happens with truncation (TODO figure out what) + DbcsWriteRead::PrepPattern::SpacePaddedDedupeTruncatedW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + // When written with WriteConsoleOutputA and read back with ReadConsoleOutputW when the font is Raster, + // we will get back de-duplicated Unicode characters with no lead / trail markings.The extra array space will remain null. + DbcsWriteRead::PrepPattern::NullPaddedDedupeW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + } + break; + } + case DbcsWriteRead::WriteMode::CrtWrite: + case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc: + case DbcsWriteRead::WriteMode::WriteConsoleFunc: + { + // Writing with the CRT down here. + if (!fReadWithUnicode) + { + // If we wrote with the CRT and are reading with A functions, the font doesn't matter. + // We will always get back the double-byte sequences appropriately labeled with leading/trailing bytes. + //DbcsWriteRead::PrepPattern::(pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + DbcsWriteRead::PrepPattern::AOnDoubledW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + // If we wrote with the CRT and are reading back with the W functions, the font does matter. + if (fIsTrueTypeFont) + { + // In a TrueType font, we will get back Unicode characters doubled up and marked with leading and trailing bytes. + DbcsWriteRead::PrepPattern::DoubledW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + // In a Raster font, we will get back de-duplicated Unicode characters with no lead/trail markings. The extra array space will remain null. + DbcsWriteRead::PrepPattern::NullPaddedDedupeW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + break; + } + default: + VERIFY_FAIL(L"Unsupported write mode"); + } +} + +void DbcsWriteRead::PrepReadConsoleOutputCharacter(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const bool fReadWithUnicode, + _Inout_updates_all_(cExpectedNeeded) CHAR_INFO* const rgciExpected, + const size_t cExpectedNeeded) +{ + if (DbcsWriteRead::WriteMode::WriteConsoleOutputFunc == WriteMode && fWriteWithUnicode) + { + if (fIsTrueTypeFont) + { + if (fReadWithUnicode) + { + DbcsWriteRead::PrepPattern::WSpaceFill(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + DbcsWriteRead::PrepPattern::TrueTypeCharANullWithAttrs(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + else + { + if (fReadWithUnicode) + { + DbcsWriteRead::PrepPattern::ACoverAttrSpacePaddedDedupeTruncatedW(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + DbcsWriteRead::PrepPattern::SpacePaddedDedupeA(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } + } + else + { + if (!fReadWithUnicode) + { + DbcsWriteRead::PrepPattern::A(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + else + { + DbcsWriteRead::PrepPattern::WNullCoverAChar(uiCodePage, pszTestData, wAttrOriginal, wAttrWritten, rgciExpected, cExpectedNeeded); + } + } +} + +void DbcsWriteRead::PrepExpected(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + const WORD wAttrOriginal, + const WORD wAttrWritten, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteWithUnicode, + const bool fIsTrueTypeFont, + const DbcsWriteRead::ReadMode ReadMode, + const bool fReadWithUnicode, + _Outptr_result_buffer_(*pcExpected) CHAR_INFO** const ppciExpected, + _Out_ size_t* const pcExpected) +{ + // We will expect to read back one CHAR_INFO for every A character we sent to the console using the assumption above. + // We expect that reading W characters will always be less than or equal to that. + size_t const cExpectedNeeded = strlen(pszTestData); + + // Allocate and zero out the space so comparisons don't fail from garbage bytes. + CHAR_INFO* rgciExpected = new CHAR_INFO[cExpectedNeeded]; + VERIFY_IS_NOT_NULL(rgciExpected); + ZeroMemory(rgciExpected, sizeof(CHAR_INFO) * cExpectedNeeded); + + switch (ReadMode) + { + case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc: + { + DbcsWriteRead::PrepReadConsoleOutput(uiCodePage, + pszTestData, + wAttrOriginal, + wAttrWritten, + WriteMode, + fWriteWithUnicode, + fIsTrueTypeFont, + fReadWithUnicode, + rgciExpected, + cExpectedNeeded); + break; + } + case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc: + { + DbcsWriteRead::PrepReadConsoleOutputCharacter(uiCodePage, + pszTestData, + wAttrOriginal, + wAttrWritten, + WriteMode, + fWriteWithUnicode, + fIsTrueTypeFont, + fReadWithUnicode, + rgciExpected, + cExpectedNeeded); + break; + } + default: + { + VERIFY_FAIL(L"Unknown read mode."); + break; + } + } + + // Return the expected array and the length that should be used for comparison at the end of the test. + *ppciExpected = rgciExpected; + *pcExpected = cExpectedNeeded; +} + +void DbcsWriteRead::RetrieveOutput(const HANDLE hOut, + const DbcsWriteRead::ReadMode ReadMode, const bool fReadUnicode, + _Out_writes_(cChars) CHAR_INFO* const rgChars, const SHORT cChars) +{ + COORD coordBufferTarget = { 0 }; + + switch (ReadMode) + { + case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc: + { + // Since we wrote (in SendOutput function) to the 0,0 line, we need to read back the same width from that line. + COORD coordBufferSize = { 0 }; + coordBufferSize.Y = 1; + coordBufferSize.X = cChars; + + SMALL_RECT srReadRegion = { 0 }; // inclusive rectangle (bottom and right are INSIDE the read area. usually are exclusive.) + srReadRegion.Right = cChars - 1; + + // return value for read region shouldn't change + SMALL_RECT const srReadRegionExpected = srReadRegion; + + if (!fReadUnicode) + { + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputA(hOut, rgChars, coordBufferSize, coordBufferTarget, &srReadRegion)); + } + else + { + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputW(hOut, rgChars, coordBufferSize, coordBufferTarget, &srReadRegion)); + } + + Log::Comment(NoThrowString().Format(L"ReadRegion T: %d L: %d B: %d R: %d", srReadRegion.Top, srReadRegion.Left, srReadRegion.Bottom, srReadRegion.Right)); + VERIFY_ARE_EQUAL(srReadRegionExpected, srReadRegion); + break; + } + case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc: + { + DWORD dwRead = 0; + if (!fReadUnicode) + { + PSTR psRead = new char[cChars]; + VERIFY_IS_NOT_NULL(psRead); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputCharacterA(hOut, psRead, cChars, coordBufferTarget, &dwRead)); + + for (size_t i = 0; i < dwRead; i++) + { + rgChars[i].Char.AsciiChar = psRead[i]; + } + + delete[] psRead; + } + else + { + PWSTR pwsRead = new wchar_t[cChars]; + VERIFY_IS_NOT_NULL(pwsRead); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputCharacterW(hOut, pwsRead, cChars, coordBufferTarget, &dwRead)); + + for (size_t i = 0; i < dwRead; i++) + { + rgChars[i].Char.UnicodeChar = pwsRead[i]; + } + + delete[] pwsRead; + } + + PWORD pwAttrs = new WORD[cChars]; + VERIFY_IS_NOT_NULL(pwAttrs); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(ReadConsoleOutputAttribute(hOut, pwAttrs, cChars, coordBufferTarget, &dwRead)); + + for (size_t i = 0; i < dwRead; i++) + { + rgChars[i].Attributes = pwAttrs[i]; + } + + delete[] pwAttrs; + break; + } + default: + VERIFY_FAIL(L"Unknown read mode"); + break; + } +} + +void DbcsWriteRead::Verify(_In_reads_(cExpected) CHAR_INFO* const rgExpected, const size_t cExpected, + _In_reads_(cExpected) CHAR_INFO* const rgActual) +{ + // We will walk through for the number of CHAR_INFOs expected. + for (size_t i = 0; i < cExpected; i++) + { + // Uncomment these lines for help debugging the verification. + /* + Log::Comment(NoThrowString().Format(L"Index: %d:", i)); + Log::Comment(VerifyOutputTraits::ToString(rgExpected[i])); + Log::Comment(VerifyOutputTraits::ToString(rgActual[i])); + */ + + VERIFY_ARE_EQUAL(rgExpected[i], rgActual[i]); + } +} + +void DbcsWriteRead::TestRunner(_In_ unsigned int const uiCodePage, + _In_ PCSTR pszTestData, + _In_opt_ WORD* const pwAttrOverride, + const bool fUseTrueType, + const DbcsWriteRead::WriteMode WriteMode, + const bool fWriteInUnicode, + const DbcsWriteRead::ReadMode ReadMode, + const bool fReadWithUnicode) +{ + // First we need to set up the tests by clearing out the first line of the buffer, + // retrieving the appropriate output handle, and getting the colors (attributes) + // used by default in the buffer (set during clearing as well). + HANDLE hOut; + WORD wAttributes; + if (!DbcsWriteRead::Setup(uiCodePage, fUseTrueType, &hOut, &wAttributes)) + { + // If we can't set up (setup will detect systems where this test cannot operate) then return early. + return; + } + + WORD const wAttrOriginal = wAttributes; + + // Some tests might want to override the colors applied to ensure both parts of the CHAR_INFO union + // work for methods that support sending that union. (i.e. not the CRT path) + if (nullptr != pwAttrOverride) + { + wAttributes = *pwAttrOverride; + } + + // The console bases the space it walks for DBCS conversions on the length of the A version of the text. + // Store that length now so we have it for our read/write operations. + size_t const cTestData = strlen(pszTestData); + + // Write the string under test into the appropriate WRITE API for this test. + DbcsWriteRead::SendOutput(hOut, uiCodePage, WriteMode, fWriteInUnicode, pszTestData, wAttributes); + + // Prepare the array of CHAR_INFO structs that we expect to receive back when we will call read in a moment. + // This can vary based on font, unicode/non-unicode (when reading AND writing), and codepage. + CHAR_INFO* pciExpected; + size_t cExpected; + DbcsWriteRead::PrepExpected(uiCodePage, pszTestData, wAttrOriginal, wAttributes, WriteMode, fWriteInUnicode, fUseTrueType, ReadMode, fReadWithUnicode, &pciExpected, &cExpected); + + // Now call the appropriate READ API for this test. + CHAR_INFO* pciActual = new CHAR_INFO[cTestData]; + VERIFY_IS_NOT_NULL(pciActual); + ZeroMemory(pciActual, sizeof(CHAR_INFO) * cTestData); + DbcsWriteRead::RetrieveOutput(hOut, ReadMode, fReadWithUnicode, pciActual, (SHORT)cTestData); + + // Loop through and verify that our expected array matches what was actually returned by the given API. + DbcsWriteRead::Verify(pciExpected, cExpected, pciActual); + + // Free allocated structures + delete[] pciActual; + delete[] pciExpected; +} + +void DbcsTests::TestDbcsWriteRead() +{ + unsigned int uiCodePage; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"uiCodePage", uiCodePage)); + + bool fUseTrueTypeFont; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fUseTrueTypeFont", fUseTrueTypeFont)); + + int iWriteMode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"WriteMode", iWriteMode)); + DbcsWriteRead::WriteMode WriteMode = (DbcsWriteRead::WriteMode)iWriteMode; + + bool fWriteInUnicode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fWriteInUnicode", fWriteInUnicode)); + + int iReadMode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"ReadMode", iReadMode)); + DbcsWriteRead::ReadMode ReadMode = (DbcsWriteRead::ReadMode)iReadMode; + + bool fReadInUnicode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fReadInUnicode", fReadInUnicode)); + + PCWSTR pwszWriteMode = L""; + switch (WriteMode) + { + case DbcsWriteRead::WriteMode::CrtWrite: + pwszWriteMode = L"CRT"; + break; + case DbcsWriteRead::WriteMode::WriteConsoleOutputFunc: + pwszWriteMode = L"WriteConsoleOutput"; + break; + case DbcsWriteRead::WriteMode::WriteConsoleOutputCharacterFunc: + pwszWriteMode = L"WriteConsoleOutputCharacter"; + break; + case DbcsWriteRead::WriteMode::WriteConsoleFunc: + pwszWriteMode = L"WriteConsole"; + break; + default: + VERIFY_FAIL(L"Write mode not supported"); + } + + PCWSTR pwszReadMode = L""; + switch (ReadMode) + { + case DbcsWriteRead::ReadMode::ReadConsoleOutputFunc: + pwszReadMode = L"ReadConsoleOutput"; + break; + case DbcsWriteRead::ReadMode::ReadConsoleOutputCharacterFunc: + pwszReadMode = L"ReadConsoleOutputCharacter"; + break; + default: + VERIFY_FAIL(L"Read mode not supported"); + } + + auto testInfo = NoThrowString().Format(L"\r\n\r\n\r\nUse '%ls' font. Write with %ls '%ls'. Check Read with %ls '%ls' API. Use %d codepage.\r\n", + fUseTrueTypeFont ? L"TrueType" : L"Raster", + pwszWriteMode, + fWriteInUnicode ? L"W" : L"A", + pwszReadMode, + fReadInUnicode ? L"W" : L"A", + uiCodePage); + + Log::Comment(testInfo); + + PCSTR pszTestData = ""; + switch (uiCodePage) + { + case ENGLISH_US_CP: + pszTestData = "QWERTYUIOP"; + break; + case JAPANESE_CP: + // Q (Hiragana I) (Hiragana KA) (Hiragana NA) Z Y X W V U T (Hiragana NI) in Shift-JIS (Codepage 932) + pszTestData = "Q\x82\xA2\x82\xa9\x82\xc8ZYXWVUT\x82\xc9"; + break; + default: + VERIFY_FAIL(L"No test data for this codepage"); + break; + } + + WORD wAttributes = 0; + + if (WriteMode == 1) + { + Log::Comment(L"We will also try to change the color since WriteConsoleOutput supports it."); + wAttributes = FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_GREEN; + } + + DbcsWriteRead::TestRunner(uiCodePage, + pszTestData, + wAttributes != 0 ? &wAttributes : nullptr, + fUseTrueTypeFont, + WriteMode, + fWriteInUnicode, + ReadMode, + fReadInUnicode); + + Log::Comment(testInfo); + +} + +// This test covers bisect-prevention handling. This is the behavior where a double-wide character will not be spliced +// across a line boundary and will instead be advanced onto the next line. +// It additionally exercises the word wrap functionality to ensure that the bisect calculations continue +// to apply properly when wrap occurs. +void DbcsTests::TestDbcsBisect() +{ + HANDLE const hOut = GetStdOutputHandle(); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP)); + + UINT dwCP = GetConsoleCP(); + VERIFY_ARE_EQUAL(dwCP, JAPANESE_CP); + + UINT dwOutputCP = GetConsoleOutputCP(); + VERIFY_ARE_EQUAL(dwOutputCP, JAPANESE_CP); + + CONSOLE_SCREEN_BUFFER_INFOEX sbiex = { 0 }; + sbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + BOOL fSuccess = GetConsoleScreenBufferInfoEx(hOut, &sbiex); + + if (CheckLastError(fSuccess, L"GetConsoleScreenBufferInfoEx")) + { + Log::Comment(L"Set cursor position to the last column in the buffer width."); + sbiex.dwCursorPosition.X = sbiex.dwSize.X - 1; + + COORD const coordEndOfLine = sbiex.dwCursorPosition; // this is the end of line position we're going to write at + COORD coordStartOfNextLine; + coordStartOfNextLine.X = 0; + coordStartOfNextLine.Y = sbiex.dwCursorPosition.Y + 1; + + fSuccess = SetConsoleCursorPosition(hOut, sbiex.dwCursorPosition); + if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx")) + { + Log::Comment(L"Attempt to write (standard WriteConsole) a double-wide character and ensure that it is placed onto the following line, not bisected."); + DWORD dwWritten = 0; + WCHAR const wchHiraganaU = L'\x3046'; + WCHAR const wchSpace = L' '; + fSuccess = WriteConsoleW(hOut, &wchHiraganaU, 1, &dwWritten, nullptr); + + if (CheckLastError(fSuccess, L"WriteConsoleW")) + { + VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only written the one character."); + + // Read the end of line character and the start of the next line. + // A proper bisect should have left the end of line character empty (a space) + // and then put the character at the beginning of the next line. + + Log::Comment(L"Confirm that the end of line was left empty to prevent bisect."); + WCHAR wchBuffer; + fSuccess = ReadConsoleOutputCharacterW(hOut, &wchBuffer, 1, coordEndOfLine, &dwWritten); + if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW")) + { + VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only read one character back at the end of the line."); + + VERIFY_ARE_EQUAL(wchSpace, wchBuffer, L"A space character should have been left at the end of the line."); + + Log::Comment(L"Confirm that the wide character was written on the next line down instead."); + WCHAR wchBuffer2[2]; + fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordStartOfNextLine, &dwWritten); + if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW")) + { + VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have only read one character back at the beginning of the next line."); + + VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The same character we passed in should have been read back."); + + Log::Comment(L"Confirm that the cursor has advanced past the double wide character."); + fSuccess = GetConsoleScreenBufferInfoEx(hOut, &sbiex); + if (CheckLastError(fSuccess, L"GetConsoleScreenBufferInfoEx")) + { + VERIFY_ARE_EQUAL(coordStartOfNextLine.Y, sbiex.dwCursorPosition.Y, L"Cursor has moved down to next line."); + VERIFY_ARE_EQUAL(coordStartOfNextLine.X + 2, sbiex.dwCursorPosition.X, L"Cursor has advanced two spaces on next line for double wide character."); + + // TODO: This bit needs to move into a UIA test + /*Log::Comment(L"We can only run the resize test in the v2 console. We'll skip it if it turns out v2 is off."); + if (IsV2Console()) + { + Log::Comment(L"Test that the character moves back up when the window is unwrapped. Make the window one larger."); + sbiex.srWindow.Right++; + sbiex.dwSize.X++; + fSuccess = SetConsoleScreenBufferInfoEx(hOut, &sbiex); + if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx")) + { + ZeroMemory(wchBuffer2, ARRAYSIZE(wchBuffer2) * sizeof(WCHAR)); + Log::Comment(L"Check that the character rolled back up onto the previous line."); + fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordEndOfLine, &dwWritten); + if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW")) + { + VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have read 1 character up on the previous line."); + + VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The character should now be up one line."); + + Log::Comment(L"Now shrink the window one more time and make sure the character rolls back down a line."); + sbiex.srWindow.Right--; + sbiex.dwSize.X--; + fSuccess = SetConsoleScreenBufferInfoEx(hOut, &sbiex); + if (CheckLastError(fSuccess, L"SetConsoleScreenBufferInfoEx")) + { + ZeroMemory(wchBuffer2, ARRAYSIZE(wchBuffer2) * sizeof(WCHAR)); + Log::Comment(L"Check that the character rolled down onto the next line again."); + fSuccess = ReadConsoleOutputCharacterW(hOut, wchBuffer2, 2, coordStartOfNextLine, &dwWritten); + if (CheckLastError(fSuccess, L"ReadConsoleOutputCharacterW")) + { + VERIFY_ARE_EQUAL(1u, dwWritten, L"We should have read 1 character back down again on the next line."); + + VERIFY_ARE_EQUAL(wchHiraganaU, wchBuffer2[0], L"The character should now be down on the 2nd line again."); + } + } + } + } + }*/ + } + } + } + } + } + } +} + +// The following W versions of the tests check that we can't insert a bisecting cell even +// when we try to force one in by writing cell-by-cell. +// NOTE: This is a change in behavior from the legacy behavior. +// V1 console would allow a lead byte to be stored in the final cell and then display it improperly. +// It would also allow this data to be read back. +// I believe this was a long standing bug because every other API entry fastidiously checked that it wasn't possible to +// "bisect" a cell and all sorts of portions of the rest of the console code try to enforce that bisects across lines can't happen. +// For the most recent revision of the V2 console (approx November 2018), we're trying to make sure that the TextBuffer's internal state +// is always correct at insert (instead of correcting it on every read). +// If it turns out that we are proven wrong in the future and this causes major problems, +// the legacy behavior is to just let it be stored and compensate for it later. (On read in every API but ReadConsoleOutput and in the selection). +void DbcsTests::TestDbcsBisectWriteCellsEndW() +{ + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 }; + info.cbSize = sizeof(info); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info)); + + CHAR_INFO originalCell; + originalCell.Char.UnicodeChar = L'\x30a2'; // Japanese full-width katakana A + originalCell.Attributes = COMMON_LVB_LEADING_BYTE | FOREGROUND_RED; + + SMALL_RECT writeRegion; + writeRegion.Top = 0; + writeRegion.Bottom = 0; + writeRegion.Left = info.dwSize.X - 1; + writeRegion.Right = info.dwSize.X - 1; + + const auto originalWriteRegion = writeRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion)); + VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion); + + SMALL_RECT readRegion = originalWriteRegion; + const auto originalReadRegion = readRegion; + CHAR_INFO readCell; + + CHAR_INFO expectedCell; + expectedCell.Char.UnicodeChar = L' '; + expectedCell.Attributes = originalCell.Attributes; + WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE); + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(originalReadRegion, readRegion); + + VERIFY_ARE_NOT_EQUAL(originalCell, readCell); + VERIFY_ARE_EQUAL(expectedCell, readCell); +} + +// This test also reflects a change in the legacy behavior (see above) +void DbcsTests::TestDbcsBisectWriteCellsBeginW() +{ + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 }; + info.cbSize = sizeof(info); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info)); + + CHAR_INFO originalCell; + originalCell.Char.UnicodeChar = L'\x30a2'; + originalCell.Attributes = COMMON_LVB_TRAILING_BYTE | FOREGROUND_RED; + + SMALL_RECT writeRegion; + writeRegion.Top = 0; + writeRegion.Bottom = 0; + writeRegion.Left = 0; + writeRegion.Right = 0; + const auto originalWriteRegion = writeRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputW(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion)); + VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion); + + SMALL_RECT readRegion = originalWriteRegion; + const auto originalReadRegion = readRegion; + CHAR_INFO readCell; + + CHAR_INFO expectedCell; + expectedCell.Char.UnicodeChar = L' '; + expectedCell.Attributes = originalCell.Attributes; + WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE); + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputW(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(originalReadRegion, readRegion); + + VERIFY_ARE_NOT_EQUAL(originalCell, readCell); + VERIFY_ARE_EQUAL(expectedCell, readCell); +} + +void DbcsTests::TestDbcsBisectWriteCellsEndA() +{ + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP)); + + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 }; + info.cbSize = sizeof(info); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info)); + + CHAR_INFO originalCell; + originalCell.Char.AsciiChar = '\x82'; + originalCell.Attributes = COMMON_LVB_LEADING_BYTE | FOREGROUND_RED; + + SMALL_RECT writeRegion; + writeRegion.Top = 0; + writeRegion.Bottom = 0; + writeRegion.Left = info.dwSize.X - 1; + writeRegion.Right = info.dwSize.X - 1; + const auto originalWriteRegion = writeRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputA(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion)); + VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion); + + SMALL_RECT readRegion = originalWriteRegion; + const auto originalReadRegion = readRegion; + CHAR_INFO readCell; + + CHAR_INFO expectedCell; + expectedCell.Char.UnicodeChar = L' '; + expectedCell.Attributes = originalCell.Attributes; + WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE); + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputA(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(originalReadRegion, readRegion); + + VERIFY_ARE_NOT_EQUAL(originalCell, readCell); + VERIFY_ARE_EQUAL(expectedCell, readCell); +} + +// This test maintains the legacy behavior for the 932 A codepage route. +void DbcsTests::TestDbcsBisectWriteCellsBeginA() +{ + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleCP(JAPANESE_CP)); + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(JAPANESE_CP)); + + const auto out = GetStdHandle(STD_OUTPUT_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFOEX info = { 0 }; + info.cbSize = sizeof(info); + VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(out, &info)); + + CHAR_INFO originalCell; + originalCell.Char.AsciiChar = '\xA9'; + originalCell.Attributes = COMMON_LVB_TRAILING_BYTE | FOREGROUND_RED; + + SMALL_RECT writeRegion; + writeRegion.Top = 0; + writeRegion.Bottom = 0; + writeRegion.Left = 0; + writeRegion.Right = 0; + const auto originalWriteRegion = writeRegion; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleOutputA(out, &originalCell, { 1, 1 }, { 0, 0 }, &writeRegion)); + VERIFY_ARE_EQUAL(originalWriteRegion, writeRegion); + + SMALL_RECT readRegion = originalWriteRegion; + const auto originalReadRegion = readRegion; + CHAR_INFO readCell; + + CHAR_INFO expectedCell; + expectedCell.Char.UnicodeChar = L'\xffff'; + expectedCell.Char.AsciiChar = originalCell.Char.AsciiChar; + expectedCell.Attributes = originalCell.Attributes; + WI_ClearAllFlags(expectedCell.Attributes, COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE); + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputA(out, &readCell, { 1, 1 }, { 0, 0 }, &readRegion)); + VERIFY_ARE_EQUAL(originalReadRegion, readRegion); + + VERIFY_ARE_NOT_EQUAL(originalCell, readCell); + VERIFY_ARE_EQUAL(expectedCell, readCell); +} + +struct MultibyteInputData +{ + PCWSTR pwszInputText; + PCSTR pszExpectedText; +}; + +const MultibyteInputData MultibyteTestDataSet[] = +{ + { L"\x3042", "\x82\xa0" }, + { L"\x3042" L"3", "\x82\xa0\x33" }, + { L"3" L"\x3042", "\x33\x82\xa0" }, + { L"3" L"\x3042" L"\x3044", "\x33\x82\xa0\x82\xa2" }, + { L"3" L"\x3042" L"\x3044" L"\x3042", "\x33\x82\xa0\x82\xa2\x82\xa0" }, + { L"3" L"\x3042" L"\x3044" L"\x3042" L"\x3044", "\x33\x82\xa0\x82\xa2\x82\xa0\x82\xa2" }, +}; + +void WriteStringToInput(HANDLE hIn, PCWSTR pwszString) +{ + size_t const cchString = wcslen(pwszString); + size_t const cRecords = cchString * 2; // We need double the input records for button down then button up. + + INPUT_RECORD* const irString = new INPUT_RECORD[cRecords]; + VERIFY_IS_NOT_NULL(irString); + + for (size_t i = 0; i < cRecords; i++) + { + irString[i].EventType = KEY_EVENT; + irString[i].Event.KeyEvent.bKeyDown = (i % 2 == 0) ? TRUE : FALSE; + irString[i].Event.KeyEvent.dwControlKeyState = 0; + irString[i].Event.KeyEvent.uChar.UnicodeChar = pwszString[i / 2]; + irString[i].Event.KeyEvent.wRepeatCount = 1; + irString[i].Event.KeyEvent.wVirtualKeyCode = 0; + irString[i].Event.KeyEvent.wVirtualScanCode = 0; + } + + DWORD dwWritten; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputW(hIn, irString, (DWORD)cRecords, &dwWritten)); + + VERIFY_ARE_EQUAL(cRecords, dwWritten, L"We should have written the number of records that were sent in by our buffer."); + + delete[] irString; +} + +void ReadStringWithGetCh(PCSTR pszExpectedText) +{ + size_t const cchString = strlen(pszExpectedText); + + for (size_t i = 0; i < cchString; i++) + { + if (!VERIFY_ARE_EQUAL((BYTE)pszExpectedText[i], _getch())) + { + break; + } + } +} + +void ReadStringWithReadConsoleInputAHelper(HANDLE hIn, PCSTR pszExpectedText, size_t cbBuffer) +{ + Log::Comment(String().Format(L" = Attempting to read back the text with a %d record length buffer. =", cbBuffer)); + + // Find out how many bytes we need to read. + size_t const cchExpectedText = strlen(pszExpectedText); + + // Increment read buffer of the size we were told. + INPUT_RECORD* const irRead = new INPUT_RECORD[cbBuffer]; + VERIFY_IS_NOT_NULL(irRead); + + // Loop reading and comparing until we've read enough times to get all the text we expect. + size_t cchRead = 0; + + while (cchRead < cchExpectedText) + { + // expected read is either the size of the buffer or the number of characters remaining, whichever is smaller. + DWORD const dwReadExpected = (DWORD)min(cbBuffer, cchExpectedText - cchRead); + + DWORD dwRead; + if (!VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputA(hIn, irRead, (DWORD)cbBuffer, &dwRead), L"Attempt to read input into buffer.")) + { + break; + } + + VERIFY_IS_GREATER_THAN_OR_EQUAL(dwRead, (DWORD)0, L"Verify we read non-negative bytes."); + + for (size_t i = 0; i < dwRead; i++) + { + // We might read more events than the ones we're looking for because some other type of event was + // inserted into the queue by outside action. Only look at the key down events. + if (irRead[i].EventType == KEY_EVENT && + irRead[i].Event.KeyEvent.bKeyDown == TRUE) + { + if (!VERIFY_ARE_EQUAL((BYTE)pszExpectedText[cchRead], (BYTE)irRead[i].Event.KeyEvent.uChar.AsciiChar)) + { + break; + } + cchRead++; + } + } + } + + delete[] irRead; +} + +void ReadStringWithReadConsoleInputA(HANDLE hIn, PCWSTR pwszWriteText, PCSTR pszExpectedText) +{ + // Figure out how long the expected length is. + size_t const cchExpectedText = strlen(pszExpectedText); + + // Test every buffer size variation from 1 to the size of the string. + for (size_t i = 1; i <= cchExpectedText; i++) + { + FlushConsoleInputBuffer(hIn); + WriteStringToInput(hIn, pwszWriteText); + ReadStringWithReadConsoleInputAHelper(hIn, pszExpectedText, i); + } +} + +void DbcsTests::TestMultibyteInputRetrieval() +{ + SetConsoleCP(932); + + UINT dwCP = GetConsoleCP(); + if (!VERIFY_ARE_EQUAL(JAPANESE_CP, dwCP, L"Ensure input codepage is Japanese.")) + { + return; + } + + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if (!VERIFY_ARE_NOT_EQUAL(INVALID_HANDLE_VALUE, hIn, L"Get input handle.")) + { + return; + } + + size_t const cDataSet = ARRAYSIZE(MultibyteTestDataSet); + + // for each item in our test data set... + for (size_t i = 0; i < cDataSet; i++) + { + MultibyteInputData data = MultibyteTestDataSet[i]; + + Log::Comment(String().Format(L"=== TEST #%d ===", i)); + Log::Comment(String().Format(L"=== Input '%ws' ===", data.pwszInputText)); + + // test by writing the string and reading back the _getch way. + Log::Comment(L" == SUBTEST A: Use _getch to retrieve. == "); + FlushConsoleInputBuffer(hIn); + WriteStringToInput(hIn, data.pwszInputText); + ReadStringWithGetCh(data.pszExpectedText); + + // test by writing the string and reading back with variable length buffers the ReadConsoleInputA way. + Log::Comment(L" == SUBTEST B: Use ReadConsoleInputA with variable length buffers to retrieve. == "); + ReadStringWithReadConsoleInputA(hIn, data.pwszInputText, data.pszExpectedText); + } + + FlushConsoleInputBuffer(hIn); +} + +void DbcsTests::TestDbcsOneByOne() +{ + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936."); + + // This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936. + // The English translation is "Test Chinese". + // We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler. + char test[] = "\xb2\xe2\xca\xd4\xd6\xd0\xce\xc4"; + + // Prepare structures for readback. + COORD coordReadPos = { 0 }; + DWORD const cchReadBack = 2u; + char chReadBack[2]; + DWORD dwReadOrWritten = 0u; + + for (size_t i = 0; i < strlen(test); i++) + { + bool const fIsLeadByte = (i % 2 == 0); + Log::Comment(fIsLeadByte ? L"Writing lead byte." : L"Writing trailing byte."); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, &(test[i]), 1u, &dwReadOrWritten, nullptr)); + VERIFY_ARE_EQUAL(1u, dwReadOrWritten, L"Verify the byte was reported written."); + + dwReadOrWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, cchReadBack, coordReadPos, &dwReadOrWritten), L"Read back character."); + if (fIsLeadByte) + { + Log::Comment(L"Characters should be empty (space) because we only wrote a lead. It should be held for later."); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[0]); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[1]); + } + else + { + Log::Comment(L"After trailing is written, character should be valid from Chinese plane (not checking exactly, just that it was composed."); + VERIFY_IS_LESS_THAN((unsigned char)'\x80', (unsigned char)chReadBack[0]); + VERIFY_IS_LESS_THAN((unsigned char)'\x80', (unsigned char)chReadBack[1]); + coordReadPos.X += 2; // advance X for next read back. Move 2 positions because it's a wide char. + } + } +} + +void DbcsTests::TestDbcsTrailLead() +{ + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936."); + + // This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936. + // The English translation is "Test Chinese". + // We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler. + char test[] = "\xb2"; + char test2[] = "\xe2\xca"; + char test3[] = "\xd4\xd6\xd0\xce\xc4"; + + // Prepare structures for readback. + COORD const coordReadPos = { 0 }; + DWORD const cchReadBack = 8u; + char chReadBack[9]; + DWORD dwReadOrWritten = 0u; + DWORD cchTestLength = 0; + + Log::Comment(L"1. Write lead byte only."); + cchTestLength = (DWORD)strlen(test); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string."); + VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written."); + dwReadOrWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, 2, coordReadPos, &dwReadOrWritten), L"Read back buffer."); + Log::Comment(L"Verify nothing is written/displayed yet. The read byte should have been consumed/stored but not yet displayed."); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[0]); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[1]); + + Log::Comment(L"2. Write trailing and next lead."); + cchTestLength = (DWORD)strlen(test2); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test2, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string."); + VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written."); + dwReadOrWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, 4, coordReadPos, &dwReadOrWritten), L"Read back buffer."); + Log::Comment(L"Verify previous lead and the trailing we just wrote formed a character. The final lead should have been consumed/stored and not yet displayed."); + VERIFY_ARE_EQUAL((unsigned char)test[0], (unsigned char)chReadBack[0]); + VERIFY_ARE_EQUAL((unsigned char)test2[0], (unsigned char)chReadBack[1]); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[2]); + VERIFY_ARE_EQUAL((unsigned char)' ', (unsigned char)chReadBack[3]); + + Log::Comment(L"3. Write trailing and finish string."); + cchTestLength = (DWORD)strlen(test3); + VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleA(hOut, test3, cchTestLength, &dwReadOrWritten, nullptr), L"Write the string."); + VERIFY_ARE_EQUAL(cchTestLength, dwReadOrWritten, L"Verify all characters reported as written."); + dwReadOrWritten = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, chReadBack, cchReadBack, coordReadPos, &dwReadOrWritten), L"Read back buffer."); + Log::Comment(L"Verify everything is displayed now that we've finished it off with the final trailing and rest of the string."); + VERIFY_ARE_EQUAL((unsigned char)test[0], (unsigned char)chReadBack[0]); + VERIFY_ARE_EQUAL((unsigned char)test2[0], (unsigned char)chReadBack[1]); + VERIFY_ARE_EQUAL((unsigned char)test2[1], (unsigned char)chReadBack[2]); + VERIFY_ARE_EQUAL((unsigned char)test3[0], (unsigned char)chReadBack[3]); + VERIFY_ARE_EQUAL((unsigned char)test3[1], (unsigned char)chReadBack[4]); + VERIFY_ARE_EQUAL((unsigned char)test3[2], (unsigned char)chReadBack[5]); + VERIFY_ARE_EQUAL((unsigned char)test3[3], (unsigned char)chReadBack[6]); + VERIFY_ARE_EQUAL((unsigned char)test3[4], (unsigned char)chReadBack[7]); +} + +void DbcsTests::TestDbcsStdCoutScenario() +{ + HANDLE const hOut = GetStdOutputHandle(); + VERIFY_IS_NOT_NULL(hOut, L"Verify output handle is valid."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(936), L"Ensure output codepage is set to Simplified Chinese 936."); + + // This is Unicode characters U+6D4B U+8BD5 U+4E2D U+6587 in Simplified Chinese Codepage 936. + // The English translation is "Test Chinese". + // We write the bytes in hex to prevent storage/interpretation issues by the source control and compiler. + char test[] = "\xb2\xe2\xca\xd4\xd6\xd0\xce\xc4"; + Log::Comment(L"Write string using printf."); + printf("%s\n", test); + + // Prepare structures for readback. + COORD coordReadPos = { 0 }; + DWORD const cchReadBack = (DWORD)strlen(test); + wistd::unique_ptr const psReadBack = wil::make_unique_failfast(cchReadBack + 1); + DWORD dwRead = 0; + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, psReadBack.get(), cchReadBack, coordReadPos, &dwRead), L"Read back printf line."); + VERIFY_ARE_EQUAL(cchReadBack, dwRead, L"We should have read as many characters as we expected (length of original printed line.)"); + VERIFY_ARE_EQUAL(String(test), String(psReadBack.get()), L"String should match what we wrote."); + + // Clean up and move down a line for next test. + ZeroMemory(psReadBack.get(), cchReadBack); + dwRead = 0; + coordReadPos.Y++; + + Log::Comment(L"Write string using std::cout."); + std::cout << test << std::endl; + + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, psReadBack.get(), cchReadBack, coordReadPos, &dwRead), L"Read back std::cout line."); + VERIFY_ARE_EQUAL(cchReadBack, dwRead, L"We should have read as many characters as we expected (length of original printed line.)"); + VERIFY_ARE_EQUAL(String(test), String(psReadBack.get()), L"String should match what we wrote."); + +} diff --git a/src/host/ft_host/CanaryTests.cpp b/src/host/ft_host/CanaryTests.cpp new file mode 100644 index 000000000..ac170666a --- /dev/null +++ b/src/host/ft_host/CanaryTests.cpp @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +// This class is intended to provide a canary (simple launch test) +// to ensure that activation of the console still works. +class CanaryTests +{ + BEGIN_TEST_CLASS(CanaryTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhostv1.dll") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + END_TEST_CLASS() + + TEST_METHOD(LaunchV1Console); +}; + +static PCWSTR pwszCmdPath = L"%WINDIR%\\system32\\cmd.exe"; +static PCSTR pszCmdGreeting = "Microsoft Windows [Version"; + +static PCWSTR pwszConhostV1Path = L"%WINDIR%\\system32\\conhostv1.dll"; + +void CanaryTests::LaunchV1Console() +{ + // First ensure that this system has the v1 console to test. + wistd::unique_ptr ConhostV1Path; + VERIFY_SUCCEEDED(ExpandPathToMutable(pwszConhostV1Path, ConhostV1Path)); + + if (!CheckIfFileExists(ConhostV1Path.get())) + { + Log::Comment(L"This system does not have the legacy conhostv1.dll module. Skipping test."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + // This will set the console to v1 mode, backing up the current state and restoring when it goes out of scope. + CommonV1V2Helper SetV1ConsoleHelper(CommonV1V2Helper::ForceV2States::V1); + + // Attempt to launch CMD.exe in a new window + // Expand any environment variables present in the command line string. + wistd::unique_ptr CmdLineMutable; + VERIFY_SUCCEEDED(ExpandPathToMutable(pwszCmdPath, CmdLineMutable)); + + // Create output handle for redirection. We'll read from it to make sure CMD started correctly. + // We'll let it have a default input handle to make sure it binds to the new console host window that will be created. + wil::unique_handle OutPipeRead; + wil::unique_handle OutPipeWrite; + SECURITY_ATTRIBUTES InheritableSecurity; + InheritableSecurity.nLength = sizeof(InheritableSecurity); + InheritableSecurity.lpSecurityDescriptor = nullptr; + InheritableSecurity.bInheritHandle = TRUE; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&OutPipeRead, &OutPipeWrite, &InheritableSecurity, 0)); + + // Create Job object to ensure child will be killed when test ends. + wil::unique_handle CanaryJob(CreateJobObjectW(nullptr, nullptr)); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION JobLimits = { 0 }; + JobLimits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + VERIFY_WIN32_BOOL_SUCCEEDED(SetInformationJobObject(CanaryJob.get(), JobObjectExtendedLimitInformation, &JobLimits, sizeof(JobLimits))); + + // Call create process + STARTUPINFOEX StartupInformation = { 0 }; + StartupInformation.StartupInfo.cb = sizeof(STARTUPINFOEX); + StartupInformation.StartupInfo.hStdOutput = OutPipeWrite.get(); + StartupInformation.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + + wil::unique_process_information ProcessInformation; + VERIFY_WIN32_BOOL_SUCCEEDED(CreateProcessW(NULL, + CmdLineMutable.get(), + NULL, + NULL, + TRUE, + CREATE_NEW_CONSOLE, + NULL, + NULL, + &StartupInformation.StartupInfo, + ProcessInformation.addressof())); + + // Attach process to job so it dies when we exit this test scope and the handle is released. + VERIFY_WIN32_BOOL_SUCCEEDED(AssignProcessToJobObject(CanaryJob.get(), ProcessInformation.hProcess)); + + // Release our ownership of the Write side of the Out Pipe now that it has been transferred to the child process. + OutPipeWrite.reset(); + + // Wait a second for work to happen. + Sleep(1000); + + // The process should still be running and active. + DWORD dwExitCode = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(GetExitCodeProcess(ProcessInformation.hProcess, &dwExitCode)); + + VERIFY_ARE_EQUAL(STILL_ACTIVE, dwExitCode); + + // Read out our redirected output to see that CMD's startup greeting has been printed + const size_t cchCmdGreeting = strlen(pszCmdGreeting); + wistd::unique_ptr pszOutputBuffer = wil::make_unique_nothrow(cchCmdGreeting + 1); + + const DWORD dwReadExpected = static_cast((cchCmdGreeting * sizeof(char))); + DWORD dwReadActual = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(OutPipeRead.get(), pszOutputBuffer.get(), dwReadExpected, &dwReadActual, nullptr)); + VERIFY_ARE_EQUAL(dwReadExpected, dwReadActual); + VERIFY_ARE_EQUAL(String(pszCmdGreeting), String(pszOutputBuffer.get())); +} diff --git a/src/host/ft_host/Common.cpp b/src/host/ft_host/Common.cpp new file mode 100644 index 000000000..8ffca5941 --- /dev/null +++ b/src/host/ft_host/Common.cpp @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +HANDLE Common::_hConsole = INVALID_HANDLE_VALUE; + +void VerifySucceededGLE(BOOL bResult) +{ + if (!bResult) + { + VERIFY_FAIL(NoThrowString().Format(L"API call failed: 0x%x", GetLastError())); + } +} + +void DoFailure(PCWSTR pwszFunc, DWORD dwErrorCode) +{ + Log::Comment(NoThrowString().Format(L"'%s' call failed with error 0x%x", pwszFunc, dwErrorCode)); + VERIFY_FAIL(); +} + +void GlePattern(PCWSTR pwszFunc) +{ + DoFailure(pwszFunc, GetLastError()); +} + +bool CheckLastErrorNegativeOneFail(DWORD dwReturn, PCWSTR pwszFunc) +{ + if (dwReturn == -1) + { + GlePattern(pwszFunc); + return false; + } + else + { + return true; + } +} + +bool CheckLastErrorZeroFail(int iValue, PCWSTR pwszFunc) +{ + if (iValue == 0) + { + GlePattern(pwszFunc); + return false; + } + else + { + return true; + } +} + +bool CheckLastErrorWait(DWORD dwReturn, PCWSTR pwszFunc) +{ + if (CheckLastErrorNegativeOneFail(dwReturn, pwszFunc)) + { + if (dwReturn == STATUS_WAIT_0) + { + return true; + } + else + { + DoFailure(pwszFunc, dwReturn); + return false; + } + } + else + { + return false; + } +} + +bool CheckLastError(HRESULT hr, PCWSTR pwszFunc) +{ + if (!SUCCEEDED(hr)) + { + DoFailure(pwszFunc, hr); + return false; + } + else + { + return true; + } +} + +bool CheckLastError(BOOL fSuccess, PCWSTR pwszFunc) +{ + if (!fSuccess) + { + GlePattern(pwszFunc); + return false; + } + else + { + return true; + } +} + +bool CheckLastError(HANDLE handle, PCWSTR pwszFunc) +{ + if (handle == INVALID_HANDLE_VALUE) + { + GlePattern(pwszFunc); + return false; + } + else + { + return true; + } +} + +HRESULT ExpandPathToMutable(_In_ PCWSTR pwszPath, _Out_ wistd::unique_ptr& MutablePath) noexcept +{ + // Find how many characters we need. + const DWORD cchExpanded = ExpandEnvironmentStringsW(pwszPath, nullptr, 0); + RETURN_LAST_ERROR_IF(0 == cchExpanded); + + // Allocate space to hold result + wistd::unique_ptr NewMutable = wil::make_unique_nothrow(cchExpanded); + RETURN_IF_NULL_ALLOC(NewMutable); + + // Expand string into allocated space + RETURN_LAST_ERROR_IF(0 == ExpandEnvironmentStringsW(pwszPath, NewMutable.get(), cchExpanded)); + + // On success, give our string back out (swapping with what was given and we'll free it for the caller.) + MutablePath.swap(NewMutable); + + return S_OK; +} + +bool CheckIfFileExists(_In_ PCWSTR pwszPath) noexcept +{ + wil::unique_hfile hFile(CreateFileW(pwszPath, + GENERIC_READ, + 0, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr)); + + if (hFile.get() != nullptr && hFile.get() != INVALID_HANDLE_VALUE) + { + return true; + } + else + { + return false; + } +} + +BOOL UnadjustWindowRectEx( + LPRECT prc, + DWORD dwStyle, + BOOL fMenu, + DWORD dwExStyle) +{ + RECT rc; + SetRectEmpty(&rc); + BOOL fRc = AdjustWindowRectEx(&rc, dwStyle, fMenu, dwExStyle); + if (fRc) { + prc->left -= rc.left; + prc->top -= rc.top; + prc->right -= rc.right; + prc->bottom -= rc.bottom; + } + return fRc; +} + +static HANDLE GetStdHandleVerify(const DWORD dwHandleType) +{ + const HANDLE hConsole = GetStdHandle(dwHandleType); + VERIFY_ARE_NOT_EQUAL(hConsole, INVALID_HANDLE_VALUE, L"Ensure we got a valid console handle"); + VERIFY_IS_NOT_NULL(hConsole, L"Ensure we got a non-null console handle"); + + return hConsole; +} + +HANDLE GetStdOutputHandle() +{ + return GetStdHandleVerify(STD_OUTPUT_HANDLE); +} + +HANDLE GetStdInputHandle() +{ + return GetStdHandleVerify(STD_INPUT_HANDLE); +} + +bool Common::TestBufferSetup() +{ + // Creating a new screen buffer for each test will make sure that it doesn't interact with the output that TAEF is spewing + // to the default output buffer at the same time. + + _hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, + 0 /*dwShareMode*/, + NULL /*lpSecurityAttributes*/, + CONSOLE_TEXTMODE_BUFFER, + NULL /*lpReserved*/); + + VERIFY_ARE_NOT_EQUAL(_hConsole, INVALID_HANDLE_VALUE, L"Creating our test screen buffer."); + + VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleActiveScreenBuffer(_hConsole), L"Applying test screen buffer to console"); + + return true; +} + +bool Common::TestBufferCleanup() +{ + if (_hConsole != INVALID_HANDLE_VALUE) + { + // Simply freeing the handle will restore the next screen buffer down in the stack. + VERIFY_WIN32_BOOL_SUCCEEDED(CloseHandle(_hConsole), L"Removing our test screen buffer."); + } + + return true; +} + +static PCWSTR pwszConsoleKeyName = L"Console"; +static PCWSTR pwszForceV2ValueName = L"ForceV2"; + +CommonV1V2Helper::CommonV1V2Helper(const ForceV2States ForceV2StateDesired) +{ + // Open console key + if (!OneCoreDelay::IsIsWindowPresent()) + { + Log::Comment(L"OneCore based systems don't have v1 state. Skipping."); + _fRestoreOnExit = false; + return; + } + + LSTATUS lstatus = RegOpenKeyExW(HKEY_CURRENT_USER, pwszConsoleKeyName, 0, KEY_READ | KEY_WRITE, &_consoleKey); + if (ERROR_ACCESS_DENIED == lstatus) + { + // UAP and some systems won't let us modify the registry. That's OK. Try to run the tests. + // Environments where we can't modify the registry should already be set up for the new/v2 console + // and not need further configuration. + Log::Comment(L"Skipping backup in environment that cannot access console key."); + _fRestoreOnExit = false; + return; + } + + VERIFY_ARE_EQUAL(ERROR_SUCCESS, lstatus); + + Log::Comment(L"Backing up v1/v2 console state."); + DWORD cbForceV2Original = sizeof(_dwForceV2Original); + + lstatus = RegQueryValueExW(_consoleKey.get(), pwszForceV2ValueName, nullptr, nullptr, (LPBYTE)&_dwForceV2Original, &cbForceV2Original); + if (ERROR_FILE_NOT_FOUND == lstatus) + { + Log::Comment(L"This machine doesn't have v1/v2 state. Skipping."); + _consoleKey.reset(); + _fRestoreOnExit = false; + } + else + { + VERIFY_WIN32_BOOL_FAILED(lstatus, L"Assert querying ForceV2 key was successful."); + _fRestoreOnExit = true; + + Log::Comment(String().Format(L"Setting v1/v2 console state to desired '%d'", ForceV2StateDesired)); + VERIFY_WIN32_BOOL_FAILED(RegSetValueExW(_consoleKey.get(), pwszForceV2ValueName, 0, REG_DWORD, (LPBYTE)&ForceV2StateDesired, sizeof(ForceV2StateDesired))); + } +} + +CommonV1V2Helper::~CommonV1V2Helper() +{ + if (_fRestoreOnExit) + { + Log::Comment(String().Format(L"Restoring v1/v2 console state to original '%d'", _dwForceV2Original)); + VERIFY_WIN32_BOOL_FAILED(RegSetValueExW(_consoleKey.get(), pwszForceV2ValueName, 0, REG_DWORD, (LPBYTE)&_dwForceV2Original, sizeof(_dwForceV2Original))); + } +} diff --git a/src/host/ft_host/Common.hpp b/src/host/ft_host/Common.hpp new file mode 100644 index 000000000..f5c0022b6 --- /dev/null +++ b/src/host/ft_host/Common.hpp @@ -0,0 +1,74 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Common.hpp + +Abstract: +- This module contains common items for the API tests + +Author: +- Michael Niksa (MiNiksa) 2015 +- Paul Campbell (PaulCam) 2015 + +Revision History: +--*/ + +#pragma once + +#include "..\..\inc\consoletaeftemplates.hpp" + +class Common +{ +public: + static bool TestBufferSetup(); + static bool TestBufferCleanup(); + static HANDLE _hConsole; +}; + +class CommonV1V2Helper +{ +public: + enum class ForceV2States : DWORD + { + V1 = 0, + V2 = 1 + }; + + CommonV1V2Helper(const ForceV2States ForceV2StateDesired); + ~CommonV1V2Helper(); + +private: + bool _fRestoreOnExit = false; + DWORD _dwForceV2Original = 0; + wil::unique_hkey _consoleKey; +}; + +// Helper to cause a VERIFY_FAIL and get the last error code for functions that return null-like things. +void VerifySucceededGLE(BOOL bResult); + +void DoFailure(PCWSTR pwszFunc, DWORD dwErrorCode); +void GlePattern(PCWSTR pwszFunc); +bool CheckLastErrorNegativeOneFail(DWORD dwReturn, PCWSTR pwszFunc); +bool CheckLastErrorZeroFail(int iValue, PCWSTR pwszFunc); +bool CheckLastErrorWait(DWORD dwReturn, PCWSTR pwszFunc); +bool CheckLastError(HRESULT hr, PCWSTR pwszFunc); +bool CheckLastError(BOOL fSuccess, PCWSTR pwszFunc); +bool CheckLastError(HANDLE handle, PCWSTR pwszFunc); + +[[nodiscard]] +bool CheckIfFileExists(_In_ PCWSTR pwszPath) noexcept; + +[[nodiscard]] +HRESULT ExpandPathToMutable(_In_ PCWSTR pwszPath, _Out_ wistd::unique_ptr& MutablePath) noexcept; + +//http://blogs.msdn.com/b/oldnewthing/archive/2013/10/17/10457292.aspx +BOOL UnadjustWindowRectEx( + LPRECT prc, + DWORD dwStyle, + BOOL fMenu, + DWORD dwExStyle); + +HANDLE GetStdInputHandle(); +HANDLE GetStdOutputHandle(); diff --git a/src/host/ft_host/DefaultResource.rc b/src/host/ft_host/DefaultResource.rc new file mode 100644 index 000000000..85ec2648d --- /dev/null +++ b/src/host/ft_host/DefaultResource.rc @@ -0,0 +1,12 @@ +//Autogenerated file name + version resource file for Device Guard whitelisting effort + +#include +#include + +#define VER_FILETYPE VFT_UNKNOWN +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR ___TARGETNAME +#define VER_INTERNALNAME_STR ___TARGETNAME +#define VER_ORIGINALFILENAME_STR ___TARGETNAME + +#include "common.ver" diff --git a/src/host/ft_host/Host.FeatureTests.vcxproj b/src/host/ft_host/Host.FeatureTests.vcxproj new file mode 100644 index 000000000..d206af5f2 --- /dev/null +++ b/src/host/ft_host/Host.FeatureTests.vcxproj @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + + {8CDB8850-7484-4EC7-B45B-181F85B2EE54} + Win32Proj + HostFeatureTests + Host.Tests.Feature + ConHost.Feature.Tests + + + + $(ProjectDir);%(AdditionalIncludeDirectories) + + + + + + + \ No newline at end of file diff --git a/src/host/ft_host/Host.FeatureTests.vcxproj.filters b/src/host/ft_host/Host.FeatureTests.vcxproj.filters new file mode 100644 index 000000000..f0e7dfc2c --- /dev/null +++ b/src/host/ft_host/Host.FeatureTests.vcxproj.filters @@ -0,0 +1,105 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {ff4de03f-e6e3-44a0-9a53-06d0b3943469} + + + {ac4e9c27-4849-46e8-bd03-0af91bcb6a87} + + + {f4d0ca10-abd1-4469-b871-ffc5098754e3} + + + + + Source Files + + + Source Files\API + + + Source Files + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files\Message + + + Source Files\CJK + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files\API + + + Source Files + + + Source Files + + + Source Files + + + Source Files\API + + + Source Files\API + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + \ No newline at end of file diff --git a/src/host/ft_host/InitTests.cpp b/src/host/ft_host/InitTests.cpp new file mode 100644 index 000000000..dbe8138e9 --- /dev/null +++ b/src/host/ft_host/InitTests.cpp @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +const DWORD _dwMaxMillisecondsToWaitOnStartup = 120 * 1000; +const DWORD _dwStartupWaitPollingIntervalInMilliseconds = 200; + +static PCWSTR pwszConsoleKeyName = L"Console"; +static PCWSTR pwszForceV2ValueName = L"ForceV2"; + +// This class is intended to set up the testing environment against the produced binary +// instead of using the Windows-default copy of console host. + +wil::unique_handle hJob; + +static FILE* std_out = nullptr; +static FILE* std_in = nullptr; + +// This will automatically try to terminate the job object (and all of the +// binaries under test that are children) whenever this class gets shut down. +// also closes the FILE pointers created by reopening stdin and stdout. +auto OnAppExitKillJob = wil::scope_exit([&] { + if (std_out != nullptr) + { + fclose(std_out); + } + if (std_in != nullptr) + { + fclose(std_in); + } + if (nullptr != hJob.get()) + { + THROW_LAST_ERROR_IF(!TerminateJobObject(hJob.get(), S_OK)); + } +}); + +wistd::unique_ptr v2ModeHelper; + +BEGIN_MODULE() + MODULE_PROPERTY(L"WinPerfSource", L"Console") + MODULE_PROPERTY(L"WinPerf.WPRProfile", L"ConsolePerf.wprp") + MODULE_PROPERTY(L"WinPerf.WPRProfileId", L"ConsolePerf.Verbose.File") + MODULE_PROPERTY(L"WinPerf.Regions", L"ConsolePerf.Regions.xml") +END_MODULE() + +MODULE_SETUP(ModuleSetup) +{ + // The sources files inside windows use a C define to say it's inside windows and we should be + // testing against the inbox conhost. This is awesome for inbox TAEF RI gate tests so it uses + // the one generated from the same build. + bool insideWindows = false; +#ifdef __INSIDE_WINDOWS + insideWindows = true; +#endif + + bool forceOpenConsole = false; + RuntimeParameters::TryGetValue(L"ForceOpenConsole", forceOpenConsole); + + if (forceOpenConsole) + { + insideWindows = false; + } + + // Look up a runtime parameter to see if we want to test as v1. + // This is useful while developing tests to try to see if they run the same on v2 and v1. + bool testAsV1 = false; + RuntimeParameters::TryGetValue(L"TestAsV1", testAsV1); + + if (testAsV1) + { + v2ModeHelper.reset(new CommonV1V2Helper(CommonV1V2Helper::ForceV2States::V1)); + } + else + { + v2ModeHelper.reset(new CommonV1V2Helper(CommonV1V2Helper::ForceV2States::V2)); + } + + // Retrieve location of directory that the test was deployed to. + // We're going to look for OpenConsole.exe in the same directory. + String value; + VERIFY_SUCCEEDED_RETURN(RuntimeParameters::TryGetValue(L"TestDeploymentDir", value)); + + // If inside windows or testing as v1, use the inbox conhost to launch by just specifying the test EXE name. + // The OS will auto-start the inbox conhost to host this process. + if (insideWindows || testAsV1) + { + value = value.Append(L"Nihilist.exe"); + } + else + { + // If we're outside or testing V2, let's use the open console binary we built. + value = value.Append(L"OpenConsole.exe Nihilist.exe"); + } + + // Must make mutable string of appropriate length to feed into args. + size_t const cchNeeded = value.GetLength() + 1; + + // We use regular new (not a smart pointer) and a scope exit delete because CreateProcess needs mutable space + // and it'd be annoying to const_cast the smart pointer's .get() just for the sake of. + PWSTR str = new WCHAR[cchNeeded]; + auto cleanStr = wil::scope_exit([&] { if (nullptr != str) { delete[] str; }}); + + VERIFY_SUCCEEDED_RETURN(StringCchCopyW(str, cchNeeded, (WCHAR*)value.GetBuffer())); + + // Create a job object to hold the OpenConsole.exe process and the child it creates + // so we can terminate it easily when we exit. + hJob.reset(CreateJobObjectW(nullptr, nullptr)); + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(nullptr != hJob); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION JobLimits = { 0 }; + JobLimits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + VERIFY_WIN32_BOOL_SUCCEEDED(SetInformationJobObject(hJob.get(), JobObjectExtendedLimitInformation, &JobLimits, sizeof(JobLimits))); + + // Setup and call create process. + STARTUPINFOW si = { 0 }; + si.cb = sizeof(STARTUPINFOW); + wil::unique_process_information pi; + + // We start suspended so we can put it in the job before it does anything + // We say new console so it doesn't run in the same window as our test. + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(CreateProcessW(nullptr, + str, + nullptr, + nullptr, + FALSE, + CREATE_NEW_CONSOLE | CREATE_SUSPENDED, + nullptr, + nullptr, + &si, + pi.addressof())); + + // Put the new OpenConsole process into the job. The default Job system means when OpenConsole + // calls CreateProcess, its children will automatically join the job. + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(AssignProcessToJobObject(hJob.get(), pi.hProcess)); + + // Let the thread run + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(-1 != ResumeThread(pi.hThread)); + + // We have to enter a wait loop here to compensate for Code Coverage instrumentation that might be + // injected into the process. That takes a while. + DWORD dwTotalWait = 0; + + JOBOBJECT_BASIC_PROCESS_ID_LIST pids; + pids.NumberOfAssignedProcesses = 2; + while (dwTotalWait < _dwMaxMillisecondsToWaitOnStartup) + { + QueryInformationJobObject(hJob.get(), + JobObjectBasicProcessIdList, + &pids, + sizeof(pids), + nullptr); + + // When there is >1 process in the job, OpenConsole has finally got around to starting cmd.exe. + // It was held up on instrumentation most likely. + if (pids.NumberOfAssignedProcesses > 1) + { + break; + } + else if (pids.NumberOfAssignedProcesses < 1) + { + VERIFY_FAIL(); + } + + Sleep(_dwStartupWaitPollingIntervalInMilliseconds); + dwTotalWait += _dwStartupWaitPollingIntervalInMilliseconds; + } + // If it took too long, throw so the test ends here. + VERIFY_IS_LESS_THAN(dwTotalWait, _dwMaxMillisecondsToWaitOnStartup); + + // Now retrieve the actual list of process IDs in the job. + DWORD cbRequired = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + sizeof(ULONG_PTR) * pids.NumberOfAssignedProcesses; + PJOBOBJECT_BASIC_PROCESS_ID_LIST pPidList = reinterpret_cast(HeapAlloc(GetProcessHeap(), + 0, + cbRequired)); + VERIFY_IS_NOT_NULL(pPidList); + auto scopeExit = wil::scope_exit([&]() { HeapFree(GetProcessHeap(), 0, pPidList); }); + + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(QueryInformationJobObject(hJob.get(), + JobObjectBasicProcessIdList, + pPidList, + cbRequired, + nullptr)); + + VERIFY_ARE_EQUAL(pids.NumberOfAssignedProcesses, pPidList->NumberOfProcessIdsInList); + + // Dig through the list to find the one that isn't the OpenConsole window and assume it's CMD.exe + DWORD dwFindPid = 0; + for (size_t i = 0; i < pPidList->NumberOfProcessIdsInList; i++) + { + ULONG_PTR const pidCandidate = pPidList->ProcessIdList[i]; + + if (pidCandidate != pi.dwProcessId && 0 != pidCandidate) + { + dwFindPid = static_cast(pidCandidate); + break; + } + } + + // If we launched the binary directly, we have to use the PID that we just launched, not search for the other attached one. + if (insideWindows || testAsV1) + { + dwFindPid = pi.dwProcessId; + } + + // Verify we found a valid pid. + VERIFY_ARE_NOT_EQUAL(0u, dwFindPid); + + // Now detach from our current console (if we have one) and instead attach + // to the one that belongs to the CMD.exe in the new OpenConsole.exe window. + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(FreeConsole()); + + // Wait a moment for the driver to be ready after freeing to attach. + Sleep(1000); + + VERIFY_WIN32_BOOL_SUCCEEDED_RETURN(AttachConsole(dwFindPid)); + + // Replace CRT handles + // These need to be reopened as read/write or they can affect some of the tests. + // + // std_out and std_in need to be closed when tests are finished, this is handled by the wil::scope_exit at the + // top of this file. + errno_t err = 0; + err = freopen_s(&std_out, "CONOUT$", "w+", stdout); + VERIFY_ARE_EQUAL(0, err); + err = freopen_s(&std_in, "CONIN$", "r+", stdin); + VERIFY_ARE_EQUAL(0, err); + + return true; +} + +MODULE_CLEANUP(ModuleCleanup) +{ + v2ModeHelper.reset(); + return true; +} diff --git a/src/host/ft_host/Message_KeyPressTests.cpp b/src/host/ft_host/Message_KeyPressTests.cpp new file mode 100644 index 000000000..9374dae47 --- /dev/null +++ b/src/host/ft_host/Message_KeyPressTests.cpp @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\..\inc\consoletaeftemplates.hpp" + +#include +#include +#include +#include +#include + +#define KEY_STATE_TOGGLED (0x1) +#define KEY_STATE_PRESSED (0x80) +#define KEY_STATE_RELEASED (0x0) + +#define KEY_MESSAGE_CONTEXT_CODE (0x20000000) +#define KEY_MESSAGE_UPKEY_CODE (0xC0000000) +#define SINGLE_KEY_REPEAT (0x00000001) +#define EXTENDED_KEY_FLAG (0x01000000) + +#define SLEEP_WAIT_TIME (2 * 1000) +#define GERMAN_KEYBOARD_LAYOUT (MAKELANGID(LANG_GERMAN, SUBLANG_GERMAN)) + +class KeyPressTests +{ + BEGIN_TEST_CLASS(KeyPressTests) + TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"conhost.exe") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincon.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"winconp.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"wincontypes.h") + TEST_CLASS_PROPERTY(L"ArtifactUnderTest", L"conmsgl1.h") + END_TEST_CLASS() + + void TurnOffModifierKeys(HWND hwnd) + { + // these are taken from GetControlKeyState. + static const WPARAM modifiers[8] = { + VK_LMENU, + VK_RMENU, + VK_LCONTROL, + VK_RCONTROL, + VK_SHIFT, + VK_NUMLOCK, + VK_SCROLL, + VK_CAPITAL + }; + for (unsigned int i = 0; i < 8; ++i) + { + PostMessage(hwnd, CM_SET_KEY_STATE, modifiers[i], KEY_STATE_RELEASED); + } + } + + TEST_METHOD(TestContextMenuKey) + { + if (!OneCoreDelay::IsPostMessageWPresent()) + { + Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + Log::Comment(L"Checks that the context menu key is correctly added to the input buffer."); + Log::Comment(L"This test will fail on some keyboard layouts. Ensure you're using a QWERTY keyboard if " \ + L"you're encountering a test failure here."); + + HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + HANDLE inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events)); + VERIFY_ARE_EQUAL(events, 0u); + + // send context menu key event + TurnOffModifierKeys(hwnd); + Sleep(SLEEP_WAIT_TIME); + UINT scanCode = MapVirtualKeyW(VK_APPS, MAPVK_VK_TO_VSC); + PostMessageW(hwnd, WM_KEYDOWN, VK_APPS, EXTENDED_KEY_FLAG | SINGLE_KEY_REPEAT | (scanCode << 16)); + Sleep(SLEEP_WAIT_TIME); + + INPUT_RECORD expectedRecord; + expectedRecord.EventType = KEY_EVENT; + expectedRecord.Event.KeyEvent.uChar.UnicodeChar = 0x0; + expectedRecord.Event.KeyEvent.bKeyDown = true; + expectedRecord.Event.KeyEvent.dwControlKeyState = ENHANCED_KEY; + expectedRecord.Event.KeyEvent.dwControlKeyState |= (GetKeyState(VK_NUMLOCK) & KEY_STATE_TOGGLED) ? NUMLOCK_ON : 0; + expectedRecord.Event.KeyEvent.wRepeatCount = SINGLE_KEY_REPEAT; + expectedRecord.Event.KeyEvent.wVirtualKeyCode = VK_APPS; + expectedRecord.Event.KeyEvent.wVirtualScanCode = (WORD)scanCode; + + // get the input record back and test it + INPUT_RECORD record; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(inputHandle, &record, 1, &events)); + VERIFY_IS_GREATER_THAN(events, 0u); + VERIFY_ARE_EQUAL(expectedRecord, record); + } + + + BEGIN_TEST_METHOD(TestAltGr) + TEST_METHOD_PROPERTY(L"Ignore[@DevTest=true]", L"false") + TEST_METHOD_PROPERTY(L"Ignore[default]", L"true") + END_TEST_METHOD() + + + TEST_METHOD(TestCoalesceSameKeyPress) + { + if (!OneCoreDelay::IsSendMessageWPresent()) + { + Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + Log::Comment(L"Testing that key events are properly coalesced when the same key is pressed repeatedly"); + BOOL successBool; + HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + HANDLE inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_ARE_EQUAL(events, 0u); + + // send a bunch of 'a' keypresses to the console + DWORD repeatCount = 1; + const unsigned int messageSendCount = 1000; + for (unsigned int i = 0; i < messageSendCount; ++i) + { + SendMessage(hwnd, + WM_CHAR, + 0x41, + repeatCount); + } + + // make sure the the keypresses got processed and coalesced + events = 0; + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_IS_GREATER_THAN(events, 0u, NoThrowString().Format(L"%d", events)); + std::unique_ptr inputBuffer = std::make_unique(1); + PeekConsoleInput(inputHandle, + inputBuffer.get(), + 1, + &events); + VERIFY_ARE_EQUAL(events, 1u); + VERIFY_ARE_EQUAL(inputBuffer[0].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(inputBuffer[0].Event.KeyEvent.wRepeatCount, messageSendCount, NoThrowString().Format(L"%d", inputBuffer[0].Event.KeyEvent.wRepeatCount)); + } + + TEST_METHOD(TestCtrlKeyDownUp) + { + BEGIN_TEST_METHOD_PROPERTIES() + // VKeys for A-Z + // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + TEST_METHOD_PROPERTY(L"Data:vKey", L"{" + "0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F," + "0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A" + "}") + END_TEST_METHOD_PROPERTIES(); + + if (!OneCoreDelay::IsSendMessageWPresent()) + { + Log::Comment(L"Ctrl key eventing scenario can't be checked on platform without window message queuing."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + UINT vk; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"vKey", vk)); + + Log::Comment(L"Testing the right number of input events is generated by Ctrl+Key press"); + BOOL successBool; + HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + HANDLE inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // Set the console to raw mode, so that it doesn't hijack any keypresses as shortcut keys + SetConsoleMode(inputHandle, 0); + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events)); + VERIFY_ARE_EQUAL(events, 0u); + + DWORD dwInMode = 0; + GetConsoleMode(inputHandle, &dwInMode); + Log::Comment(NoThrowString().Format(L"Mode:0x%x", dwInMode)); + + UINT vkCtrl = VK_LCONTROL; // Need this instead of VK_CONTROL + UINT uiCtrlScancode = MapVirtualKey(vkCtrl , MAPVK_VK_TO_VSC); + // According to + // KEY_KEYDOWN https://msdn.microsoft.com/en-us/library/windows/desktop/ms646280(v=vs.85).aspx + // KEY_UP https://msdn.microsoft.com/en-us/library/windows/desktop/ms646281(v=vs.85).aspx + LPARAM CtrlFlags = (LOBYTE(uiCtrlScancode)<<16) | SINGLE_KEY_REPEAT; + LPARAM CtrlUpFlags = CtrlFlags | KEY_MESSAGE_UPKEY_CODE; + + UINT uiScancode = MapVirtualKey(vk , MAPVK_VK_TO_VSC); + LPARAM DownFlags = (LOBYTE(uiScancode) << 16) | SINGLE_KEY_REPEAT; + LPARAM UpFlags = DownFlags | KEY_MESSAGE_UPKEY_CODE; + + Log::Comment(NoThrowString().Format(L"Testing Ctrl+%c", vk)); + Log::Comment(NoThrowString().Format(L"DownFlags=0x%x, CtrlFlags=0x%x", DownFlags, CtrlFlags)); + Log::Comment(NoThrowString().Format(L"UpFlags=0x%x, CtrlUpFlags=0x%x", UpFlags, CtrlUpFlags)); + + // Don't Use PostMessage, those events come in the wrong order. + // Also can't use SendInput because of the whole test window backgrounding thing. + // It'd work locally, until you minimize the window. + SendMessage(hwnd, WM_KEYDOWN, vkCtrl, CtrlFlags); + SendMessage(hwnd, WM_KEYDOWN, vk, DownFlags); + SendMessage(hwnd, WM_KEYUP, vk, UpFlags); + SendMessage(hwnd, WM_KEYUP, vkCtrl, CtrlUpFlags); + + Sleep(50); + + events = 0; + successBool = GetNumberOfConsoleInputEvents(inputHandle, &events); + VERIFY_IS_TRUE(!!successBool); + VERIFY_IS_GREATER_THAN(events, 0u, NoThrowString().Format(L"%d events found", events)); + + std::unique_ptr inputBuffer = std::make_unique(16); + PeekConsoleInput(inputHandle, + inputBuffer.get(), + 16, + &events); + + for (size_t i = 0; i < events; i++) + { + INPUT_RECORD rc = inputBuffer[i]; + switch (rc.EventType) + { + case KEY_EVENT: + { + Log::Comment(NoThrowString().Format( + L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char: %c (0x%x) KeyState: 0x%x", + rc.Event.KeyEvent.bKeyDown, + rc.Event.KeyEvent.wRepeatCount, + rc.Event.KeyEvent.wVirtualKeyCode, + rc.Event.KeyEvent.wVirtualScanCode, + rc.Event.KeyEvent.uChar.UnicodeChar != 0 ? rc.Event.KeyEvent.uChar.UnicodeChar : ' ', + rc.Event.KeyEvent.uChar.UnicodeChar, + rc.Event.KeyEvent.dwControlKeyState + )); + + break; + } + default: + Log::Comment(NoThrowString().Format(L"Another event type was found.")); + } + } + VERIFY_ARE_EQUAL(events, 4u); + VERIFY_ARE_EQUAL(inputBuffer[0].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(inputBuffer[1].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(inputBuffer[2].EventType, KEY_EVENT); + VERIFY_ARE_EQUAL(inputBuffer[3].EventType, KEY_EVENT); + + FlushConsoleInputBuffer(inputHandle); + } + + TEST_METHOD(TestMaximize) + { + if (!OneCoreDelay::IsSendMessageWPresent()) + { + Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping."); + Log::Result(WEX::Logging::TestResults::Skipped); + return; + } + + const HANDLE inputHandle = GetStdHandle(STD_INPUT_HANDLE); + const HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + + // Need the console to be in processed input for this to work + SetConsoleMode(inputHandle, ENABLE_PROCESSED_INPUT); + FlushConsoleInputBuffer(inputHandle); + + LONG oldStyle = GetWindowLongW(hwnd, GWL_STYLE); + LONG oldExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE); + + // According to + // KEY_KEYDOWN https://msdn.microsoft.com/en-us/library/windows/desktop/ms646280(v=vs.85).aspx + // KEY_UP https://msdn.microsoft.com/en-us/library/windows/desktop/ms646281(v=vs.85).aspx + const UINT vsc = MapVirtualKey(VK_F11, MAPVK_VK_TO_VSC); + const LPARAM F11Flags = (LOBYTE(vsc)<<16) | SINGLE_KEY_REPEAT; + const LPARAM F11UpFlags = F11Flags | KEY_MESSAGE_UPKEY_CODE; + + // Send F11 key down and up. lParam is VirtualScanCode and RepeatCount + SendMessage(hwnd, WM_KEYDOWN, VK_F11, F11Flags ); + SendMessage(hwnd, WM_KEYUP, VK_F11, F11UpFlags ); + + LONG maxStyle = GetWindowLongW(hwnd, GWL_STYLE); + LONG maxExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE); + + // Send F11 key down and up. lParam is VirtualScanCode and RepeatCount + SendMessage(hwnd, WM_KEYDOWN, VK_F11, F11Flags ); + SendMessage(hwnd, WM_KEYUP, VK_F11, F11UpFlags ); + + LONG newStyle = GetWindowLongW(hwnd, GWL_STYLE); + LONG newExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE); + + // Maximize windows should not be Overlapped & have a popup + // Extended style should have a window edge when not maximized + VERIFY_IS_TRUE(WI_IsFlagSet(maxStyle, WS_POPUP)); + VERIFY_IS_TRUE(WI_AreAllFlagsClear(maxStyle, WS_OVERLAPPEDWINDOW)); + VERIFY_IS_TRUE(WI_IsFlagClear(maxExStyle, WS_EX_WINDOWEDGE)); + + VERIFY_IS_TRUE(WI_IsFlagClear(newStyle, WS_POPUP)); + VERIFY_IS_TRUE(WI_AreAllFlagsSet(newStyle, WS_OVERLAPPEDWINDOW)); + VERIFY_IS_TRUE(WI_IsFlagSet(newExStyle, WS_EX_WINDOWEDGE)); + + VERIFY_ARE_NOT_EQUAL(maxStyle, oldStyle); + VERIFY_ARE_NOT_EQUAL(maxExStyle, oldExStyle); + + // Ignore the scrollbars when comparing styles + WI_ClearAllFlags(oldStyle, WS_HSCROLL | WS_VSCROLL); + WI_ClearAllFlags(newStyle, WS_HSCROLL | WS_VSCROLL); + VERIFY_ARE_EQUAL(oldStyle, newStyle); + VERIFY_ARE_EQUAL(oldExStyle, newExStyle); + } +}; + +void KeyPressTests::TestAltGr() +{ + Log::Comment(L"Checks that alt-gr behavior is maintained."); + HWND hwnd = GetConsoleWindow(); + VERIFY_IS_TRUE(!!IsWindow(hwnd)); + HANDLE inputHandle = GetStdHandle(STD_INPUT_HANDLE); + DWORD events = 0; + + // flush input buffer + FlushConsoleInputBuffer(inputHandle); + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events)); + VERIFY_ARE_EQUAL(events, 0u); + + // create german locale string + std::wstringstream wss; + wss << std::setfill(L'0') << std::setw(8) << std::hex << GERMAN_KEYBOARD_LAYOUT; + std::wstring germanKeyboardLayoutString(wss.str()); + + // save current keyboard layout + wchar_t originalLocaleId[KL_NAMELENGTH]; + GetKeyboardLayoutName(originalLocaleId); + + // make console window the topmost window + SetForegroundWindow(hwnd); + + // change to german keyboard layout + PostMessage(hwnd, CM_SET_KEYBOARD_LAYOUT, std::stoi(germanKeyboardLayoutString), NULL); + Sleep(SLEEP_WAIT_TIME); + LoadKeyboardLayout(germanKeyboardLayoutString.c_str(), KLF_ACTIVATE); + + // turn off all modifier keys + TurnOffModifierKeys(hwnd); + + // set right control key to be pressed + PostMessage(hwnd, CM_SET_KEY_STATE, VK_LCONTROL, KEY_STATE_PRESSED); + PostMessage(hwnd, CM_SET_KEY_STATE, VK_CONTROL, KEY_STATE_PRESSED); + // set right alt to be pressed + PostMessage(hwnd, CM_SET_KEY_STATE, VK_RMENU, KEY_STATE_PRESSED); + PostMessage(hwnd, CM_SET_KEY_STATE, VK_MENU, KEY_STATE_PRESSED); + Sleep(SLEEP_WAIT_TIME); + + // flush input buffer in preparation of the key event + FlushConsoleInputBuffer(inputHandle); + VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events)); + VERIFY_ARE_EQUAL(events, 0u); + + // send the key event that will be turned into an '@' + UINT scanCode = MapVirtualKey('Q', MAPVK_VK_TO_VSC); + PostMessage(hwnd, WM_KEYDOWN, 'Q', KEY_MESSAGE_CONTEXT_CODE | SINGLE_KEY_REPEAT | (scanCode << 16)); + Sleep(SLEEP_WAIT_TIME); + + // reset the keymap + TurnOffModifierKeys(hwnd); + + // create expected input record + INPUT_RECORD expectedRecord; + expectedRecord.EventType = KEY_EVENT; + expectedRecord.Event.KeyEvent.uChar.UnicodeChar = L'@'; + expectedRecord.Event.KeyEvent.bKeyDown = true; + expectedRecord.Event.KeyEvent.dwControlKeyState = RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED; + expectedRecord.Event.KeyEvent.wRepeatCount = SINGLE_KEY_REPEAT; + expectedRecord.Event.KeyEvent.wVirtualKeyCode = L'Q'; + expectedRecord.Event.KeyEvent.wVirtualScanCode = (WORD)scanCode; + + // read input records and compare + const int maxRecordLookup = 20; // some arbitrary value to grab some records + Log::Comment(L"Looking for input record matching:"); + Log::Comment(VerifyOutputTraits::ToString(expectedRecord)); + INPUT_RECORD records[20]; + VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(inputHandle, records, maxRecordLookup, &events)); + VERIFY_IS_GREATER_THAN(events, 0u); + bool successBool = false; + // look for the expected record somewhere in the returned records + for (unsigned int i = 0; i < events; ++i) + { + Log::Comment(VerifyOutputTraits::ToString(records[i])); + if (VerifyCompareTraits::AreEqual(records[i], expectedRecord)) + { + successBool = true; + break; + } + } + VERIFY_IS_TRUE(successBool); + + // reset the keyboard layout + WPARAM originalLocale; + std::wstringstream localeStringStream(originalLocaleId); + localeStringStream >> std::hex >> originalLocale; + PostMessage(hwnd, CM_SET_KEYBOARD_LAYOUT, originalLocale, NULL); + LoadKeyboardLayout(originalLocaleId, KLF_ACTIVATE | KLF_SUBSTITUTE_OK); +} diff --git a/src/host/ft_host/OneCoreDelay.cpp b/src/host/ft_host/OneCoreDelay.cpp new file mode 100644 index 000000000..f01bbc9aa --- /dev/null +++ b/src/host/ft_host/OneCoreDelay.cpp @@ -0,0 +1,364 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "OneCoreDelay.hpp" + +BOOLEAN +__stdcall +OneCoreDelay::IsIsWindowPresent() +{ +#ifdef __INSIDE_WINDOWS + return ::IsIsWindowPresent(); +#else + return true; +#endif +} + +BOOLEAN +__stdcall +OneCoreDelay::IsGetSystemMetricsPresent() +{ +#ifdef __INSIDE_WINDOWS + return ::IsGetSystemMetricsPresent(); +#else + return true; +#endif +} + +BOOLEAN +__stdcall +OneCoreDelay::IsPostMessageWPresent() +{ +#ifdef __INSIDE_WINDOWS + return ::IsPostMessageWPresent(); +#else + return true; +#endif +} + +BOOLEAN +__stdcall +OneCoreDelay::IsSendMessageWPresent() +{ +#ifdef __INSIDE_WINDOWS + return ::IsSendMessageWPresent(); +#else + return true; +#endif +} + +HMODULE GetUser32() +{ + static HMODULE _hUser32 = nullptr; + if (_hUser32 == nullptr) + { + _hUser32 = LoadLibraryExW(L"user32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + } + + return _hUser32; +} + +HMODULE GetKernel32() +{ + static HMODULE _hKernel32 = nullptr; + if (_hKernel32 == nullptr) + { + _hKernel32 = LoadLibraryExW(L"kernel32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + } + + return _hKernel32; +} + +BOOL +APIENTRY +OneCoreDelay::AddConsoleAliasA( + _In_ LPSTR Source, + _In_ LPSTR Target, + _In_ LPSTR ExeName) +{ + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnAddConsoleAliasA)(LPSTR Source, LPSTR Target, LPSTR ExeName); + + static PfnAddConsoleAliasA pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnAddConsoleAliasA)GetProcAddress(h, "AddConsoleAliasA"); + } + + if (pfn != nullptr) + { + return pfn(Source, Target, ExeName); + } + } + + return FALSE; +} + +BOOL +APIENTRY +OneCoreDelay::AddConsoleAliasW( + _In_ LPWSTR Source, + _In_ LPWSTR Target, + _In_ LPWSTR ExeName) +{ + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnAddConsoleAliasW)(LPWSTR Source, LPWSTR Target, LPWSTR ExeName); + + static PfnAddConsoleAliasW pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnAddConsoleAliasW)GetProcAddress(h, "AddConsoleAliasW"); + } + + if (pfn != nullptr) + { + return pfn(Source, Target, ExeName); + } + } + + return FALSE; +} + +DWORD +APIENTRY +OneCoreDelay::GetConsoleAliasA( + _In_ LPSTR Source, + _Out_writes_(TargetBufferLength) LPSTR TargetBuffer, + _In_ DWORD TargetBufferLength, + _In_ LPSTR ExeName) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnGetConsoleAliasA)(LPSTR Source, LPSTR TargetBuffer, DWORD TargetBufferLength, LPSTR ExeName); + + static PfnGetConsoleAliasA pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetConsoleAliasA)GetProcAddress(h, "GetConsoleAliasA"); + } + + if (pfn != nullptr) + { + return pfn(Source, TargetBuffer, TargetBufferLength, ExeName); + } + } + + return FALSE; + } + +DWORD +APIENTRY +OneCoreDelay::GetConsoleAliasW( + _In_ LPWSTR Source, + _Out_writes_(TargetBufferLength) LPWSTR TargetBuffer, + _In_ DWORD TargetBufferLength, + _In_ LPWSTR ExeName) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnGetConsoleAliasW)(LPWSTR Source, LPWSTR TargetBuffer,DWORD TargetBufferLength, LPWSTR ExeName); + + static PfnGetConsoleAliasW pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetConsoleAliasW)GetProcAddress(h, "GetConsoleAliasW"); + } + + if (pfn != nullptr) + { + return pfn(Source, TargetBuffer, TargetBufferLength, ExeName); + } + } + + return FALSE; + } + +BOOL +WINAPI +OneCoreDelay::GetCurrentConsoleFont( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _Out_ PCONSOLE_FONT_INFO lpConsoleCurrentFont + ) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnGetCurrentConsoleFont)(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFO lpConsoleCurrentFont); + + static PfnGetCurrentConsoleFont pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetCurrentConsoleFont)GetProcAddress(h, "GetCurrentConsoleFont"); + } + + if (pfn != nullptr) + { + return pfn(hConsoleOutput, bMaximumWindow, lpConsoleCurrentFont); + } + } + + return FALSE; + } + +BOOL +WINAPI +OneCoreDelay::GetCurrentConsoleFontEx( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _Out_ PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnGetCurrentConsoleFontEx)(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); + + static PfnGetCurrentConsoleFontEx pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetCurrentConsoleFontEx)GetProcAddress(h, "GetCurrentConsoleFontEx"); + } + + if (pfn != nullptr) + { + return pfn(hConsoleOutput, bMaximumWindow, lpConsoleCurrentFontEx); + } + } + + return FALSE; + } + +BOOL +WINAPI +OneCoreDelay::SetCurrentConsoleFontEx( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _In_ PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnSetCurrentConsoleFontEx)(HANDLE hConsoleOutput, BOOL bMaximumWindow, PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); + + static PfnSetCurrentConsoleFontEx pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnSetCurrentConsoleFontEx)GetProcAddress(h, "SetCurrentConsoleFontEx"); + } + + if (pfn != nullptr) + { + return pfn(hConsoleOutput, bMaximumWindow, lpConsoleCurrentFontEx); + } + } + + return FALSE; + } + +COORD +WINAPI +OneCoreDelay::GetConsoleFontSize( + _In_ HANDLE hConsoleOutput, + _In_ DWORD nFont + ) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef COORD(WINAPI *PfnGetConsoleFontSize)(HANDLE hConsoleOutput, DWORD nFont); + + static PfnGetConsoleFontSize pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetConsoleFontSize)GetProcAddress(h, "GetConsoleFontSize"); + } + + if (pfn != nullptr) + { + return pfn(hConsoleOutput, nFont); + } + } + + return {0}; + } + +BOOL +WINAPI +OneCoreDelay::GetNumberOfConsoleMouseButtons( + _Out_ LPDWORD lpNumberOfMouseButtons + ) + { + HMODULE h = GetKernel32(); + + if (h != nullptr) + { + typedef BOOL(WINAPI *PfnGetNumberOfConsoleMouseButtons)(LPDWORD lpNumberOfMouseButtons); + + static PfnGetNumberOfConsoleMouseButtons pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetNumberOfConsoleMouseButtons)GetProcAddress(h, "GetNumberOfConsoleMouseButtons"); + } + + if (pfn != nullptr) + { + return pfn(lpNumberOfMouseButtons); + } + } + + return FALSE; + } + +HMENU +WINAPI +OneCoreDelay::GetMenu( + _In_ HWND hWnd) + { + HMODULE h = GetUser32(); + + if (h != nullptr) + { + typedef HMENU(WINAPI *PfnGetMenu)(HWND hWnd); + + static PfnGetMenu pfn = nullptr; + + if (pfn == nullptr) + { + pfn = (PfnGetMenu)GetProcAddress(h, "GetMenu"); + } + + if (pfn != nullptr) + { + return pfn(hWnd); + } + } + + return nullptr; + } + + diff --git a/src/host/ft_host/OneCoreDelay.hpp b/src/host/ft_host/OneCoreDelay.hpp new file mode 100644 index 000000000..01c2faa29 --- /dev/null +++ b/src/host/ft_host/OneCoreDelay.hpp @@ -0,0 +1,110 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OneCoreDelay.hpp + +Abstract: +- This module contains API definitions that aren't available on OneCore. + It will help us to call them on a full Desktop machine and fail on OneCore. + +Author: +- Michael Niksa (MiNiksa) 2017 + +Revision History: +--*/ + +#pragma once + +#include "..\..\inc\consoletaeftemplates.hpp" + +namespace OneCoreDelay +{ + BOOLEAN + __stdcall + IsIsWindowPresent(); + + BOOLEAN + __stdcall + IsGetSystemMetricsPresent(); + + BOOLEAN + __stdcall + IsPostMessageWPresent(); + + BOOLEAN + __stdcall + IsSendMessageWPresent(); + +BOOL +APIENTRY +AddConsoleAliasA( + _In_ LPSTR Source, + _In_ LPSTR Target, + _In_ LPSTR ExeName); + +BOOL +APIENTRY +AddConsoleAliasW( + _In_ LPWSTR Source, + _In_ LPWSTR Target, + _In_ LPWSTR ExeName); + +DWORD +APIENTRY +GetConsoleAliasA( + _In_ LPSTR Source, + _Out_writes_(TargetBufferLength) LPSTR TargetBuffer, + _In_ DWORD TargetBufferLength, + _In_ LPSTR ExeName); + +DWORD +APIENTRY +GetConsoleAliasW( + _In_ LPWSTR Source, + _Out_writes_(TargetBufferLength) LPWSTR TargetBuffer, + _In_ DWORD TargetBufferLength, + _In_ LPWSTR ExeName); + +BOOL +WINAPI +GetCurrentConsoleFont( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _Out_ PCONSOLE_FONT_INFO lpConsoleCurrentFont + ); + +BOOL +WINAPI +GetCurrentConsoleFontEx( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _Out_ PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); + +BOOL +WINAPI +SetCurrentConsoleFontEx( + _In_ HANDLE hConsoleOutput, + _In_ BOOL bMaximumWindow, + _In_ PCONSOLE_FONT_INFOEX lpConsoleCurrentFontEx); + +COORD +WINAPI +GetConsoleFontSize( + _In_ HANDLE hConsoleOutput, + _In_ DWORD nFont + ); + +BOOL +WINAPI +GetNumberOfConsoleMouseButtons( + _Out_ LPDWORD lpNumberOfMouseButtons + ); + +HMENU +WINAPI +GetMenu( + _In_ HWND hWnd); + +}; diff --git a/src/host/ft_host/precomp.cpp b/src/host/ft_host/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/host/ft_host/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/host/ft_host/precomp.h b/src/host/ft_host/precomp.h new file mode 100644 index 000000000..26b14a1f7 --- /dev/null +++ b/src/host/ft_host/precomp.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "windows.h" +#include "wincon.h" +#include "windowsx.h" + +#include "WexTestClass.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include + +// Extension API set presence checks. +#ifdef __INSIDE_WINDOWS +#include +#include +#include +#endif + +#define CM_SET_KEY_STATE (WM_USER+18) +#define CM_SET_KEYBOARD_LAYOUT (WM_USER+19) + +#include "OneCoreDelay.hpp" + +// Include our common helpers +#include "common.hpp" diff --git a/src/host/ft_host/product.pbxproj b/src/host/ft_host/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/host/ft_host/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/host/ft_host/runtest.bat b/src/host/ft_host/runtest.bat new file mode 100644 index 000000000..8e315a1b3 --- /dev/null +++ b/src/host/ft_host/runtest.bat @@ -0,0 +1 @@ +%~dp0\..\..\..\bin\x64\Debug\OpenConsole.exe cmd.exe /K te.exe %~dp0\..\..\..\bin\x64\Debug\Conhost.API.Tests.dll %* diff --git a/src/host/ft_host/sources b/src/host/ft_host/sources new file mode 100644 index 000000000..7a8837b2a --- /dev/null +++ b/src/host/ft_host/sources @@ -0,0 +1,49 @@ +!include ..\sources.test.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.Host.FeatureTests +TARGETTYPE = DYNLINK +DLLDEF = + +# ------------------------------------- +# Special Test Config +# ------------------------------------- + +C_DEFINES=$(C_DEFINES) -D__INSIDE_WINDOWS + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + InitTests.cpp \ + CanaryTests.cpp \ + Common.cpp \ + OneCoreDelay.cpp \ + API_AliasTests.cpp \ + API_BufferTests.cpp \ + API_CursorTests.cpp \ + API_DimensionsTests.cpp \ + API_FileTests.cpp \ + API_FillOutputTests.cpp \ + API_FontTests.cpp \ + API_InputTests.cpp \ + API_ModeTests.cpp \ + API_OutputTests.cpp \ + API_RgbColorTests.cpp \ + API_TitleTests.cpp \ + API_PolicyTests.cpp \ + CJK_DbcsTests.cpp \ + Message_KeyPressTests.cpp \ + DefaultResource.rc # Autogenerated file name + version for Device Guard whitelisting effort + +# ------------------------------------- +# Localization +# ------------------------------------- + +# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe. +C_DEFINES=$(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)""" +MUI_VERIFY_NO_LOC_RESOURCE=1 diff --git a/src/host/ft_host/sources.dep b/src/host/ft_host/sources.dep new file mode 100644 index 000000000..21c0b9dbd --- /dev/null +++ b/src/host/ft_host/sources.dep @@ -0,0 +1,6 @@ +BUILD_PASS2_CONSUMES= \ + onecore\windows\core\console\open\src\tools\nihilist|PASS2 \ + +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ + diff --git a/src/host/ft_host/testmd.definition b/src/host/ft_host/testmd.definition new file mode 100644 index 000000000..f9ca6691f --- /dev/null +++ b/src/host/ft_host/testmd.definition @@ -0,0 +1,44 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "Host-FeatureTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "/ScreenCaptureOnError" + }, + "Dependencies": { + "Files": [ + { + "SourcePath": "$(PROJECT_ROOT)\\core\\console\\open\\src\\ConsolePerf.wprp", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + }, + { + "SourcePath": "$(PROJECT_ROOT)\\core\\console\\open\\src\\ConsolePerf.regions.xml", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + } + ], + "RemoteFiles": [ ], + "Packages": [ "Microsoft.Console.Tools.Nihilist" ] + }, + "Logs": [ ], + "Plugins": [ ], + "Profiles": [ + { + "Name": "Performance", + "AdditionalPlugins": [ + { + "Type": "Microsoft.TestInfrastructure.UniversalTest.TestMD.Plugins.Winperf.WinperfPlugin", + "Parameters": { + "MachineConfigurationParameter": "none", + "LocalReproUpload": "true" + } + } + ], + "Execution": { + "AdditionalParameter": "/select:\"@IsPerfTest=true\"" + } + } + ] +} \ No newline at end of file diff --git a/src/host/ft_integrity/IntegrityTest.cpp b/src/host/ft_integrity/IntegrityTest.cpp new file mode 100644 index 000000000..3be3783c3 --- /dev/null +++ b/src/host/ft_integrity/IntegrityTest.cpp @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.hpp" +#include "IntegrityTest.hpp" + +// WinRT namespaces + +using namespace Platform; +using namespace Microsoft::OneCoreUap::Test::AppModel; + +static PCWSTR c_pwszLowIntegrity = L"Low Integrity"; +static PCWSTR c_pwszMedIntegrity = L"Medium Integrity"; +static PCWSTR c_pwszHighIntegrity = L"High Integrity"; +static PCWSTR c_pwszSysIntegrity = L"System Integrity"; +static PCWSTR c_pwszUnkIntegrity = L"UNKNOWN INTEGIRTY"; + +static void s_ExpandAnyEnvStrings(_Inout_ std::unique_ptr& cmdline) +{ + const DWORD cchNeeded = ExpandEnvironmentStringsW(cmdline.get(), nullptr, 0); + THROW_LAST_ERROR_IF(0 == cchNeeded); + + std::unique_ptr cmdlineExpanded = std::make_unique(cchNeeded); + + THROW_LAST_ERROR_IF(0 == ExpandEnvironmentStringsW(cmdline.get(), cmdlineExpanded.get(), cchNeeded)); + + cmdline.swap(cmdlineExpanded); +} + +static void s_RunViaCreateProcess(_In_ PCWSTR pwszExePath) +{ + STARTUPINFOW si = { 0 }; + si.cb = sizeof(si); + si.wShowWindow = SW_SHOWNORMAL; + + wil::unique_process_information pi; + + // We will need a mutable string to give to CreateProcessW. + const size_t cchNeeded = wcslen(pwszExePath) + 1; + std::unique_ptr cmdlineMutable = std::make_unique(cchNeeded); + THROW_IF_FAILED(StringCchCopyW(cmdlineMutable.get(), cchNeeded, pwszExePath)); + + // Replace the environment vars with their actual paths + s_ExpandAnyEnvStrings(cmdlineMutable); + + LOG_OUTPUT(L"Launching '%s'", cmdlineMutable.get()); + + THROW_IF_WIN32_BOOL_FALSE(CreateProcessW(nullptr, + cmdlineMutable.get(), + nullptr, + nullptr, + TRUE, + 0, + nullptr, + nullptr, + &si, + &pi)); + + WaitForSingleObject(pi.hProcess, INFINITE); +} + +static void s_SetConIntegrityLow() +{ + // This is absolute paths because OneCoreUAPTest wouldn't accept relative paths here. + // We're trying to call this: + // C:\\windows\\system32\\icacls.exe C:\\data\\test\\bin\\conintegrity.exe /setintegritylevel low + + // First assemble with WinRT strings including the Test Deployment Directory C:\data\test\bin which can vary. + auto commandLine = ref new String(L"%WINDIR%\\system32\\icacls.exe ") + + TAEFHelper::GetTestDeploymentDirectory() + ref new String(L"conintegrity.exe /setintegritylevel low"); + + // Now call our helper to munge the environment strings then run it and wait for exit. + s_RunViaCreateProcess(commandLine->Data()); +} + +bool ModuleSetup() +{ + IntegrityTest::s_LogMyIntegrityLevel(L"ModSetup"); + + THROW_IF_FAILED(::WinRTHelper_Register()); + THROW_IF_FAILED(Windows::Foundation::Initialize(WINRT_INIT_MULTITHREADED)); + + TestHelper::Initialize(); + + // Set ConIntegrity.exe to low integrity with ICACLS. + // We have to do this from SYSTEM context. + s_SetConIntegrityLow(); + + return true; +} + +bool ModuleCleanup() +{ + IntegrityTest::s_LogMyIntegrityLevel(L"ModCleanup"); + + TestHelper::Uninitialize(); + Windows::Foundation::Uninitialize(); + + return true; +} + +bool IntegrityTest::ClassSetup() +{ + IntegrityTest::s_LogMyIntegrityLevel(L"ClassSetup"); + + THROW_IF_FAILED(Windows::Foundation::Initialize(WINRT_INIT_MULTITHREADED)); + + // Get Appx location + auto testDeploymentDir = TAEFHelper::GetTestDeploymentDirectory(); + LOG_OUTPUT(L"Test Deployment Dir: \"%s\"", testDeploymentDir->Data()); + + // Deploy App + +#if defined(_X86_) + auto vclibPackage = DeploymentHelper::AddPackageIfNotPresent(testDeploymentDir + ref new String(L"Microsoft.VCLibs.x86.14.00.appx")); +#elif defined (_AMD64_) + auto vclibPackage = DeploymentHelper::AddPackageIfNotPresent(testDeploymentDir + ref new String(L"Microsoft.VCLibs.x64.14.00.appx")); +#elif defined (_ARM_) + auto vclibPackage = DeploymentHelper::AddPackageIfNotPresent(testDeploymentDir + ref new String(L"Microsoft.VCLibs.arm.14.00.appx")); +#elif defined (_ARM64_) + auto vclibPackage = DeploymentHelper::AddPackageIfNotPresent(testDeploymentDir + ref new String(L"Microsoft.VCLibs.arm64.14.00.appx")); +#else +#error Unknown architecture for test. +#endif + + auto appPackage = DeploymentHelper::AddPackage(testDeploymentDir + ref new String(L"ConsoleIntegrityUWP.appx")); + VERIFY_ARE_EQUAL(appPackage->Size, 1u); + + // Get App's AUMID + auto appAumids = appPackage->GetAt(0)->AUMIDs; + VERIFY_IS_NOT_NULL(appAumids); + VERIFY_ARE_EQUAL(appAumids->Size, 1u); + + // save off aumid + this->_appAumid = appAumids->GetAt(0); + + return true; +} + +bool IntegrityTest::ClassCleanup() +{ + s_LogMyIntegrityLevel(L"ClassCleanup"); + + Windows::Foundation::Uninitialize(); + return true; +} + +void IntegrityTest::_RunWin32ConIntegrityLowHelper() +{ + s_RunViaCreateProcess(L"conintegrity.exe"); +} + +void IntegrityTest::_RunUWPConIntegrityAppHelper() +{ + // We need to start the execution alias from the current user's location. + // We can't assume it will find it in the PATH. + std::wstring cmdline(L"%localappdata%\\microsoft\\windowsapps\\conintegrityuwp.exe"); + + s_RunViaCreateProcess(cmdline.c_str()); +} + +void IntegrityTest::_RunUWPConIntegrityViaTile() +{ + LOG_OUTPUT(L"Launching %s", _appAumid->Data()); + + auto viewDescriptor = NavigationHelper::LaunchApplication(_appAumid); + + LOG_OUTPUT(L" AUMID: \"%s\"", viewDescriptor->AUMID->Data()); + LOG_OUTPUT(L" Args: \"%s\"", viewDescriptor->Args->Data()); + LOG_OUTPUT(L" Tile Id: \"%s\"", viewDescriptor->TileId->Data()); + LOG_OUTPUT(L" View Id: %u", viewDescriptor->ViewId); + LOG_OUTPUT(L" Process Id: %u, 0x%x", viewDescriptor->ProcessId, viewDescriptor->ProcessId); + LOG_OUTPUT(L" Host Id: 0x%016llx", viewDescriptor->HostId); + LOG_OUTPUT(L" PSM Key: \"%s\"", viewDescriptor->PSMKey->Data()); + + // There's not really a wait for exit here, so just sleep. + ::Sleep(5000); + + // Terminate + LOG_OUTPUT(L"Terminating"); + NavigationHelper::CloseView(viewDescriptor->ViewId); +} + +// These are shorthands for the function calls, their True/False return code, and then the GetLastError +// They are serialized into an extremely short string to deal with potentially small console buffers +// on OneCore-derived Windows SKUs. +// Example: RCOW;1;0 = ReadConsoleOutputW returning TRUE and a GetLastError() of 0. +// Please see conintegrity.exe and conintegrityuwp.exe for how they are formed. +static PCWSTR _rgpwszExpectedSuccess[] = +{ + L"RCOW;1;0", + L"RCOA;1;0", + L"RCOCW;1;0", + L"RCOCA;1;0", + L"RCOAttr;1;0", + L"WCIA;1;0", + L"WCIW;1;0" +}; + +static PCWSTR _rgpwszExpectedFail[] = +{ + L"RCOW;0;5", + L"RCOA;0;5", + L"RCOCW;0;5", + L"RCOCA;0;5", + L"RCOAttr;0;5", + L"WCIA;0;5", + L"WCIW;0;5" +}; + +void IntegrityTest::_TestValidationHelper(const bool fIsBlockExpected, + _In_ PCWSTR pwszIntegrityExpected) +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(csbiex); + GetConsoleScreenBufferInfoEx(GetStdHandle(STD_OUTPUT_HANDLE), + &csbiex); + + LOG_OUTPUT(L"Buffer Size X:%d Y:%d", csbiex.dwSize.X, csbiex.dwSize.Y); + + size_t cch = csbiex.dwSize.X; + wistd::unique_ptr stringData = wil::make_unique_nothrow(cch); + THROW_IF_NULL_ALLOC(stringData); + + COORD coordRead = { 0 }; + for (coordRead.Y = 0; coordRead.Y < 8; coordRead.Y++) + { + ZeroMemory(stringData.get(), sizeof(wchar_t) * cch); + + DWORD dwRead = 0; + + ReadConsoleOutputCharacterW(GetStdHandle(STD_OUTPUT_HANDLE), + stringData.get(), + (DWORD)cch, + coordRead, + &dwRead); + + WEX::Common::String strExpected; + WEX::Common::String strActual; + + // At position 0, check the integrity. + if (coordRead.Y == 0) + { + strExpected = pwszIntegrityExpected; + } + else + { + // For the rest, check whether the API call worked. + if (fIsBlockExpected) + { + strExpected = _rgpwszExpectedFail[coordRead.Y - 1]; + } + else + { + strExpected = _rgpwszExpectedSuccess[coordRead.Y - 1]; + } + } + stringData[strExpected.GetLength()] = L'\0'; + strActual = stringData.get(); + VERIFY_ARE_EQUAL(strExpected, strActual); + + LOG_OUTPUT(stringData.get()); + } +} + +void IntegrityTest::TestLaunchLowILFromHigh() +{ + s_LogMyIntegrityLevel(L"TestBody"); + _RunWin32ConIntegrityLowHelper(); + + PCWSTR pwszIntegrityExpected = s_GetMyIntegrityLevel(); + bool fIsBlockExpected = false; + + // If I'm High, expect low. + // Otherwise if I'm System, expect system. + if (0 == wcscmp(pwszIntegrityExpected, c_pwszHighIntegrity)) + { + pwszIntegrityExpected = c_pwszLowIntegrity; + fIsBlockExpected = true; + } + + _TestValidationHelper(fIsBlockExpected, pwszIntegrityExpected); +} + +void IntegrityTest::TestLaunchLowILFromMedium() +{ + s_LogMyIntegrityLevel(L"TestBody"); + _RunWin32ConIntegrityLowHelper(); + _TestValidationHelper(true, c_pwszLowIntegrity); +} + +void IntegrityTest::TestLaunchAppFromHigh() +{ + s_LogMyIntegrityLevel(L"TestBody"); + _RunUWPConIntegrityAppHelper(); + _TestValidationHelper(true, c_pwszLowIntegrity); +} + +void IntegrityTest::TestLaunchAppFromMedium() +{ + s_LogMyIntegrityLevel(L"TestBody"); + _RunUWPConIntegrityAppHelper(); + _TestValidationHelper(true, c_pwszLowIntegrity); +} + +void IntegrityTest::TestLaunchAppAlone() +{ + s_LogMyIntegrityLevel(L"TestBody"); + _RunUWPConIntegrityViaTile(); +} + +PCWSTR IntegrityTest::s_GetMyIntegrityLevel() +{ + DWORD dwIntegrityLevel = 0; + + // Get the Integrity level. + wistd::unique_ptr tokenLabel; + THROW_IF_FAILED(wil::GetTokenInformationNoThrow(tokenLabel, GetCurrentProcessToken())); + + dwIntegrityLevel = *GetSidSubAuthority(tokenLabel->Label.Sid, + (DWORD)(UCHAR)(*GetSidSubAuthorityCount(tokenLabel->Label.Sid) - 1)); + + switch (dwIntegrityLevel) + { + case SECURITY_MANDATORY_LOW_RID: + return c_pwszLowIntegrity; + case SECURITY_MANDATORY_MEDIUM_RID: + return c_pwszMedIntegrity; + case SECURITY_MANDATORY_HIGH_RID: + return c_pwszHighIntegrity; + case SECURITY_MANDATORY_SYSTEM_RID: + return c_pwszSysIntegrity; + default: + return c_pwszUnkIntegrity; + } +} + +void IntegrityTest::s_LogMyIntegrityLevel(PCWSTR WhoAmI) +{ + LOG_OUTPUT(L"%s: %s", WhoAmI, s_GetMyIntegrityLevel()); +} diff --git a/src/host/ft_integrity/IntegrityTest.hpp b/src/host/ft_integrity/IntegrityTest.hpp new file mode 100644 index 000000000..99b707407 --- /dev/null +++ b/src/host/ft_integrity/IntegrityTest.hpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +BEGIN_MODULE() + // We need setup fixtures to run as system to ensure we have authority. + MODULE_PROPERTY(L"RunFixtureAs:Module", L"System") +END_MODULE() + +MODULE_SETUP(ModuleSetup); +MODULE_CLEANUP(ModuleCleanup); + +class IntegrityTest +{ +public: + BEGIN_TEST_CLASS(IntegrityTest) + // We need each method to start it in its own console. + TEST_CLASS_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_CLASS() + + TEST_CLASS_SETUP(ClassSetup) + TEST_CLASS_CLEANUP(ClassCleanup) + + BEGIN_TEST_METHOD(TestLaunchLowILFromHigh) + TEST_METHOD_PROPERTY(L"RunAs", L"ElevatedUserOrSystem") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestLaunchLowILFromMedium) + TEST_METHOD_PROPERTY(L"RunAs", L"RestrictedUser") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestLaunchAppFromHigh) + TEST_METHOD_PROPERTY(L"RunAs", L"ElevatedUserOrSystem") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestLaunchAppFromMedium) + TEST_METHOD_PROPERTY(L"RunAs", L"RestrictedUser") + END_TEST_METHOD() + + BEGIN_TEST_METHOD(TestLaunchAppAlone) + TEST_METHOD_PROPERTY(L"RunAs", L"RestrictedUser") + END_TEST_METHOD() + + static PCWSTR s_GetMyIntegrityLevel(); + static void s_LogMyIntegrityLevel(PCWSTR WhoAmI); + +private: + Platform::String^ _appAumid; + + void _RunWin32ConIntegrityLowHelper(); + void _RunUWPConIntegrityAppHelper(); + + void _RunUWPConIntegrityViaTile(); + + void _TestValidationHelper(const bool fIsBlockExpected, + _In_ PCWSTR pwszIntegrityExpected); +}; diff --git a/src/host/ft_integrity/precomp.hpp b/src/host/ft_integrity/precomp.hpp new file mode 100644 index 000000000..10dd2d42d --- /dev/null +++ b/src/host/ft_integrity/precomp.hpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// Windows +#include +#include +#include +#include +#include + +// WRL +#include + +// WEX +#include +#define LOG_OUTPUT(fmt, ...) WEX::Logging::Log::Comment(WEX::Common::String().Format(fmt, __VA_ARGS__)) + +// wil +#include +#include + +// STL +#include +#include +#include +#include + +// AppModel TestHelper +#include diff --git a/src/host/ft_integrity/product.pbxproj b/src/host/ft_integrity/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/host/ft_integrity/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/host/ft_integrity/sources b/src/host/ft_integrity/sources new file mode 100644 index 000000000..a7eddd8be --- /dev/null +++ b/src/host/ft_integrity/sources @@ -0,0 +1,71 @@ +!if 0 +Copyright (c) Microsoft Corporation. All rights reserved. +!endif + +TARGETNAME=Microsoft.Console.Host.IntegrityTests +TARGETTYPE=DYNLINK +TARGET_DESTINATION=UnitTests + +TEST_CODE=1 +UNIVERSAL_TEST = 1 + +DLLDEF= + +USE_NATIVE_EH=1 +USE_MSVCRT=1 +USE_UNICRT=1 +USE_CX=1 +CONSUME_WINRT=1 + +USE_STL=1 +STL_VER=STL_VER_CURRENT +USE_STLDLL=1 + +C_DEFINES=$(C_DEFINES) -DUNICODE -D_UNICODE +C_DEFINES=$(C_DEFINES)\ + /AI$(ONECOREBASE_INTERNAL_REF_PATH_L)\AppModel\Test\Common + +PRECOMPILED_INCLUDE=precomp.hpp +PRECOMPILED_CXX=1 + +INCLUDES=\ + $(INCLUDES); \ + $(COM_INC_PATH); \ + $(ONECOREBASE_INTERNAL_INC_PATH_L)\appmodel\test\common; \ + $(ONECOREREDIST_INTERNAL_INC_PATH_L)\TAEF; \ + $(ONECORE_PRIV_SDK_INC_PATH); \ + $(MINCORE_INTERNAL_PRIV_SDK_INC_PATH_L); \ + +TARGETLIBS=\ + $(TARGETLIBS) \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecoreuap_internal.lib \ + $(ONECOREREDIST_INTERNAL_LIB_PATH_L)\TAEF\Wex.Common.lib \ + $(ONECOREREDIST_INTERNAL_LIB_PATH_L)\TAEF\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ + $(ONECOREBASE_INTERNAL_LIB_PATH_L)\appmodel\test\common\microsoft.onecoreuap.test.appmodel.lib \ + +SOURCES=\ + IntegrityTest.cpp + +VARIABLE_OVERRIDES=$(VARIABLE_OVERRIDES);OBJ_PATH=$(OBJ_PATH) + +!if 0 +This is used to control the arch loaded into the testmd.definition +!endif +!if "$(_BUILDARCH)" == "amd64" +UNIVERSAL_TEST_MACROS=CoffeeArch=x64 +!else +UNIVERSAL_TEST_MACROS=CoffeeArch=$(_BUILDARCH) +!endif + +!if 0 +This is used to adjust the source of the VCLibs package +!endif +!if "$(_BUILDARCH)" == "x86" +UNIVERSAL_TEST_MACROS=$(UNIVERSAL_TEST_MACROS);CoffeeDir=i386 +!else +UNIVERSAL_TEST_MACROS=$(UNIVERSAL_TEST_MACROS);CoffeeDir=$(_BUILDARCH) +!endif + + + diff --git a/src/host/ft_integrity/sources.dep b/src/host/ft_integrity/sources.dep new file mode 100644 index 000000000..5b945b247 --- /dev/null +++ b/src/host/ft_integrity/sources.dep @@ -0,0 +1,9 @@ +PUBLIC_PASS1_CONSUMES= \ + onecore\base\appmodel\test\common\testhelper\winrt\private|PASS1 \ + +BUILD_PASS2_CONSUMES= \ + onecore\base\appmodel\test\common\testhelper\samples\nativecxapp\appx|PASS2 \ + + +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/host/ft_integrity/testmd.definition b/src/host/ft_integrity/testmd.definition new file mode 100644 index 000000000..402f090f5 --- /dev/null +++ b/src/host/ft_integrity/testmd.definition @@ -0,0 +1,27 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "Host-IntegrityTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ + { + "SourcePath": "$(OSDEPENDSROOT)\\Microsoft.VCLibs\\appx\\$(CoffeeDir)\\Microsoft.VCLibs.$(CoffeeArch).14.00.appx", + "DestinationFolderPath": "$$(TEST_DEPLOY_BIN)" + } + ], + "RemoteFiles": [ ], + "Packages": [ + "Microsoft.AppModel.TestAreaLibrary.FunctionalTests.Native", + "Microsoft.Console.Tools.ConIntegrity", + "Microsoft.Console.Tools.ConIntegrityUWP" + ] + }, + "Logs": [ ], + "Plugins": [ ] +} \ No newline at end of file diff --git a/src/host/ft_uia/AccessibilityTests.cs b/src/host/ft_uia/AccessibilityTests.cs new file mode 100644 index 000000000..344901e8a --- /dev/null +++ b/src/host/ft_uia/AccessibilityTests.cs @@ -0,0 +1,614 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the certain key presses. +//---------------------------------------------------------------------------------------------------------------------- +using System; +using System.Windows; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Automation; +using System.Windows.Automation.Text; + +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using WEX.Logging.Interop; + +using Conhost.UIA.Tests.Common; +using Conhost.UIA.Tests.Common.NativeMethods; +using Conhost.UIA.Tests.Elements; +using OpenQA.Selenium; +using System.Drawing; + +using System.Runtime.InteropServices; + +namespace Conhost.UIA.Tests +{ + + class InvalidElementException : Exception + { + + } + + [TestClass] + class AccessibilityTests + { + public TestContext TestContext { get; set; } + + private AutomationElement GetWindowUiaElement(CmdApp app) + { + IntPtr handle = app.GetWindowHandle(); + AutomationElement windowUiaElement = AutomationElement.FromHandle(handle); + return windowUiaElement; + } + + private AutomationElement GetTextAreaUiaElement(CmdApp app) + { + AutomationElement windowUiaElement = GetWindowUiaElement(app); + AutomationElementCollection descendants = windowUiaElement.FindAll(TreeScope.Descendants, Condition.TrueCondition); + for (int i = 0; i < descendants.Count; ++i) + { + AutomationElement poss = descendants[i]; + if (poss.Current.AutomationId.Equals("Text Area")) + { + return poss; + } + } + throw new InvalidElementException(); + } + + private int _GetTotalRows(CmdApp app) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenBufferInfo = app.GetScreenBufferInfo(); + return screenBufferInfo.dwSize.Y; + } + + private void _ClearScreenBuffer(CmdApp app) + { + IntPtr outHandle = app.GetStdOutHandle(); + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenInfo = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + screenInfo.cbSize = (uint)Marshal.SizeOf(screenInfo); + WinCon.GetConsoleScreenBufferInfoEx(outHandle, ref screenInfo); + int charCount = screenInfo.dwSize.X * screenInfo.dwSize.Y; + string writeString = new string(' ', charCount); + WinCon.COORD coord = new WinCon.COORD(); + coord.X = 0; + coord.Y = 0; + UInt32 charsWritten = 0; + WinCon.WriteConsoleOutputCharacter(outHandle, + writeString, + (uint)charCount, + coord, + ref charsWritten); + Verify.AreEqual((UInt32)charCount, charsWritten); + } + + private void _WriteCharTestText(CmdApp app) + { + IntPtr outHandle = app.GetStdOutHandle(); + WinCon.COORD coord = new WinCon.COORD(); + coord.X = 0; + coord.Y = 0; + string row1 = "1234567890"; + string row2 = " abcdefghijk"; + UInt32 charsWritten = 0; + WinCon.WriteConsoleOutputCharacter(outHandle, + row1, + (uint)row1.Length, + coord, + ref charsWritten); + + coord.Y = 1; + WinCon.WriteConsoleOutputCharacter(outHandle, + row2, + (uint)row2.Length, + coord, + ref charsWritten); + + } + + private void _FillOutputBufferWithData(CmdApp app) + { + for (int i = 0; i < _GetTotalRows(app) * 2 / 3; ++i) + { + // each echo command uses up 3 lines in the buffer: + // 1. output text + // 2. newline + // 3. new prompt line + app.UIRoot.SendKeys("echo "); + app.UIRoot.SendKeys(i.ToString()); + app.UIRoot.SendKeys(Keys.Enter); + } + } + + [TestMethod] + public void CanAccessAccessibilityTree() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement windowUiaElement = GetWindowUiaElement(app); + Verify.IsTrue(windowUiaElement.Current.AutomationId.Equals("Console Window")); + } + } + + [TestMethod] + public void CanAccessTextAreaUiaElement() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + Verify.IsTrue(textAreaUiaElement != null); + } + } + + [TestMethod] + public void CanGetDocumentRangeText() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + // get the text from uia api + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange documentRange = textPattern.DocumentRange; + string allText = documentRange.GetText(-1); + // get text from console api + IntPtr hConsole = app.GetStdOutHandle(); + using (ViewportArea area = new ViewportArea(app)) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenInfo = app.GetScreenBufferInfo(); + Rectangle rect = new Rectangle(0, 0, screenInfo.dwSize.X, screenInfo.dwSize.Y); + IEnumerable viewportText = area.GetLinesInRectangle(hConsole, rect); + + // the uia api does not return spaces beyond the last + // non -whitespace character so we need to trim those from + // the viewportText. The uia api also inserts \r\n to indicate + // a new linen so we need to add those back in after trimming. + string consoleText = ""; + for (int i = 0; i < viewportText.Count(); ++i) + { + consoleText += viewportText.ElementAt(i).Trim() + "\r\n"; + } + consoleText = consoleText.Trim(); + allText = allText.Trim(); + // compare + Verify.IsTrue(consoleText.Equals(allText)); + } + } + } + + [TestMethod] + public void CanGetTextAtCharacterLevel() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + const int noMaxLength = -1; + const string row1 = "1234567890"; + const string row2 = " abcdefghijk"; + _ClearScreenBuffer(app); + _WriteCharTestText(app); + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] ranges = textPattern.GetVisibleRanges(); + TextPatternRange range = ranges[0].Clone(); + + // should be able to get each char in row1 + range.ExpandToEnclosingUnit(TextUnit.Character); + foreach (char ch in row1) + { + string text = range.GetText(noMaxLength); + Verify.AreEqual(ch.ToString(), text); + range.Move(TextUnit.Character, 1); + } + + // should be able to get each char in row2, including starting spaces + range = ranges[1].Clone(); + range.ExpandToEnclosingUnit(TextUnit.Character); + foreach (char ch in row2) + { + string text = range.GetText(noMaxLength); + Verify.AreEqual(ch.ToString(), text); + range.Move(TextUnit.Character, 1); + } + + // taking half of each row should return correct text with + // spaces if they appear before the last non-whitespace char + range = ranges[0].Clone(); + range.MoveEndpointByUnit(TextPatternRangeEndpoint.Start, TextUnit.Character, 8); + range.MoveEndpointByUnit(TextPatternRangeEndpoint.End, TextUnit.Character, 8); + Verify.AreEqual("90\r\n abcde", range.GetText(noMaxLength)); + } + } + + [TestMethod] + public void CanGetVisibleRange() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + // get the ranges from uia api + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] ranges = textPattern.GetVisibleRanges(); + + // get the ranges from the console api + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenInfo = app.GetScreenBufferInfo(); + int viewportHeight = screenInfo.srWindow.Bottom - screenInfo.srWindow.Top + 1; + + // we should have one range per line in the viewport + Verify.AreEqual(ranges.GetLength(0), viewportHeight); + + // each line should have the same text + ViewportArea viewport = new ViewportArea(app); + IntPtr hConsole = app.GetStdOutHandle(); + for (int i = 0; i < viewportHeight; ++i) + { + Rectangle rect = new Rectangle(0, i, screenInfo.dwSize.X, 1); + IEnumerable text = viewport.GetLinesInRectangle(hConsole, rect); + Verify.AreEqual(text.ElementAt(0).Trim(), ranges[i].GetText(-1).Trim()); + } + } + } + + /* + // TODO this is commented out because it will fail. It fails because the c# api of RangeFromPoint + // throws an exception when passed a point that is outside the bounds of the window, which is + // allowed in the c++ version and exactly what we want to test. Another way to test this case needs + // to be found that doesn't go through the c# api. + [TestMethod] + public void CanGetRangeFromPoint() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + // RangeFromPoint returns the range closest to the point provided + // so we have three cases along the y dimension: + // - point above the console window + // - point in the console window + // - point below the console window + + // get the window dimensions and pick the point locations + IntPtr handle = app.GetWindowHandle(); + User32.RECT windowRect; + User32.GetWindowRect(handle, out windowRect); + + List points = new List(); + int middleOfWindow = (windowRect.bottom + windowRect.top) / 2; + const int windowEdgeOffset = 10; + // x doesn't matter until we support more ranges than lines + points.Add(new System.Windows.Point(windowRect.left, windowRect.top - windowEdgeOffset)); + points.Add(new System.Windows.Point(windowRect.left, middleOfWindow)); + points.Add(new System.Windows.Point(windowRect.left, windowRect.bottom + windowEdgeOffset)); + + + // get the ranges from uia api + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + List textPatternRanges = new List(); + foreach (System.Windows.Point p in points) + { + textPatternRanges.Add(textPattern.RangeFromPoint(p)); + } + + // ranges should be in correct order (starting at top and + // going down screen) + Rect lastBoundingRect = textPatternRanges.ElementAt(0).GetBoundingRectangles()[0]; + foreach (TextPatternRange range in textPatternRanges) + { + Rect[] boundingRects = range.GetBoundingRectangles(); + // since the ranges returned by RangeFromPoint are supposed to be degenerate, + // there should be only one bounding rect per TextPatternRange + Verify.AreEqual(boundingRects.GetLength(0), 1); + + Verify.IsTrue(boundingRects[0].Top >= lastBoundingRect.Top); + lastBoundingRect = boundingRects[0]; + } + + + } + } + + } + */ + + [TestMethod] + public void CanCloneTextRangeProvider() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange textPatternRange = textPattern.DocumentRange; + // clone it + TextPatternRange copyRange = textPatternRange.Clone(); + Verify.IsTrue(copyRange.Compare(textPatternRange)); + // change the copy and make sure the compare fails + copyRange.MoveEndpointByRange(TextPatternRangeEndpoint.End, copyRange, TextPatternRangeEndpoint.Start); + Verify.IsFalse(copyRange.Compare(textPatternRange)); + } + } + + [TestMethod] + public void CanCompareTextRangeProviderEndpoints() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange textPatternRange = textPattern.DocumentRange; + // comparing an endpoint to itself should be the same + Verify.AreEqual(0, textPatternRange.CompareEndpoints(TextPatternRangeEndpoint.Start, + textPatternRange, + TextPatternRangeEndpoint.Start)); + // comparing an earlier endpoint to a later one should be negative + Verify.IsGreaterThan(0, textPatternRange.CompareEndpoints(TextPatternRangeEndpoint.Start, + textPatternRange, + TextPatternRangeEndpoint.End)); + // comparing a later endpoint to an earlier one should be positive + Verify.IsLessThan(0, textPatternRange.CompareEndpoints(TextPatternRangeEndpoint.End, + textPatternRange, + TextPatternRangeEndpoint.Start)); + } + } + + [TestMethod] + public void CanExpandToEnclosingUnitTextRangeProvider() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] visibleRanges = textPattern.GetVisibleRanges(); + TextPatternRange testRange = visibleRanges.First().Clone(); + + // change testRange to a degenerate range and then expand to a line + testRange.MoveEndpointByRange(TextPatternRangeEndpoint.End, testRange, TextPatternRangeEndpoint.Start); + Verify.AreEqual(0, testRange.CompareEndpoints(TextPatternRangeEndpoint.Start, + testRange, + TextPatternRangeEndpoint.End)); + testRange.ExpandToEnclosingUnit(TextUnit.Line); + Verify.IsTrue(testRange.Compare(visibleRanges[0])); + + // expand to document size + testRange.ExpandToEnclosingUnit(TextUnit.Document); + Verify.IsTrue(testRange.Compare(textPattern.DocumentRange)); + + // shrink back to a line + testRange.ExpandToEnclosingUnit(TextUnit.Line); + Verify.IsTrue(testRange.Compare(visibleRanges[0])); + + // make the text buffer start to cycle its buffer + _FillOutputBufferWithData(app); + + // expand to document range again + testRange.ExpandToEnclosingUnit(TextUnit.Document); + Verify.IsTrue(testRange.Compare(textPattern.DocumentRange)); + } + } + + [TestMethod] + public void CanMoveRange() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] visibleRanges = textPattern.GetVisibleRanges(); + TextPatternRange testRange = visibleRanges.First().Clone(); + + + // assumes range is at the top of the screen buffer + Action testMovement = delegate (TextPatternRange range) + { + // the range is at the top of the screen + // buffer, we shouldn't be able to move up. + int moveAmount = range.Move(TextUnit.Line, -1); + Verify.AreEqual(0, moveAmount); + + // move to the bottom of the screen + // - 1 because we're already on the 0th row + int rowsToMove = _GetTotalRows(app) - 1; + moveAmount = range.Move(TextUnit.Line, rowsToMove); + Verify.AreEqual(rowsToMove, moveAmount); + + // try to move one more row down, we should not be able to + moveAmount = range.Move(TextUnit.Line, 1); + Verify.AreEqual(0, moveAmount); + + // move the range up to the top again, one row at a time, + // making sure that we have only one line being encompassed + // by the range. We check this by counting the number of + // bounding rectangles that represent the range. + for (int i = 0; i < rowsToMove; ++i) + { + moveAmount = range.Move(TextUnit.Line, -1); + // we need to scroll into view or getting the boundary + // rectangles might return 0 + Verify.AreEqual(-1, moveAmount); + range.ScrollIntoView(true); + Rect[] boundingRects = range.GetBoundingRectangles(); + Verify.AreEqual(1, boundingRects.GetLength(0)); + } + + // and back down to the bottom, one row at a time + for (int i = 0; i < rowsToMove; ++i) + { + moveAmount = range.Move(TextUnit.Line, 1); + // we need to scroll into view or getting the boundary + // rectangles might return 0 + Verify.AreEqual(1, moveAmount); + range.ScrollIntoView(true); + Rect[] boundingRects = range.GetBoundingRectangles(); + Verify.AreEqual(1, boundingRects.GetLength(0)); + } + + }; + + testMovement(testRange); + + // test again with unaligned text buffer and screen buffer + _FillOutputBufferWithData(app); + Globals.WaitForTimeout(); + + visibleRanges = textPattern.GetVisibleRanges(); + testRange = visibleRanges.First().Clone(); + // move range back to the top + while (true) + { + int moveCount = testRange.Move(TextUnit.Line, -1); + if (moveCount == 0) + { + break; + } + } + + testMovement(testRange); + } + } + + [TestMethod] + public void CanMoveEndpointByUnitNearTopBoundary() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] visibleRanges = textPattern.GetVisibleRanges(); + TextPatternRange testRange = visibleRanges.First().Clone(); + + // assumes that range is a line range at the top of the screen buffer + Action testTopBoundary = delegate(TextPatternRange range) + { + // the first visible range is at the top of the screen + // buffer, we shouldn't be able to move the starting endpoint up + int moveAmount = range.MoveEndpointByUnit(TextPatternRangeEndpoint.Start, TextUnit.Line, -1); + Verify.AreEqual(0, moveAmount); + + // we should be able to move the ending endpoint back, creating a degenerate range + moveAmount = range.MoveEndpointByUnit(TextPatternRangeEndpoint.End, TextUnit.Line, -1); + Verify.AreEqual(-1, moveAmount); + + // the range should now be degenerate and the ending + // endpoint should not be able to be moved back again + string rangeText = range.GetText(-1); + Verify.AreEqual("", rangeText); + moveAmount = range.MoveEndpointByUnit(TextPatternRangeEndpoint.End, TextUnit.Line, -1); + Verify.AreEqual(-1, moveAmount); + }; + + testTopBoundary(testRange); + + // we want to test that the boundaries are still observed + // when the screen buffer index and text buffer index don't align. + // write a bunch of text to the screen to fill up the text + // buffer and make it start to reuse its buffer + _FillOutputBufferWithData(app); + Globals.WaitForTimeout(); + + // move all the way to the bottom + visibleRanges = textPattern.GetVisibleRanges(); + testRange = visibleRanges.Last().Clone(); + while (true) + { + int moved = testRange.Move(TextUnit.Line, 1); + if (moved == 0) + { + break; + } + } + // we're at the bottom of the screen buffer, so move back to the top + // so we can test + int rowsToMove = -1 * (_GetTotalRows(app) - 1); + int moveCount = testRange.Move(TextUnit.Line, rowsToMove); + Verify.AreEqual(rowsToMove, moveCount); + testRange.ScrollIntoView(true); + + testTopBoundary(testRange); + } + } + + [TestMethod] + public void CanMoveEndpointByUnitNearBottomBoundary() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] visibleRanges = textPattern.GetVisibleRanges(); + TextPatternRange testRange = visibleRanges.First().Clone(); + + // assumes that range is a line range at the bottom of the screen buffer + Action testBottomBoundary = delegate (TextPatternRange range) + { + // the range is at the bottom of the screen buffer, we + // shouldn't be able to move the endpoint endpoint down + int moveAmount = range.MoveEndpointByUnit(TextPatternRangeEndpoint.End, TextUnit.Line, 1); + Verify.AreEqual(0, moveAmount); + + // we shouldn't be able to move the starting endpoint down either + moveAmount = range.MoveEndpointByUnit(TextPatternRangeEndpoint.Start, TextUnit.Line, 1); + Verify.AreEqual(0, moveAmount); + }; + + // move the range to the bottom of the screen + int rowsToMove = _GetTotalRows(app) - 1; + int moveCount = testRange.Move(TextUnit.Line, rowsToMove); + Verify.AreEqual(rowsToMove, moveCount); + + testBottomBoundary(testRange); + + // we want to test that the boundaries are still observed + // when the screen buffer index and text buffer index don't align. + // write a bunch of text to the screen to fill up the text + // buffer and make it start to reuse its buffer + _FillOutputBufferWithData(app); + Globals.WaitForTimeout(); + + // move all the way to the top + visibleRanges = textPattern.GetVisibleRanges(); + testRange = visibleRanges.First().Clone(); + while (true) + { + int moved = testRange.Move(TextUnit.Line, -1); + if (moved == 0) + { + break; + } + } + + // we're at the top of the screen buffer, so move back to the bottom + // so we can test + rowsToMove = _GetTotalRows(app) - 1; + moveCount = testRange.Move(TextUnit.Line, rowsToMove); + Verify.AreEqual(rowsToMove, moveCount); + testRange.ScrollIntoView(true); + + testBottomBoundary(testRange); + } + } + + [TestMethod] + public void CanGetBoundingRectangles() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + AutomationElement textAreaUiaElement = GetTextAreaUiaElement(app); + TextPattern textPattern = textAreaUiaElement.GetCurrentPattern(TextPattern.Pattern) as TextPattern; + TextPatternRange[] visibleRanges = textPattern.GetVisibleRanges(); + // copy the first range + TextPatternRange firstRange = visibleRanges[0].Clone(); + // only one bounding rect should be returned for the one line + Rect[] boundingRects = firstRange.GetBoundingRectangles(); + Verify.AreEqual(1, boundingRects.GetLength(0)); + // expand to two lines, verify we get a bounding rect per line + firstRange.MoveEndpointByRange(TextPatternRangeEndpoint.End, visibleRanges[1], TextPatternRangeEndpoint.End); + boundingRects = firstRange.GetBoundingRectangles(); + Verify.AreEqual(2, boundingRects.GetLength(0)); + } + } + } +} diff --git a/src/host/ft_uia/CloseTests.cs b/src/host/ft_uia/CloseTests.cs new file mode 100644 index 000000000..fffd23918 --- /dev/null +++ b/src/host/ft_uia/CloseTests.cs @@ -0,0 +1,118 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for verifying the mechanism of closing processes attached to the console host window. +//---------------------------------------------------------------------------------------------------------------------- + +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.IO.Pipes; + using System.Threading; + + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using System.Threading.Tasks; + + [TestClass] + public class CloseTests + { + public static Queue messages = new Queue(); + + public TestContext TestContext { get; set; } + + private static string closeTestBinaryLocation; + private static readonly string testPipeName = "ConsoleUIACloseTests"; + private static readonly uint processCount = 4; + + private static readonly string attachPattern = "closetest: child {0}: attached to console"; + private static readonly string spacerStartsWith = "closetest: attached process list:"; + private static readonly string pausingPattern = "closetest: child {0}: CTRL_CLOSE_EVENT received, pausing..."; + private static readonly string exitingPattern = "closetest: child {0}: CTRL_CLOSE_EVENT received, exiting..."; + + [ClassInitialize] + public static void ClassSetup(TestContext context) + { + Log.Comment("Searching for CloseTest.exe in the same directory where this test was launched from..."); + closeTestBinaryLocation = Path.Combine(context.TestDeploymentDir, "CloseTest.exe"); + Verify.IsFalse(string.IsNullOrEmpty(closeTestBinaryLocation)); + Verify.IsTrue(File.Exists(closeTestBinaryLocation)); + } + + public void ListenToPipe(NamedPipeServerStream stream, CancellationToken token) + { + StreamReader reader = new StreamReader(stream); + while (!reader.EndOfStream && !token.IsCancellationRequested) + { + string line = reader.ReadLine(); + if (!string.IsNullOrWhiteSpace(line)) + { + messages.Enqueue(line); + } + } + } + + public void MakePipeServer(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + NamedPipeServerStream str = new NamedPipeServerStream(testPipeName, PipeDirection.In, 250); + str.WaitForConnection(); + + Task.Run(() => ListenToPipe(str, token), token); + } + } + + [TestMethod] + [TestProperty("IsolationLevel", "Method")] + public void CheckClose() + { + string closeTestCmdLine = $"{closeTestBinaryLocation} -n {processCount} --log {testPipeName} --delay 1000 --no-realloc"; + + using (var tokenSource = new CancellationTokenSource()) + { + var token = tokenSource.Token; + + Task.Run(() => MakePipeServer(token), token); + + Log.Comment("Connect a test console window to the close test binary and wait for a few seconds."); + CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext, closeTestCmdLine); + Thread.Sleep(TimeSpan.FromSeconds(2)); + NativeMethods.Win32BoolHelper(WinCon.FreeConsole(), "Free console bindings so we aren't shut down when we kill the window."); + Log.Comment("Click the close button on the window then wait a few seconds for it to cleanup."); + app.GetCloseButton().Click(); + + Thread.Sleep(TimeSpan.FromSeconds(5)); + tokenSource.Cancel(); + + Log.Comment("Compare the output we received on our pipes to what we expected to get in terms of ordering and process count."); + + for (uint i = 1; i <= processCount; i++) + { + string expected = string.Format(attachPattern, i); + Verify.AreEqual(expected, messages.Dequeue()); + } + + Verify.IsTrue(messages.Dequeue().StartsWith(spacerStartsWith)); + + for (uint i = processCount; i >= 1; i--) + { + string expected; + expected = string.Format(pausingPattern, i); + Verify.AreEqual(expected, messages.Dequeue()); + expected = string.Format(exitingPattern, i); + Verify.AreEqual(expected, messages.Dequeue()); + } + } + } + } +} diff --git a/src/host/ft_uia/Common/AutoHelpers.cs b/src/host/ft_uia/Common/AutoHelpers.cs new file mode 100644 index 000000000..42b424bd4 --- /dev/null +++ b/src/host/ft_uia/Common/AutoHelpers.cs @@ -0,0 +1,31 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Console UI Automation test helpers +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + using System.Globalization; + + using WEX.Logging.Interop; + + public static class AutoHelpers + { + public static string FormatInvariant(string format, params object[] args) + { + return string.Format(CultureInfo.InvariantCulture, format, args); + } + + public static void LogInvariant(string format, params object[] args) + { + Log.Comment(CultureInfo.InvariantCulture, format, args); + } + + public static bool DwordToBool(this int value) + { + return value != 0; + } + } +} diff --git a/src/host/ft_uia/Common/CheckBoxMeta.cs b/src/host/ft_uia/Common/CheckBoxMeta.cs new file mode 100644 index 000000000..3f7a2a69a --- /dev/null +++ b/src/host/ft_uia/Common/CheckBoxMeta.cs @@ -0,0 +1,56 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// CheckBox metadata information helper class. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using OpenQA.Selenium.Appium; + using System; + using System.Globalization; + + using WEX.Logging.Interop; + + public struct CheckBoxMeta + { + public AppiumWebElement Box { get; private set; } + public string ValueName { get; private set; } + public bool IsInverse { get; private set; } + public bool IsGlobalOnly { get; private set; } + public bool IsV2Property { get; private set; } + public NativeMethods.Wtypes.PROPERTYKEY? PropKey { get; private set; } + + public CheckBoxMeta(AppiumWebElement window, string englishText, string valueName, bool isInverse, bool isGlobalOnly, bool isV2Property, NativeMethods.Wtypes.PROPERTYKEY? propKey) + : this() + { + this.Box = window.FindElementByName(englishText); + this.ValueName = valueName; + this.IsInverse = isInverse; + this.IsGlobalOnly = isGlobalOnly; + this.IsV2Property = isV2Property; + this.PropKey = propKey; + } + + public void Check() + { + if (!IsChecked()) + { + Box.Click(); + } + } + + public void Uncheck() + { + if (IsChecked()) + { + Box.Click(); + } + } + + public bool IsChecked() + { + return Box.Selected; + } + } +} diff --git a/src/host/ft_uia/Common/Globals.cs b/src/host/ft_uia/Common/Globals.cs new file mode 100644 index 000000000..b957c5264 --- /dev/null +++ b/src/host/ft_uia/Common/Globals.cs @@ -0,0 +1,37 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Console UI Automation global settings +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + + public static class Globals + { + public const int Timeout = 500; // in milliseconds + public const int AppCreateTimeout = 3000; // in milliseconds + + // These were pulled via UISpy from system defined window classes. + public const string PopupMenuClassId = "#32768"; + public const string DialogWindowClassId = "#32770"; + + public static void WaitForTimeout() + { + System.Threading.Thread.Sleep(Globals.Timeout); + } + } + + public enum OpenTarget + { + Defaults, + Specifics + } + + public enum CreateType + { + ProcessOnly, + ShortcutFile + } +} diff --git a/src/host/ft_uia/Common/NativeMethods.cs b/src/host/ft_uia/Common/NativeMethods.cs new file mode 100644 index 000000000..522feb275 --- /dev/null +++ b/src/host/ft_uia/Common/NativeMethods.cs @@ -0,0 +1,861 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper class for storing P/Invoke and COM Interop definitions. +//---------------------------------------------------------------------------------------------------------------------- + +namespace Conhost.UIA.Tests.Common.NativeMethods +{ + using System; + using System.Drawing; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + using System.Text; + + using Microsoft.Win32; + using WEX.TestExecution; + using WEX.Logging.Interop; + + // Small extension method helpers to make C# feel closer to native. + public static class NativeExtensions + { + public static int LoWord(this int val) + { + return val & 0xffff; + } + + public static int HiWord(this int val) + { + return (val >> 16) & 0xffff; + } + } + + public static class NativeMethods + { + public static void Win32BoolHelper(bool result, string actionMessage) + { + if (!result) + { + string errorMsg = string.Format("Win32 error occurred: 0x{0:X}", Marshal.GetLastWin32Error()); + Log.Comment(errorMsg); + } + + Verify.IsTrue(result, actionMessage); + } + + public static void Win32NullHelper(IntPtr result, string actionMessage) + { + if (result == IntPtr.Zero) + { + string errorMsg = string.Format("Win32 error occurred: 0x{0:X}", Marshal.GetLastWin32Error()); + Log.Comment(errorMsg); + } + + Verify.IsNotNull(result, actionMessage); + } + } + + public static class WinCon + { + [Flags()] + public enum CONSOLE_SELECTION_INFO_FLAGS : uint + { + CONSOLE_NO_SELECTION = 0x0, + CONSOLE_SELECTION_IN_PROGRESS = 0x1, + CONSOLE_SELECTION_NOT_EMPTY = 0x2, + CONSOLE_MOUSE_SELECTION = 0x4, + CONSOLE_MOUSE_DOWN = 0x8 + } + + public enum CONSOLE_STD_HANDLE : int + { + STD_INPUT_HANDLE = -10, + STD_OUTPUT_HANDLE = -11, + STD_ERROR_HANDLE = -12 + } + + public enum CONSOLE_ATTRIBUTES : ushort + { + FOREGROUND_BLUE = 0x1, + FOREGROUND_GREEN = 0x2, + FOREGROUND_RED = 0x4, + FOREGROUND_INTENSITY = 0x8, + FOREGROUND_YELLOW = FOREGROUND_RED | FOREGROUND_GREEN, + FOREGROUND_CYAN = FOREGROUND_GREEN | FOREGROUND_BLUE, + FOREGROUND_MAGENTA = FOREGROUND_RED | FOREGROUND_BLUE, + FOREGROUND_COLORS = FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN, + FOREGROUND_ALL = FOREGROUND_COLORS | FOREGROUND_INTENSITY, + BACKGROUND_BLUE = 0x10, + BACKGROUND_GREEN = 0x20, + BACKGROUND_RED = 0x40, + BACKGROUND_INTENSITY = 0x80, + BACKGROUND_YELLOW = BACKGROUND_RED | BACKGROUND_GREEN, + BACKGROUND_CYAN = BACKGROUND_GREEN | BACKGROUND_BLUE, + BACKGROUND_MAGENTA = BACKGROUND_RED | BACKGROUND_BLUE, + BACKGROUND_COLORS = BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_GREEN, + BACKGROUND_ALL = BACKGROUND_COLORS | BACKGROUND_INTENSITY, + COMMON_LVB_LEADING_BYTE = 0x100, + COMMON_LVB_TRAILING_BYTE = 0x200, + COMMON_LVB_GRID_HORIZONTAL = 0x400, + COMMON_LVB_GRID_LVERTICAL = 0x800, + COMMON_LVB_GRID_RVERTICAL = 0x1000, + COMMON_LVB_REVERSE_VIDEO = 0x4000, + COMMON_LVB_UNDERSCORE = 0x8000 + } + + public enum CONSOLE_OUTPUT_MODES : uint + { + ENABLE_PROCESSED_OUTPUT = 0x1, + ENABLE_WRAP_AT_EOL_OUTPUT = 0x2, + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4 + } + + //CHAR_INFO struct, which was a union in the old days + // so we want to use LayoutKind.Explicit to mimic it as closely + // as we can + [StructLayout(LayoutKind.Explicit)] + public struct CHAR_INFO + { + [FieldOffset(0)] + internal char UnicodeChar; + [FieldOffset(0)] + internal char AsciiChar; + [FieldOffset(2)] //2 bytes seems to work properly + internal CONSOLE_ATTRIBUTES Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct COORD + { + public short X; + public short Y; + + public override string ToString() + { + return string.Format("(X:{0} Y:{1})", X, Y); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct SMALL_RECT + { + public short Left; + public short Top; + public short Right; + public short Bottom; + + public short Width + { + get + { + // The API returns bottom/right as the inclusive lower-right + // corner, so we need +1 for the true width + return (short)(this.Right - this.Left + 1); + } + } + + public short Height + { + get + { + // The API returns bottom/right as the inclusive lower-right + // corner, so we need +1 for the true height + return (short)(this.Bottom - this.Top + 1); + } + } + + public override string ToString() + { + return string.Format("(L:{0} T:{1} R:{2} B:{3})", Left, Top, Right, Bottom); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CONSOLE_CURSOR_INFO + { + public uint dwSize; + public bool bVisible; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CONSOLE_FONT_INFO + { + public int nFont; + public COORD dwFontSize; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CONSOLE_SELECTION_INFO + { + public CONSOLE_SELECTION_INFO_FLAGS Flags; + public COORD SelectionAnchor; + public SMALL_RECT Selection; + + public override string ToString() + { + return string.Format("Flags:{0:X} Anchor:{1} Selection:{2}", Flags, SelectionAnchor, Selection); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CONSOLE_SCREEN_BUFFER_INFO + { + public COORD dwSize; + public COORD dwCursorPosition; + public CONSOLE_ATTRIBUTES wAttributes; + public SMALL_RECT srWindow; + public COORD dwMaximumWindowSize; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CONSOLE_SCREEN_BUFFER_INFO_EX + { + public uint cbSize; + public COORD dwSize; + public COORD dwCursorPosition; + public CONSOLE_ATTRIBUTES wAttributes; + public SMALL_RECT srWindow; + public COORD dwMaximumWindowSize; + + public CONSOLE_ATTRIBUTES wPopupAttributes; + public bool bFullscreenSupported; + + internal COLORREF black; + internal COLORREF darkBlue; + internal COLORREF darkGreen; + internal COLORREF darkCyan; + internal COLORREF darkRed; + internal COLORREF darkMagenta; + internal COLORREF darkYellow; + internal COLORREF gray; + internal COLORREF darkGray; + internal COLORREF blue; + internal COLORREF green; + internal COLORREF cyan; + internal COLORREF red; + internal COLORREF magenta; + internal COLORREF yellow; + internal COLORREF white; + } + + [StructLayout(LayoutKind.Sequential)] + public struct COLORREF + { + internal uint ColorDWORD; + + public COLORREF(Color color) + { + ColorDWORD = (uint)color.R + (((uint)color.G) << 8) + (((uint)color.B) << 16); + } + + public COLORREF(uint r, uint g, uint b) + { + ColorDWORD = r + (g << 8) + (b << 16); + } + + public Color GetColor() + { + return Color.FromArgb((int)(0x000000FFU & ColorDWORD), + (int)(0x0000FF00U & ColorDWORD) >> 8, (int)(0x00FF0000U & ColorDWORD) >> 16); + } + + public void SetColor(Color color) + { + ColorDWORD = (uint)color.R + (((uint)color.G) << 8) + (((uint)color.B) << 16); + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetStdHandle(CONSOLE_STD_HANDLE nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AttachConsole(UInt32 dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool SetConsoleTitle(string ConsoleTitle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleMode(IntPtr hConsoleOutputHandle, out CONSOLE_OUTPUT_MODES lpMode); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern uint GetConsoleTitle(StringBuilder lpConsoleTitle, int nSize); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetConsoleWindow(); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleSelectionInfo(out CONSOLE_SELECTION_INFO lpConsoleSelectionInfo); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleWindowInfo(IntPtr hConsoleOutput, bool bAbsolute, [In] ref SMALL_RECT lpConsoleWindow); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleCursorInfo(IntPtr hConsoleOutput, out CONSOLE_CURSOR_INFO lpConsoleCursorInfo); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleScreenBufferInfoEx(IntPtr hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO_EX ConsoleScreenBufferInfo); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleScreenBufferInfoEx(IntPtr ConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFO_EX ConsoleScreenBufferInfoEx); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetCurrentConsoleFont(IntPtr hConsoleOutput, bool bMaximumWindow, out CONSOLE_FONT_INFO lpConsoleCurrentFont); + + [DllImport("kernel32.dll", EntryPoint = "ReadConsoleOutputW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool ReadConsoleOutput( + IntPtr hConsoleOutput, + /* This pointer is treated as the origin of a two-dimensional array of CHAR_INFO structures + whose size is specified by the dwBufferSize parameter.*/ + [MarshalAs(UnmanagedType.LPArray), Out] CHAR_INFO[,] lpBuffer, + COORD dwBufferSize, + COORD dwBufferCoord, + ref SMALL_RECT lpReadRegion); + + [DllImport("kernel32.dll", EntryPoint = "WriteConsoleOutputCharacterW", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool WriteConsoleOutputCharacter( + IntPtr hConsoleOutput, + string lpCharacter, + UInt32 nLength, + COORD dwWriteCoord, + ref UInt32 lpNumberOfCharsWritten); + + } + + /// + /// The definitions within this file match the winconp.h file that is generated from wincon.w + /// Please see \windows\published\main\wincon.w + /// + public static class WinConP + { + private static readonly Guid PKEY_Console_FormatId = new Guid(0x0C570607, 0x0396, 0x43DE, new byte[] { 0x9D, 0x61, 0xE3, 0x21, 0xD7, 0xDF, 0x50, 0x26 }); + + public static readonly Wtypes.PROPERTYKEY PKEY_Console_ForceV2 = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 1 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_WrapText = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 2 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_FilterOnPaste = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 3 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_CtrlKeysDisabled = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 4 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_LineSelection = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 5 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_WindowTransparency = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 6 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_TrimZeros = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 7 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_CursorType = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 8 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_CursorColor = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 9 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_InterceptCopyPaste = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 10 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_DefaultForeground = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 11 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_DefaultBackground = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 12 }; + public static readonly Wtypes.PROPERTYKEY PKEY_Console_TerminalScrolling = new Wtypes.PROPERTYKEY() { fmtid = PKEY_Console_FormatId, pid = 13 }; + + public static readonly uint NT_CONSOLE_PROPS_SIG = 0xA0000002; + public static readonly uint NT_FE_CONSOLE_PROPS_SIG = 0xA0000004; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct NT_CONSOLE_PROPS + { + public Shell32.DATABLOCK_HEADER dbh; + public short wFillAttribute; + public short wPopupFillAttribute; + public WinCon.COORD dwScreenBufferSize; + public WinCon.COORD dwWindowSize; + public WinCon.COORD dwWindowOrigin; + public int nFont; + public int nInputBufferSize; + public WinCon.COORD dwFontSize; + public uint uFontFamily; + public uint uFontWeight; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string FaceName; + public uint uCursorSize; + public int bFullScreen; + public int bQuickEdit; + public int bInsertMode; + public int bAutoPosition; + public uint uHistoryBufferSize; + public uint uNumberOfHistoryBuffers; + public int bHistoryNoDup; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public int[] ColorTable; + public uint CursorType; + public WinCon.COLORREF CursorColor; + public bool InterceptCopyPaste; + public WinCon.COLORREF DefaultForeground; + public WinCon.COLORREF DefaultBackground; + public bool TerminalScrolling; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NT_FE_CONSOLE_PROPS + { + Shell32.DATABLOCK_HEADER dbh; + uint uCodePage; + } + } + + public static class User32 + { + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd162897(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public Int32 left; + public Int32 top; + public Int32 right; + public Int32 bottom; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public Int32 x; + public Int32 y; + } + + public const int WHEEL_DELTA = 120; + + [DllImport("user32.dll")] + public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + public const int GWL_STYLE = (-16); + public const int GWL_EXSTYLE = (-20); + + [DllImport("user32.dll", SetLastError = true)] + public static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + public static extern bool AdjustWindowRectEx(ref RECT lpRect, int dwStyle, bool bMenu, int dwExStyle); + + [DllImport("user32.dll")] + public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); + + public enum WindowMessages : UInt32 + { + WM_KEYDOWN = 0x0100, + WM_KEYUP = 0x0101, + WM_CHAR = 0x0102, + WM_MOUSEWHEEL = 0x020A, + WM_MOUSEHWHEEL = 0x020E, + WM_USER = 0x0400, + CM_SET_KEY_STATE = WM_USER + 18 + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessages Msg, Int32 wParam, IntPtr lParam); + + public enum SPI : uint + { + SPI_GETWHEELSCROLLLINES = 0x0068, + SPI_GETWHEELSCROLLCHARACTERS = 0x006C + } + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool SystemParametersInfo(SPI uiAction, uint uiParam, ref uint pvParam, uint fWinIni); + + + public enum WinEventId : uint + { + EVENT_CONSOLE_CARET = 0x4001, + EVENT_CONSOLE_UPDATE_REGION = 0x4002, + EVENT_CONSOLE_UPDATE_SIMPLE = 0x4003, + EVENT_CONSOLE_UPDATE_SCROLL = 0x4004, + EVENT_CONSOLE_LAYOUT = 0x4005, + EVENT_CONSOLE_START_APPLICATION = 0x4006, + EVENT_CONSOLE_END_APPLICATION = 0x4007 + } + + [Flags] + public enum WinEventFlags : uint + { + WINEVENT_OUTOFCONTEXT = 0x0000, // Events are ASYNC + WINEVENT_SKIPOWNTHREAD = 0x0001, // Don't call back for events on installer's thread + WINEVENT_SKIPOWNPROCESS = 0x0002, // Don't call back for events on installer's process + WINEVENT_INCONTEXT = 0x0004, // Events are SYNC, this causes your dll to be injected into every process + } + + public delegate void WinEventDelegate(IntPtr hWinEventHook, WinEventId eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetWinEventHook(WinEventId eventMin, WinEventId eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, WinEventFlags dwFlags); + + [DllImport("user32.dll")] + public static extern bool UnhookWinEvent(IntPtr hWinEventHook); + + [DllImport("user32.dll", SetLastError = true)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + public struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public uint time; + public POINT pt; + } + + public enum PM : uint + { + PM_NOREMOVE = 0x0000, + PM_REMOVE = 0x0001, + PM_NOYIELD = 0x0002, + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, PM wRemoveMsg); + + [DllImport("user32.dll", SetLastError = true)] + public static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr DispatchMessage(ref MSG lpmsg); + } + + public static class Shell32 + { + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb773249(v=vs.85).aspx + [StructLayout(LayoutKind.Sequential)] + public struct DATABLOCK_HEADER + { + public int cbSize; + public int dwSignature; + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb774944(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Enums/SLGP_FLAGS.html + /// IShellLink.GetPath fFlags: Flags that specify the type of path information to retrieve + [Flags()] + public enum SLGP_FLAGS + { + /// Retrieves the standard short (8.3 format) file name + SLGP_SHORTPATH = 0x1, + /// Retrieves the Universal Naming Convention (UNC) path name of the file + SLGP_UNCPRIORITY = 0x2, + /// Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded + SLGP_RAWPATH = 0x4 + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb774952(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Enums/SLR_FLAGS.html + /// IShellLink.Resolve fFlags + [Flags()] + public enum SLR_FLAGS + { + /// + /// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set, + /// the high-order word of fFlags can be set to a time-out value that specifies the + /// maximum amount of time to be spent resolving the link. The function returns if the + /// link cannot be resolved within the time-out duration. If the high-order word is set + /// to zero, the time-out duration will be set to the default value of 3,000 milliseconds + /// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out + /// duration, in milliseconds. + /// + SLR_NO_UI = 0x1, + /// Obsolete and no longer used + SLR_ANY_MATCH = 0x2, + /// If the link object has changed, update its path and list of identifiers. + /// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine + /// whether or not the link object has changed. + SLR_UPDATE = 0x4, + /// Do not update the link information + SLR_NOUPDATE = 0x8, + /// Do not execute the search heuristics + SLR_NOSEARCH = 0x10, + /// Do not use distributed link tracking + SLR_NOTRACK = 0x20, + /// Disable distributed link tracking. By default, distributed link tracking tracks + /// removable media across multiple devices based on the volume name. It also uses the + /// Universal Naming Convention (UNC) path to track remote file systems whose drive letter + /// has changed. Setting SLR_NOLINKINFO disables both types of tracking. + SLR_NOLINKINFO = 0x40, + /// Call the Microsoft Windows Installer + SLR_INVOKE_MSI = 0x80 + } + + [ComImport, Guid("00021401-0000-0000-C000-000000000046")] + public class ShellLink + { + // Making new of this class will call CoCreate e.g. new ShellLink(); + // Cast to one of the interfaces below will QueryInterface. e.g. (IPersistFile)new ShellLink(); + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb774950(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Interfaces/IShellLinkW.html + /// The IShellLink interface allows Shell links to be created, modified, and resolved + [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] + public interface IShellLinkW + { + /// Retrieves the path and file name of a Shell link object + void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WinBase.WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); + /// Retrieves the list of item identifiers for a Shell link object + void GetIDList(out IntPtr ppidl); + /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. + void SetIDList(IntPtr pidl); + /// Retrieves the description string for a Shell link object + void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); + /// Sets the description for a Shell link object. The description can be any application-defined string + void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); + /// Retrieves the name of the working directory for a Shell link object + void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); + /// Sets the name of the working directory for a Shell link object + void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); + /// Retrieves the command-line arguments associated with a Shell link object + void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); + /// Sets the command-line arguments for a Shell link object + void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); + /// Retrieves the hot key for a Shell link object + void GetHotkey(out short pwHotkey); + /// Sets a hot key for a Shell link object + void SetHotkey(short wHotkey); + /// Retrieves the show command for a Shell link object + void GetShowCmd(out int piShowCmd); + /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. + void SetShowCmd(int iShowCmd); + /// Retrieves the location (path and index) of the icon for a Shell link object + void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, + int cchIconPath, out int piIcon); + /// Sets the location (path and index) of the icon for a Shell link object + void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); + /// Sets the relative path to the Shell link object + void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); + /// Attempts to find the target of a Shell link, even if it has been moved or renamed + void Resolve(IntPtr hwnd, SLR_FLAGS fFlags); + /// Sets the path and file name of a Shell link object + void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb774916(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Interfaces/IShellLonkDataList.html + [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("45e2b4ae-b1c3-11d0-b92f-00a0c90312e1")] + public interface IShellLinkDataList + { + void AddDataBlock(IntPtr pDataBlock); + void CopyDataBlock(uint dwSig, out IntPtr ppDataBlock); + void RemoveDataBlock(uint dwSig); + void GetFlags(out uint pdwFlags); + void SetFlags(uint dwFlags); + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms688695(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Interfaces/IPersist.html + [ComImport, Guid("0000010c-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPersist + { + [PreserveSig] + void GetClassID(out Guid pClassID); + } + + + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms687223(v=vs.85).aspx + // http://www.pinvoke.net/default.aspx/Interfaces/IPersistFile.html + [ComImport, Guid("0000010b-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPersistFile : IPersist + { + new void GetClassID(out Guid pClassID); + + [PreserveSig] + int IsDirty(); + + [PreserveSig] + void Load([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode); + + [PreserveSig] + void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName, + [In, MarshalAs(UnmanagedType.Bool)] bool fRemember); + + [PreserveSig] + void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName); + + [PreserveSig] + void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName); + } + + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb761474(v=vs.85).aspx + // http://www.pinvoke.net/default.aspx/Interfaces/IPropertyStore.html + [ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPropertyStore + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount([Out] out uint cProps); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAt([In] uint iProp, out Wtypes.PROPERTYKEY pkey); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetValue([In] ref Wtypes.PROPERTYKEY key, out object pv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetValue([In] ref Wtypes.PROPERTYKEY key, [In] ref object pv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Commit(); + } + } + + public static class WinBase + { + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365740(v=vs.85).aspx + // http://www.pinvoke.net/default.aspx/Structures/WIN32_FIND_DATA.html + // The CharSet must match the CharSet of the corresponding PInvoke signature + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WIN32_FIND_DATAW + { + public uint dwFileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + public uint dwReserved0; + public uint dwReserved1; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string cFileName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] + public string cAlternateFileName; + } + + public enum STARTF : Int32 + { + STARTF_TITLEISLINKNAME = 0x00000800 + } + + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwYSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public STARTF dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFOEX + { + public STARTUPINFO StartupInfo; + public IntPtr lpAttributeList; + } + + [Flags] + public enum CP_CreationFlags : uint + { + CREATE_SUSPENDED = 0x4, + CREATE_NEW_CONSOLE = 0x10, + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool CreateProcess(string lpApplicationName, + string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + CP_CreationFlags dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, IntPtr lpName); + + [DllImport("kernel32.dll")] + public static extern bool TerminateJobObject(IntPtr hJob, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern int ResumeThread(IntPtr hThread); + + public enum JOBOBJECTINFOCLASS : uint + { + JobObjectBasicProcessIdList = 3 + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_PROCESS_ID_LIST + { + public uint NumberOfAssignedProcesses; + public uint NumberOfProcessIdsInList; + public IntPtr ProcessId; + public IntPtr ProcessId2; + } + + [DllImport("kernel32.dll")] + public static extern bool QueryInformationJobObject(IntPtr hJob, + JOBOBJECTINFOCLASS JobObjectInformationClass, + IntPtr lpJobObjectInfo, + int cbJobObjectInfoLength, + IntPtr lpReturnLength); + } + + public static class Wtypes + { + // http://msdn.microsoft.com/en-us/library/windows/desktop/bb773381(v=vs.85).aspx + // http://pinvoke.net/default.aspx/Structures/PROPERTYKEY.html + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct PROPERTYKEY + { + public Guid fmtid; + public uint pid; + } + } + + public static class ObjBase + { + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380337(v=vs.85).aspx + // http://www.pinvoke.net/default.aspx/Enums/StgmConstants.html + [Flags] + public enum STGM + { + STGM_READ = 0x0, + STGM_WRITE = 0x1, + STGM_READWRITE = 0x2, + STGM_SHARE_DENY_NONE = 0x40, + STGM_SHARE_DENY_READ = 0x30, + STGM_SHARE_DENY_WRITE = 0x20, + STGM_SHARE_EXCLUSIVE = 0x10, + STGM_PRIORITY = 0x40000, + STGM_CREATE = 0x1000, + STGM_CONVERT = 0x20000, + STGM_FAILIFTHERE = 0x0, + STGM_DIRECT = 0x0, + STGM_TRANSACTED = 0x10000, + STGM_NOSCRATCH = 0x100000, + STGM_NOSNAPSHOT = 0x200000, + STGM_SIMPLE = 0x8000000, + STGM_DIRECT_SWMR = 0x400000, + STGM_DELETEONRELEASE = 0x4000000 + } + } +} diff --git a/src/host/ft_uia/Common/RegistryHelper.cs b/src/host/ft_uia/Common/RegistryHelper.cs new file mode 100644 index 000000000..d96b88efe --- /dev/null +++ b/src/host/ft_uia/Common/RegistryHelper.cs @@ -0,0 +1,123 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Console UI Automation registry manipulation +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + using System.Diagnostics; + using System.IO; + + using Microsoft.Win32; + + using WEX.Common.Managed; + using WEX.TestExecution; + + public class RegistryHelper : IDisposable + { + private static readonly string regExePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "reg.exe"); + private static readonly string regSaveCommand = @"export {0} {1}"; // 0 = path in reg, 1 = file name + private static readonly string regDelCommand = @"delete {0} /f"; // 0 = path in reg + private static readonly string regLoadCommand = @"import {0}"; // 0 = file + + private static readonly string regNode = @"HKCU\Console"; + private static readonly string regNodeVerbose = @"HKEY_CURRENT_USER\Console"; + + private string backupFile; + + public RegistryHelper() + { + + } + + ~RegistryHelper() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (!string.IsNullOrEmpty(this.backupFile)) + { + this.RestoreRegistry(); + } + } + } + + public void SetDefaultValue(string valueName, object valueValue) + { + AutoHelpers.LogInvariant("Setting registry key {0}'s value name {1} to value {2}", regNode, valueName, valueValue); + Registry.SetValue(regNodeVerbose, valueName, valueValue); + } + + public void BackupRegistry() + { + AutoHelpers.LogInvariant("Save existing registry key status."); + this.backupFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + AutoHelpers.LogInvariant("Backing up registry to file: {0}", backupFile); + + Verify.IsFalse(File.Exists(backupFile)); + + string backupCmd = AutoHelpers.FormatInvariant(regSaveCommand, regNode, backupFile); + + AutoHelpers.LogInvariant("Calling command: {0} {1}", regExePath, backupCmd); + + Process regProc = Process.Start(regExePath, backupCmd); + regProc.WaitForExit(); + + Verify.IsTrue(File.Exists(backupFile)); + } + + private void DeleteRegistry() + { + AutoHelpers.LogInvariant("Deleting registry node: {0}", regNode); + string deleteCmd = AutoHelpers.FormatInvariant(regDelCommand, regNode); + + Process regProc = Process.Start(regExePath, deleteCmd); + regProc.WaitForExit(); + } + + public void RestoreRegistry() + { + AutoHelpers.LogInvariant("Restore settings to pre-test status."); + this.DeleteRegistry(); + + AutoHelpers.LogInvariant("Restoring registry from file: {0}", backupFile); + + Verify.IsTrue(File.Exists(backupFile)); + + string restoreCmd = AutoHelpers.FormatInvariant(regLoadCommand, backupFile); + + Process regProc = Process.Start(regExePath, restoreCmd); + regProc.WaitForExit(); + + File.Delete(backupFile); + + this.backupFile = null; + } + + public RegistryKey GetMatchingKey(OpenTarget target) + { + switch (target) + { + case OpenTarget.Defaults: + return Registry.CurrentUser.OpenSubKey(@"Console"); + case OpenTarget.Specifics: + return Registry.CurrentUser.OpenSubKey(@"Console").OpenSubKey("%SystemRoot%_system32_cmd.exe"); + default: + throw new NotImplementedException(AutoHelpers.FormatInvariant("This type of registry key isn't implemented: {0}", target.ToString())); + } + } + } +} diff --git a/src/host/ft_uia/Common/ShortcutHelper.cs b/src/host/ft_uia/Common/ShortcutHelper.cs new file mode 100644 index 000000000..f844480cf --- /dev/null +++ b/src/host/ft_uia/Common/ShortcutHelper.cs @@ -0,0 +1,178 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Console UI Automation shortcut file manipulation +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Runtime.InteropServices; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common.NativeMethods; + + public class ShortcutHelper : IDisposable + { + private bool isDisposed = false; + + public string ShortcutPath { get; private set; } + + public ShortcutHelper() + { + this.ShortcutPath = null; + } + + ~ShortcutHelper() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + this.CleanupTempCmdShortcut(); + } + + this.isDisposed = true; + } + + public void CreateTempCmdShortcut() + { + string tempPath = Path.Combine(Path.GetTempPath(), AutoHelpers.FormatInvariant("{0}.lnk", Path.GetRandomFileName())); + AutoHelpers.LogInvariant("Creating temporary shortcut: {0}", tempPath); + + Shell32.IShellLinkW link = (Shell32.IShellLinkW)new Shell32.ShellLink(); + link.SetDescription("Created by Conhost.UIA.Tests.Common.ShortcutHelper"); + link.SetPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe")); + + Shell32.IPersistFile persist = (Shell32.IPersistFile)link; // performs QI + persist.Save(tempPath, false); + + this.ShortcutPath = tempPath; + } + + public void CleanupTempCmdShortcut() + { + if (!string.IsNullOrEmpty(this.ShortcutPath)) + { + File.Delete(this.ShortcutPath); + this.ShortcutPath = null; + } + } + + public WinConP.NT_CONSOLE_PROPS GetConsoleProps() + { + Shell32.IPersistFile persist = (Shell32.IPersistFile)new Shell32.ShellLink(); + persist.Load(this.ShortcutPath, (uint)ObjBase.STGM.STGM_READ); + + Shell32.IShellLinkDataList sldl = (Shell32.IShellLinkDataList)persist; + + WinConP.NT_CONSOLE_PROPS props = new WinConP.NT_CONSOLE_PROPS(); + IntPtr ppDataBlock = Marshal.AllocHGlobal(Marshal.SizeOf(props)); + + try + { + sldl.CopyDataBlock(WinConP.NT_CONSOLE_PROPS_SIG, out ppDataBlock); + + // The marshaler doesn't like using the existing instance that we made above because it's a value type and + // there are potential string pointers here. Give it the type instead and it can handle setting everything up. + props = (WinConP.NT_CONSOLE_PROPS)Marshal.PtrToStructure(ppDataBlock, typeof(WinConP.NT_CONSOLE_PROPS)); + } + finally + { + Marshal.FreeHGlobal(ppDataBlock); + } + + return props; + } + + public void SetConsoleProps(WinConP.NT_CONSOLE_PROPS props) + { + Shell32.IPersistFile persist = (Shell32.IPersistFile)new Shell32.ShellLink(); + persist.Load(this.ShortcutPath, (uint)ObjBase.STGM.STGM_READWRITE); + + Shell32.IShellLinkDataList sldl = (Shell32.IShellLinkDataList)persist; + + sldl.RemoveDataBlock(WinConP.NT_CONSOLE_PROPS_SIG); + + IntPtr ppDataBlock = Marshal.AllocHGlobal(Marshal.SizeOf(props)); + try + { + Marshal.StructureToPtr(props, ppDataBlock, false); + + // we are assuming that the signature is set properly on the NT_CONSOLE_PROPS structure + Verify.AreEqual(props.dbh.dwSignature, (int)WinConP.NT_CONSOLE_PROPS_SIG); + sldl.AddDataBlock(ppDataBlock); + + persist.Save(null, true); // 2nd var is ignored when 1st is null + } + finally + { + Marshal.FreeHGlobal(ppDataBlock); + } + } + + public IDictionary GetFromPropertyStore(IEnumerable keys) + { + if (keys == null) + { + throw new NotSupportedException(AutoHelpers.FormatInvariant("Keys passed cannot be null")); + } + + Shell32.IPersistFile persist = (Shell32.IPersistFile)new Shell32.ShellLink(); + persist.Load(this.ShortcutPath, (uint)ObjBase.STGM.STGM_READ); + + Shell32.IPropertyStore store = (Shell32.IPropertyStore)persist; + + Dictionary results = new Dictionary(); + + foreach (Wtypes.PROPERTYKEY key in keys) + { + Wtypes.PROPERTYKEY pkey = key; // iteration variables are read-only and we need to pass by ref + object pv; + store.GetValue(ref pkey, out pv); + + results.Add(key, pv); + } + + return results; + } + + public void SetToPropertyStore(IDictionary properties) + { + if (properties == null) + { + throw new NotSupportedException(AutoHelpers.FormatInvariant("Properties passed cannot be null.")); + } + + Shell32.IPersistFile persist = (Shell32.IPersistFile)new Shell32.ShellLink(); + persist.Load(this.ShortcutPath, (uint)ObjBase.STGM.STGM_READWRITE); + + Shell32.IPropertyStore store = (Shell32.IPropertyStore)persist; + + foreach (Wtypes.PROPERTYKEY key in properties.Keys) + { + Wtypes.PROPERTYKEY pkey = key; // iteration variables are read-only and we need to pass by ref + object pv = properties[key]; + + store.SetValue(ref pkey, ref pv); + } + + persist.Save(null, true); + } + } +} diff --git a/src/host/ft_uia/Common/SliderMeta.cs b/src/host/ft_uia/Common/SliderMeta.cs new file mode 100644 index 000000000..ad1371857 --- /dev/null +++ b/src/host/ft_uia/Common/SliderMeta.cs @@ -0,0 +1,64 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Range Value Slider metadata information helper class. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + using System.Globalization; + + using WEX.Logging.Interop; + using OpenQA.Selenium.Appium; + using OpenQA.Selenium; + + public struct SliderMeta + { + /// + /// When testing a slider, this is the position we expect to set it to. + /// + public enum ExpectedPosition + { + Maximum, + Minimum + } + + public AppiumWebElement Slider { get; private set; } + public string ValueName { get; private set; } + public bool IsV2Property { get; private set; } + public NativeMethods.Wtypes.PROPERTYKEY? PropKey { get; private set; } + + + public SliderMeta(AppiumWebElement slider, string valueName, bool isV2Property, NativeMethods.Wtypes.PROPERTYKEY? propKey) + : this() + { + this.Slider = slider; + this.ValueName = valueName; + this.IsV2Property = isV2Property; + this.PropKey = propKey; + } + + public void SetToMaximum() + { + this.Slider.Click(); + this.Slider.SendKeys(Keys.End); + } + + public void SetToMinimum() + { + this.Slider.Click(); + this.Slider.SendKeys(Keys.Home); + } + + public int GetMaximum() + { + return 100; + } + + public int GetMinimum() + { + return 30; + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Common/VersionSelector.cs b/src/host/ft_uia/Common/VersionSelector.cs new file mode 100644 index 000000000..8f4406f65 --- /dev/null +++ b/src/host/ft_uia/Common/VersionSelector.cs @@ -0,0 +1,40 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Console UI Automation Version selection +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Common +{ + using System; + + using WEX.Logging.Interop; + + public static class VersionSelector + { + private static string v1v2ValueName = "ForceV2"; + + public static void SetConsoleVersion(RegistryHelper reg, ConsoleVersion version) + { + switch (version) + { + case ConsoleVersion.V1: + Log.Comment("Setting console to v1 mode."); + reg.SetDefaultValue(v1v2ValueName, 0); + break; + case ConsoleVersion.V2: + Log.Comment("Setting console to v2 mode."); + reg.SetDefaultValue(v1v2ValueName, 1); + break; + default: + throw new NotImplementedException(); + } + } + } + + public enum ConsoleVersion + { + V1, // legacy 1990s console + V2 // revision from 2014/2015. + } +} diff --git a/src/host/ft_uia/Elements/CmdApp.cs b/src/host/ft_uia/Elements/CmdApp.cs new file mode 100644 index 000000000..2db9e0d8e --- /dev/null +++ b/src/host/ft_uia/Elements/CmdApp.cs @@ -0,0 +1,425 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Helper and wrapper for generating the base test application and its UI Root node. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Diagnostics; + using System.IO; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + + using OpenQA.Selenium; + using OpenQA.Selenium.Remote; + using OpenQA.Selenium.Appium; + using OpenQA.Selenium.Appium.iOS; + using OpenQA.Selenium.Interactions; + + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using System.Runtime.InteropServices; + using System.Drawing; + using System.Text; + using System.Threading.Tasks; + using System.Threading; + using System.Security.Principal; + + public class CmdApp : IDisposable + { + private static readonly string binPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); + private static readonly string linkPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + @"Microsoft\Windows\Start Menu\Programs\System Tools\Command Prompt.lnk"); + + protected const string AppDriverUrl = "http://127.0.0.1:4723"; + + private IntPtr job; + private int pid; + + public IOSDriver Session { get; private set; } + public Actions Actions { get; private set; } + public AppiumWebElement UIRoot { get; private set; } + + private IntPtr hStdOut = IntPtr.Zero; + private IntPtr hStdErr = IntPtr.Zero; + private bool isDisposed = false; + + private TestContext context; + + public CmdApp(CreateType type, TestContext context, string pathOverride = "") + { + this.context = context; + this.CreateCmdProcess(type, pathOverride); + } + + public CmdApp(string path, string linkpath, TestContext context) + { + + } + + ~CmdApp() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public AppiumWebElement GetTitleBar() + { + return this.UIRoot.FindElementByAccessibilityId("TitleBar"); + } + + public AppiumWebElement GetCloseButton() + { + return this.UIRoot.FindElementByName("Close"); + } + + public IntPtr GetStdOutHandle() + { + return hStdOut; + } + public IntPtr GetStdErrHandle() + { + return hStdErr; + } + + public void SetWrapState(bool WrapOn) + { + // Go to property sheet and make sure that wrap is set + using (PropertiesDialog props = new PropertiesDialog(this)) + { + props.Open(OpenTarget.Specifics); + using (Tabs tabs = new Tabs(props)) + { + tabs.SetWrapState(WrapOn); + } + props.Close(PropertiesDialog.CloseAction.OK); + } + } + + public bool IsVirtualTerminalEnabled(IntPtr hConsoleOutput) + { + WinCon.CONSOLE_OUTPUT_MODES modes; + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleMode(hConsoleOutput, out modes), "Retrieve console output mode flags."); + + return (modes & WinCon.CONSOLE_OUTPUT_MODES.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + } + + public string GetWindowTitle() + { + int size = 100; + StringBuilder builder = new StringBuilder(size); + + WinCon.GetConsoleTitle(builder, size); + + return builder.ToString(); + } + + public IntPtr GetWindowHandle() + { + return WinCon.GetConsoleWindow(); + } + + public int GetPid() + { + return pid; + } + + public int GetRowsPerScroll() + { + uint rows = 0; + NativeMethods.Win32BoolHelper(User32.SystemParametersInfo(User32.SPI.SPI_GETWHEELSCROLLLINES, 0, ref rows, 0), "Retrieve rows per click."); + + return (int)rows; + } + + public int GetColsPerScroll() + { + uint cols = 0; + NativeMethods.Win32BoolHelper(User32.SystemParametersInfo(User32.SPI.SPI_GETWHEELSCROLLCHARACTERS, 0, ref cols, 0), "Retrieve cols per click."); + + return (int)cols; + } + + public void ScrollWindow(int scrolls = -1) + { + User32.SendMessage(WinCon.GetConsoleWindow(), User32.WindowMessages.WM_MOUSEWHEEL, (User32.WHEEL_DELTA * scrolls) << 16, IntPtr.Zero); + } + + public void HScrollWindow(int scrolls = -1) + { + User32.SendMessage(WinCon.GetConsoleWindow(), User32.WindowMessages.WM_MOUSEHWHEEL, (User32.WHEEL_DELTA * scrolls) << 16, IntPtr.Zero); + } + + public WinCon.COORD GetCursorPosition(IntPtr hConsole) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex = GetScreenBufferInfo(hConsole); + + return sbiex.dwCursorPosition; + } + + public void FillCursorPosition(IntPtr hConsole, ref Point pt) + { + WinCon.COORD coord = GetCursorPosition(hConsole); + pt.X = coord.X; + pt.Y = coord.Y; + } + + public bool IsCursorVisible(IntPtr hConsole) + { + WinCon.CONSOLE_CURSOR_INFO cursorInfo = GetCursorInfo(hConsole); + return cursorInfo.bVisible; + } + + public WinCon.CONSOLE_ATTRIBUTES GetCurrentAttributes(IntPtr hConsole) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex = GetScreenBufferInfo(hConsole); + return sbiex.wAttributes; + } + + public WinCon.SMALL_RECT GetViewport(IntPtr hConsole) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex = GetScreenBufferInfo(hConsole); + + return sbiex.srWindow; + } + + public WinCon.CONSOLE_CURSOR_INFO GetCursorInfo(IntPtr hConsole) + { + WinCon.CONSOLE_CURSOR_INFO cursorInfo = new WinCon.CONSOLE_CURSOR_INFO(); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleCursorInfo(hConsole, out cursorInfo), "Get cursor display information."); + return cursorInfo; + } + + public WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX GetScreenBufferInfo() + { + return GetScreenBufferInfo(GetStdOutHandle()); + } + + public WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX GetScreenBufferInfo(IntPtr hConsole) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + sbiex.cbSize = (uint)Marshal.SizeOf(sbiex); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Get screen buffer info for cursor position."); + return sbiex; + } + + public void SetScreenBufferInfo(WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex) + { + SetScreenBufferInfo(GetStdOutHandle(), sbiex); + } + + public void SetScreenBufferInfo(IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex) + { + NativeMethods.Win32BoolHelper(WinCon.SetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Set screen buffer info with given data."); + } + + + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + // ensure we're exited when this is destroyed or disposed of explicitly + this.ExitCmdProcess(); + + this.isDisposed = true; + } + } + + private void CreateCmdProcess(string path, string link = "") + { + //string AdminPrefix = "Administrator: "; + string WindowTitleToFind = "Host.Tests.UIA window under test"; + + job = WinBase.CreateJobObject(IntPtr.Zero, IntPtr.Zero); + NativeMethods.Win32NullHelper(job, "Creating job object to hold binaries under test."); + + Log.Comment("Attempting to launch command-line application at '{0}'", path); + + string binaryToRunPath = path; + +#if __INSIDE_WINDOWS + string launchArgs = binaryToRunPath; +#else + string openConsolePath = Path.Combine(this.context.TestDeploymentDir, "OpenConsole.exe"); + + string launchArgs = $"{openConsolePath} {binaryToRunPath}"; +#endif + + WinBase.STARTUPINFO si = new WinBase.STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + + // If we were given a LNK file to startup with, set the STARTUPINFO structure to pass that information in to the console host. + if (!string.IsNullOrEmpty(link)) + { + si.dwFlags |= WinBase.STARTF.STARTF_TITLEISLINKNAME; + si.lpTitle = link; + } + + WinBase.PROCESS_INFORMATION pi = new WinBase.PROCESS_INFORMATION(); + + NativeMethods.Win32BoolHelper(WinBase.CreateProcess(null, + launchArgs, + IntPtr.Zero, + IntPtr.Zero, + false, + WinBase.CP_CreationFlags.CREATE_NEW_CONSOLE | WinBase.CP_CreationFlags.CREATE_SUSPENDED, + IntPtr.Zero, + null, + ref si, + out pi), + "Attempting to create child host window process."); + + Log.Comment($"Host window PID: {pi.dwProcessId}"); + + NativeMethods.Win32BoolHelper(WinBase.AssignProcessToJobObject(job, pi.hProcess), "Assigning new host window (suspended) to job object."); + NativeMethods.Win32BoolHelper(-1 != WinBase.ResumeThread(pi.hThread), "Resume host window process now that it is attached and its launch of the child application will be caught in the job object."); + + Globals.WaitForTimeout(); + + WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST list = new WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST(); + list.NumberOfAssignedProcesses = 2; + + int listptrsize = Marshal.SizeOf(list); + IntPtr listptr = Marshal.AllocHGlobal(listptrsize); + Marshal.StructureToPtr(list, listptr, false); + + TimeSpan totalWait = TimeSpan.Zero; + TimeSpan waitLimit = TimeSpan.FromSeconds(30); + TimeSpan pollInterval = TimeSpan.FromMilliseconds(500); + while (totalWait < waitLimit) + { + WinBase.QueryInformationJobObject(job, WinBase.JOBOBJECTINFOCLASS.JobObjectBasicProcessIdList, listptr, listptrsize, IntPtr.Zero); + list = (WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST)Marshal.PtrToStructure(listptr, typeof(WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST)); + + if (list.NumberOfAssignedProcesses > 1) + { + break; + } + else if (list.NumberOfAssignedProcesses < 1) + { + Verify.Fail("Somehow we lost the one console host process in the job already."); + } + + Thread.Sleep(pollInterval); + totalWait += pollInterval; + } + Verify.IsLessThan(totalWait, waitLimit); + + WinBase.QueryInformationJobObject(job, WinBase.JOBOBJECTINFOCLASS.JobObjectBasicProcessIdList, listptr, listptrsize, IntPtr.Zero); + list = (WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST)Marshal.PtrToStructure(listptr, typeof(WinBase.JOBOBJECT_BASIC_PROCESS_ID_LIST)); + + Verify.AreEqual(list.NumberOfAssignedProcesses, list.NumberOfProcessIdsInList); + +#if __INSIDE_WINDOWS + pid = pi.dwProcessId; +#else + // Take whichever PID isn't the host window's PID as the child. + pid = pi.dwProcessId == (int)list.ProcessId ? (int)list.ProcessId2 : (int)list.ProcessId; + Log.Comment($"Child command app PID: {pid}"); +#endif + + // Free any attached consoles and attach to the console we just created. + // The driver will bind our calls to the Console APIs into the child process. + // This will allow us to use the APIs to get/set the console state of the test window. + NativeMethods.Win32BoolHelper(WinCon.FreeConsole(), "Free existing console bindings."); + // need to wait a bit or we might not be able to reliably attach + System.Threading.Thread.Sleep(Globals.Timeout); + NativeMethods.Win32BoolHelper(WinCon.AttachConsole((uint)pid), "Bind to the new PID for console APIs."); + + // we need to wait here for a bit or else + // setting the console window title will fail. + System.Threading.Thread.Sleep(Globals.Timeout * 5); + NativeMethods.Win32BoolHelper(WinCon.SetConsoleTitle(WindowTitleToFind), "Set the window title so AppDriver can find it."); + + DesiredCapabilities appCapabilities = new DesiredCapabilities(); + appCapabilities.SetCapability("app", @"Root"); + Session = new IOSDriver(new Uri(AppDriverUrl), appCapabilities); + + Verify.IsNotNull(Session); + Actions = new Actions(Session); + Verify.IsNotNull(Session); + + Globals.WaitForTimeout(); + + // If we are running as admin, the child window title will have a prefix appended as it will also run as admin. + //if (IsRunningAsAdmin()) + //{ + // WindowTitleToFind = $"{AdminPrefix}{WindowTitleToFind}"; + //} + + Log.Comment($"Searching for window title '{WindowTitleToFind}'"); + this.UIRoot = Session.FindElementByName(WindowTitleToFind); + this.hStdOut = WinCon.GetStdHandle(WinCon.CONSOLE_STD_HANDLE.STD_OUTPUT_HANDLE); + Verify.IsNotNull(this.hStdOut, "Ensure output handle is valid."); + this.hStdErr = WinCon.GetStdHandle(WinCon.CONSOLE_STD_HANDLE.STD_ERROR_HANDLE); + Verify.IsNotNull(this.hStdErr, "Ensure error handle is valid."); + + // Set the timeout to 15 seconds after we found the initial window. + Session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15); + } + + private bool IsRunningAsAdmin() + { + return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + } + + private void CreateCmdProcess(CreateType type, string pathOverride = "") + { + switch (type) + { + case CreateType.ProcessOnly: + if (!string.IsNullOrEmpty(pathOverride)) + { + this.CreateCmdProcess(pathOverride); + } + else + { + this.CreateCmdProcess(binPath); + } + break; + case CreateType.ShortcutFile: + if (!string.IsNullOrEmpty(pathOverride)) + { + this.CreateCmdProcess(binPath, pathOverride); + } + else + { + this.CreateCmdProcess(binPath, linkPath); + } + break; + default: + throw new NotImplementedException(AutoHelpers.FormatInvariant("CreateType '{0}' not implemented.", type.ToString())); + } + } + + private void ExitCmdProcess() + { + // Release attachment to the child process console. + WinCon.FreeConsole(); + + this.UIRoot = null; + + if (this.job != IntPtr.Zero) + { + WinBase.TerminateJobObject(this.job, 0); + } + this.job = IntPtr.Zero; + } + + public WinEventSystem AttachWinEventSystem(IWinEventCallbacks callbacks) + { + return new WinEventSystem(callbacks, (uint)this.pid); + } + } +} diff --git a/src/host/ft_uia/Elements/ColorsTab.cs b/src/host/ft_uia/Elements/ColorsTab.cs new file mode 100644 index 000000000..3b723115b --- /dev/null +++ b/src/host/ft_uia/Elements/ColorsTab.cs @@ -0,0 +1,51 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the Colors tab of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + + using Conhost.UIA.Tests.Common; + using NativeMethods = Conhost.UIA.Tests.Common.NativeMethods; + using OpenQA.Selenium.Appium; + + public class ColorsTab : TabBase + { + public SliderMeta OpacitySlider { get; private set; } + + public ColorsTab(PropertiesDialog propDialog) : base(propDialog, " Colors ") + { + } + + protected override void PopulateItemsOnNavigate(AppiumWebElement propWindow) + { + var slider = propWindow.FindElementByClassName("msctls_trackbar32"); + + this.OpacitySlider = new SliderMeta(slider, "WindowAlpha", true, NativeMethods.WinConP.PKEY_Console_WindowTransparency); + } + + public override IEnumerable GetObjectsDisabledForV1Console() + { + return new AppiumWebElement[] { this.OpacitySlider.Slider }; + } + + public override IEnumerable GetObjectsUnaffectedByV1V2Switch() + { + return new AppiumWebElement[0]; + } + + public override IEnumerable GetCheckboxesForVerification() + { + return new CheckBoxMeta[0]; + } + + public override IEnumerable GetSlidersForVerification() + { + return new SliderMeta[] { this.OpacitySlider }; + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/FontTab.cs b/src/host/ft_uia/Elements/FontTab.cs new file mode 100644 index 000000000..728e0c2dc --- /dev/null +++ b/src/host/ft_uia/Elements/FontTab.cs @@ -0,0 +1,48 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the Font tab of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + + using Conhost.UIA.Tests.Common; + using NativeMethods = Conhost.UIA.Tests.Common.NativeMethods; + using OpenQA.Selenium.Appium; + + public class FontTab : TabBase + { + public FontTab(PropertiesDialog propDialog) : base(propDialog, " Font ") + { + + } + + protected override void PopulateItemsOnNavigate(AppiumWebElement propWindow) + { + return; + } + + public override IEnumerable GetObjectsDisabledForV1Console() + { + return new AppiumWebElement[0]; + } + + public override IEnumerable GetObjectsUnaffectedByV1V2Switch() + { + return new AppiumWebElement[0]; + } + + public override IEnumerable GetCheckboxesForVerification() + { + return new CheckBoxMeta[0]; + } + + public override IEnumerable GetSlidersForVerification() + { + return new SliderMeta[0]; + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/LayoutTab.cs b/src/host/ft_uia/Elements/LayoutTab.cs new file mode 100644 index 000000000..1644fba05 --- /dev/null +++ b/src/host/ft_uia/Elements/LayoutTab.cs @@ -0,0 +1,53 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the Layout tab of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Conhost.UIA.Tests.Common; + using NativeMethods = Conhost.UIA.Tests.Common.NativeMethods; + using OpenQA.Selenium.Appium; + + public class LayoutTab : TabBase + { + public List Checkboxes { get; private set; } + public CheckBoxMeta WrapTextCheckBox { get; private set; } + + public LayoutTab(PropertiesDialog propDialog) : base(propDialog, " Layout ") + { + } + + protected override void PopulateItemsOnNavigate(AppiumWebElement propWindow) + { + this.Checkboxes = new List(); + this.WrapTextCheckBox = new CheckBoxMeta(propWindow, "Wrap text output on resize", "LineWrap", false, false, true, NativeMethods.WinConP.PKEY_Console_WrapText); + this.Checkboxes.Add(this.WrapTextCheckBox); + } + + public override IEnumerable GetObjectsDisabledForV1Console() + { + return this.Checkboxes.Select(meta => meta.Box); + } + + public override IEnumerable GetObjectsUnaffectedByV1V2Switch() + { + return new AppiumWebElement[0]; + } + + public override IEnumerable GetCheckboxesForVerification() + { + return this.Checkboxes; + } + + public override IEnumerable GetSlidersForVerification() + { + return new SliderMeta[0]; + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/OptionsTab.cs b/src/host/ft_uia/Elements/OptionsTab.cs new file mode 100644 index 000000000..0bb2fbfa0 --- /dev/null +++ b/src/host/ft_uia/Elements/OptionsTab.cs @@ -0,0 +1,61 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the Options tab of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Conhost.UIA.Tests.Common; + using NativeMethods = Conhost.UIA.Tests.Common.NativeMethods; + using OpenQA.Selenium.Appium; + + public class OptionsTab : TabBase + { + public List Checkboxes { get; private set; } + public CheckBoxMeta GlobalV1V2Box { get; private set; } + public AppiumWebElement MoreInfoLink { get; private set; } + + public OptionsTab(PropertiesDialog propDialog) : base(propDialog, " Options ") + { + } + + protected override void PopulateItemsOnNavigate(AppiumWebElement propWindow) + { + this.GlobalV1V2Box = new CheckBoxMeta(propWindow, "Use legacy console (requires relaunch)", "ForceV2", false, true, false, NativeMethods.WinConP.PKEY_Console_ForceV2); + + this.Checkboxes = new List(); + this.Checkboxes.Add(new CheckBoxMeta(propWindow, "Enable line wrapping selection", "LineSelection", false, false, true, NativeMethods.WinConP.PKEY_Console_LineSelection)); + this.Checkboxes.Add(new CheckBoxMeta(propWindow, "Filter clipboard contents on paste", "FilterOnPaste", false, false, true, NativeMethods.WinConP.PKEY_Console_FilterOnPaste)); + this.Checkboxes.Add(new CheckBoxMeta(propWindow, "Enable Ctrl key shortcuts", "CtrlKeyShortcutsDisabled", true, false, true, NativeMethods.WinConP.PKEY_Console_CtrlKeysDisabled)); + this.Checkboxes.Add(new CheckBoxMeta(propWindow, "Extended text selection keys", "ExtendedEditKey", false, true, false, null)); + this.Checkboxes.Add(new CheckBoxMeta(propWindow, "Use Ctrl+Shift+C/V as Copy/Paste", "InterceptCopyPaste", false, false, true, NativeMethods.WinConP.PKEY_Console_InterceptCopyPaste)); + + this.MoreInfoLink = propWindow.FindElementByName("new console features"); + } + + public override IEnumerable GetObjectsDisabledForV1Console() + { + return this.Checkboxes.Select(meta => meta.Box); + } + + public override IEnumerable GetObjectsUnaffectedByV1V2Switch() + { + return new AppiumWebElement[] { this.GlobalV1V2Box.Box, this.MoreInfoLink }; + } + + public override IEnumerable GetCheckboxesForVerification() + { + return this.Checkboxes; + } + + public override IEnumerable GetSlidersForVerification() + { + return new SliderMeta[0]; + } + } +} diff --git a/src/host/ft_uia/Elements/PropertiesDialog.cs b/src/host/ft_uia/Elements/PropertiesDialog.cs new file mode 100644 index 000000000..6f1f8cba0 --- /dev/null +++ b/src/host/ft_uia/Elements/PropertiesDialog.cs @@ -0,0 +1,130 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + + using OpenQA.Selenium.Appium; + + using Conhost.UIA.Tests.Common; + + public class PropertiesDialog : IDisposable + { + public enum CloseAction + { + OK, + Cancel + } + + public AppiumWebElement PropWindow { get; private set; } + public AppiumWebElement Tabs { get; private set; } + + private AppiumWebElement okButton; + private AppiumWebElement cancelButton; + + private CmdApp app; + + private bool isOpened; + + public PropertiesDialog(CmdApp app) + { + this.app = app; + } + + ~PropertiesDialog() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // if we're being disposed of, close the dialog. + if (this.isOpened) + { + this.ClosePropertiesDialog(CloseAction.Cancel); + } + } + } + + public void Open(OpenTarget target) + { + if (this.isOpened) + { + throw new InvalidOperationException("Can't open an already opened window."); + } + + this.OpenPropertiesDialog(this.app, target); + this.isOpened = true; + } + + public void Close(CloseAction action) + { + if (!this.isOpened) + { + throw new InvalidOperationException("Can't close an unopened window."); + } + + this.ClosePropertiesDialog(action); + this.isOpened = false; + } + + private void OpenPropertiesDialog(CmdApp app, OpenTarget target) + { + var titleBar = app.GetTitleBar(); + app.Session.Mouse.ContextClick(titleBar.Coordinates); + + Globals.WaitForTimeout(); + var contextMenu = app.Session.FindElementByClassName(Globals.PopupMenuClassId); + + AppiumWebElement propButton; + switch (target) + { + case OpenTarget.Specifics: + propButton = contextMenu.FindElementByName("Properties"); + break; + case OpenTarget.Defaults: + propButton = contextMenu.FindElementByName("Defaults"); + break; + default: + throw new NotImplementedException(AutoHelpers.FormatInvariant("Open Properties dialog doesn't yet support target type of '{0}'", target.ToString())); + } + + propButton.Click(); + + Globals.WaitForTimeout(); + + this.PropWindow = this.app.UIRoot.FindElementByClassName(Globals.DialogWindowClassId); + this.Tabs = this.PropWindow.FindElementByClassName("SysTabControl32"); + + okButton = this.PropWindow.FindElementByName("OK"); + cancelButton = this.PropWindow.FindElementByName("Cancel"); + } + + private void ClosePropertiesDialog(CloseAction action) + { + switch (action) + { + case CloseAction.OK: + okButton.Click(); + break; + case CloseAction.Cancel: + cancelButton.Click(); + break; + } + + Globals.WaitForTimeout(); + } + } +} diff --git a/src/host/ft_uia/Elements/TabBase.cs b/src/host/ft_uia/Elements/TabBase.cs new file mode 100644 index 000000000..f4a1a7f98 --- /dev/null +++ b/src/host/ft_uia/Elements/TabBase.cs @@ -0,0 +1,69 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating various tabs of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + + using Conhost.UIA.Tests.Common; + using OpenQA.Selenium.Appium; + + public abstract class TabBase : IDisposable + { + protected string tabName; + + protected PropertiesDialog propDialog; + + private TabBase() + { + + } + + public TabBase(PropertiesDialog propDialog, string tabName) + { + this.propDialog = propDialog; + this.tabName = tabName; + } + + ~TabBase() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Don't need to dispose of anything now, but this helps maintain the pattern used by other controls. + } + + public void NavigateToTab() + { + AutoHelpers.LogInvariant("Navigating to '{0}'", this.tabName); + var tab = this.propDialog.Tabs.FindElementByName(this.tabName); + + tab.Click(); + Globals.WaitForTimeout(); + + this.PopulateItemsOnNavigate(this.propDialog.PropWindow); + + } + + protected abstract void PopulateItemsOnNavigate(AppiumWebElement propWindow); + + public abstract IEnumerable GetObjectsDisabledForV1Console(); + public abstract IEnumerable GetObjectsUnaffectedByV1V2Switch(); + + public abstract IEnumerable GetCheckboxesForVerification(); + + public abstract IEnumerable GetSlidersForVerification(); + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/Tabs.cs b/src/host/ft_uia/Elements/Tabs.cs new file mode 100644 index 000000000..26e8c6393 --- /dev/null +++ b/src/host/ft_uia/Elements/Tabs.cs @@ -0,0 +1,110 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating all known tabs of the properties dialog. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Conhost.UIA.Tests.Common; + + public class Tabs : IDisposable + { + protected PropertiesDialog propDialog; + + protected List tabs; + + public Tabs(PropertiesDialog propDialog) + { + this.propDialog = propDialog; + + this.InitializeAllTabs(); + } + + private void InitializeAllTabs() + { + this.tabs = new List(); + + this.tabs.Add(new OptionsTab(this.propDialog)); + this.tabs.Add(new FontTab(this.propDialog)); + this.tabs.Add(new LayoutTab(this.propDialog)); + this.tabs.Add(new ColorsTab(this.propDialog)); + + } + + private Tabs() + { + + } + + ~Tabs() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Don't need to dispose of anything now, but this helps maintain the pattern used by other controls. + } + + + public IEnumerable AllTabs + { + get + { + return this.tabs; + } + } + + public enum GlobalState + { + ConsoleV1, + ConsoleV2 + } + + public void SetGlobalState(GlobalState state) + { + AutoHelpers.LogInvariant("Updating global state to '{0}'", state.ToString()); + OptionsTab tabWithGlobal = this.tabs.Single(tab => typeof(OptionsTab) == tab.GetType()) as OptionsTab; + tabWithGlobal.NavigateToTab(); + + switch (state) + { + case GlobalState.ConsoleV1: + tabWithGlobal.GlobalV1V2Box.Check(); + break; + case GlobalState.ConsoleV2: + tabWithGlobal.GlobalV1V2Box.Uncheck(); + break; + default: + throw new NotImplementedException(); + } + } + + public void SetWrapState(bool isWrapOn) + { + AutoHelpers.LogInvariant("Updating wrap state to '{0}'", isWrapOn.ToString()); + LayoutTab tabWithWrap = this.tabs.Single(tab => typeof(LayoutTab) == tab.GetType()) as LayoutTab; + tabWithWrap.NavigateToTab(); + + if (isWrapOn) + { + tabWithWrap.WrapTextCheckBox.Check(); + } + else + { + tabWithWrap.WrapTextCheckBox.Uncheck(); + } + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/ViewportArea.cs b/src/host/ft_uia/Elements/ViewportArea.cs new file mode 100644 index 000000000..2f659b8c6 --- /dev/null +++ b/src/host/ft_uia/Elements/ViewportArea.cs @@ -0,0 +1,239 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Wrapper and helper for instantiating and interacting with the main text region (viewport area) of the console. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests.Elements +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Linq; + using System.Runtime.InteropServices; + using System.Text; + + using OpenQA.Selenium.Appium; + + using WEX.Logging.Interop; + using WEX.TestExecution; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using OpenQA.Selenium; + + public class ViewportArea : IDisposable + { + private CmdApp app; + private Point clientTopLeft; + private Size sizeFont; + + private ViewportStates state; + + public enum ViewportStates + { + Normal, + Mark, // keyboard selection + Select, // mouse selection + Scroll + } + + public ViewportArea(CmdApp app) + { + this.app = app; + + this.state = ViewportStates.Normal; + + this.InitializeFont(); + this.InitializeWindow(); + } + + ~ViewportArea() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Don't need to dispose of anything now, but this helps maintain the pattern used by other controls. + } + + private void InitializeFont() + { + AutoHelpers.LogInvariant("Initializing font data for viewport area..."); + this.sizeFont = new Size(); + + IntPtr hCon = app.GetStdOutHandle(); + Verify.IsNotNull(hCon, "Check that we obtained the console output handle."); + + WinCon.CONSOLE_FONT_INFO cfi = new WinCon.CONSOLE_FONT_INFO(); + NativeMethods.Win32BoolHelper(WinCon.GetCurrentConsoleFont(hCon, false, out cfi), "Attempt to get current console font."); + + this.sizeFont.Width = cfi.dwFontSize.X; + this.sizeFont.Height = cfi.dwFontSize.Y; + AutoHelpers.LogInvariant("Font size is X:{0} Y:{1}", this.sizeFont.Width, this.sizeFont.Height); + } + + private void InitializeWindow() + { + AutoHelpers.LogInvariant("Initializing window data for viewport area..."); + + IntPtr hWnd = app.GetWindowHandle(); + + User32.RECT lpRect; + User32.GetClientRect(hWnd, out lpRect); + + int style = User32.GetWindowLong(hWnd, User32.GWL_STYLE); + int exStyle = User32.GetWindowLong(hWnd, User32.GWL_EXSTYLE); + + Verify.IsTrue(User32.AdjustWindowRectEx(ref lpRect, style, false, exStyle)); + + this.clientTopLeft = new Point(); + this.clientTopLeft.X = Math.Abs(lpRect.left); + this.clientTopLeft.Y = Math.Abs(lpRect.top); + AutoHelpers.LogInvariant("Top left corner of client area is at X:{0} Y:{1}", this.clientTopLeft.X, this.clientTopLeft.Y); + } + + public void ExitModes() + { + app.UIRoot.SendKeys(Keys.Escape); + this.state = ViewportStates.Normal; + } + + public void EnterMode(ViewportStates state) + { + if (state == ViewportStates.Normal) + { + ExitModes(); + return; + } + + var titleBar = app.UIRoot.FindElementByAccessibilityId("TitleBar"); + app.Session.Mouse.ContextClick(titleBar.Coordinates); + + Globals.WaitForTimeout(); + var contextMenu = app.Session.FindElementByClassName(Globals.PopupMenuClassId); + + var editButton = contextMenu.FindElementByName("Edit"); + + editButton.Click(); + Globals.WaitForTimeout(); + + Globals.WaitForTimeout(); + + AppiumWebElement subMenuButton; + switch (state) + { + case ViewportStates.Mark: + subMenuButton = app.Session.FindElementByName("Mark"); + break; + default: + throw new NotImplementedException(AutoHelpers.FormatInvariant("Set Mode doesn't yet support type of '{0}'", state.ToString())); + } + + subMenuButton.Click(); + Globals.WaitForTimeout(); + + this.state = state; + } + + + // Accepts Point in characters. Will convert to pixels and move to the right location relative to this viewport. + public void MouseMove(Point pt) + { + Log.Comment($"Character position {pt.X}, {pt.Y}"); + + Point modPoint = pt; + ConvertCharacterOffsetToPixelPosition(ref modPoint); + + Log.Comment($"Pixel position {modPoint.X}, {modPoint.Y}"); + + app.Session.Mouse.MouseMove(app.UIRoot.Coordinates, modPoint.X, modPoint.Y); + } + + public void MouseDown() + { + app.Session.Mouse.MouseDown(null); + } + + public void MouseUp() + { + app.Session.Mouse.MouseUp(null); + } + + private void ConvertCharacterOffsetToPixelPosition(ref Point pt) + { + // Scale by pixel count per character + pt.X *= this.sizeFont.Width; + pt.Y *= this.sizeFont.Height; + + // Move it to center of character + pt.X += this.sizeFont.Width / 2; + pt.Y += this.sizeFont.Height / 2; + + // Adjust to the top left corner of the client rectangle. + pt.X += this.clientTopLeft.X; + pt.Y += this.clientTopLeft.Y; + } + + public WinCon.CHAR_INFO GetCharInfoAt(IntPtr handle, Point pt) + { + Size size = new Size(1, 1); + Rectangle rect = new Rectangle(pt, size); + + WinCon.CHAR_INFO[,] data = GetCharInfoInRectangle(handle, rect); + + return data[0, 0]; + } + + public WinCon.CHAR_INFO[,] GetCharInfoInRectangle(IntPtr handle, Rectangle rect) + { + WinCon.SMALL_RECT readRectangle = new WinCon.SMALL_RECT(); + readRectangle.Top = (short)rect.Top; + readRectangle.Bottom = (short)(rect.Bottom - 1); + readRectangle.Left = (short)rect.Left; + readRectangle.Right = (short)(rect.Right - 1); + + WinCon.COORD dataBufferSize = new WinCon.COORD(); + dataBufferSize.X = (short)rect.Width; + dataBufferSize.Y = (short)rect.Height; + + WinCon.COORD dataBufferPos = new WinCon.COORD(); + dataBufferPos.X = 0; + dataBufferPos.Y = 0; + + WinCon.CHAR_INFO[,] data = new WinCon.CHAR_INFO[dataBufferSize.Y, dataBufferSize.X]; + + NativeMethods.Win32BoolHelper(WinCon.ReadConsoleOutput(handle, data, dataBufferSize, dataBufferPos, ref readRectangle), string.Format("Attempting to read rectangle (L: {0}, T: {1}, R: {2}, B: {3}) from output buffer.", readRectangle.Left, readRectangle.Top, readRectangle.Right, readRectangle.Bottom)); + + return data; + } + + public IEnumerable GetLinesInRectangle(IntPtr handle, Rectangle rect) + { + WinCon.CHAR_INFO[,] data = GetCharInfoInRectangle(handle, rect); + List lines = new List(); + + for (int row = 0; row < data.GetLength(0); row++) + { + StringBuilder builder = new StringBuilder(); + + for (int col = 0; col < data.GetLength(1); col++) + { + char z = data[row, col].UnicodeChar; + builder.Append(z); + } + + lines.Add(builder.ToString()); + } + + return lines; + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/Elements/WinEventSystem.cs b/src/host/ft_uia/Elements/WinEventSystem.cs new file mode 100644 index 000000000..a51bd32b7 --- /dev/null +++ b/src/host/ft_uia/Elements/WinEventSystem.cs @@ -0,0 +1,177 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// Helper and wrapper for attaching a WinEvent framework around a console application. +//---------------------------------------------------------------------------------------------------------------------- + +namespace Conhost.UIA.Tests.Elements +{ + using Common.NativeMethods; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using WEX.Logging.Interop; + using WEX.TestExecution; + + public interface IWinEventCallbacks + { + void CaretSelection(int x, int y); + void CaretVisible(int x, int y); + void UpdateRegion(int left, int top, int right, int bottom); + void UpdateSimple(int x, int y, int character, int attribute); + void UpdateScroll(int deltaX, int deltaY); + void Layout(); + void StartApplication(int processId, int childId); + void EndApplication(int processId, int childId); + } + + public class WinEventSystem : IDisposable + { + public WinEventSystem(IWinEventCallbacks callbacks, uint pid) + { + this.AttachWinEventHook(callbacks, pid); + } + + ~WinEventSystem() + { + this.Dispose(false); + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed = false; + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + DetachWinEventHook(); + this.isDisposed = true; + } + } + + bool messagePumpDone = false; + Task messagePumpTask = null; + + private void AttachWinEventHook(IWinEventCallbacks callbacks, uint pid) + { + if (messagePumpTask == null) + { + messagePumpDone = false; + messagePumpTask = Task.Run(() => MessagePump(callbacks, pid)); + } + } + + private void DetachWinEventHook() + { + if (messagePumpTask != null) + { + messagePumpDone = true; + messagePumpTask.Wait(); + + messagePumpTask = null; + } + } + + // This must be public or marshalling cannot call back to it. + public class WinEventCallback + { + private uint pid; + private IWinEventCallbacks callbacks; + + public WinEventCallback(IWinEventCallbacks callbacks, uint pidOfInterest) + { + this.pid = pidOfInterest; + this.callbacks = callbacks; + } + + public void WinEventProc(IntPtr hWinEventHook, User32.WinEventId eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + { + uint dwProcessId; + User32.GetWindowThreadProcessId(hwnd, out dwProcessId); + + if (dwProcessId != pid) + { + return; + } + + switch (eventType) + { + case User32.WinEventId.EVENT_CONSOLE_CARET: + switch (idObject) + { + case 1: + callbacks.CaretSelection(idChild.LoWord(), idChild.HiWord()); + break; + case 2: + callbacks.CaretVisible(idChild.LoWord(), idChild.HiWord()); + break; + default: + Verify.Fail($" idObject: {idObject} - INVALID VALUE!!- "); + break; + } + break; + case User32.WinEventId.EVENT_CONSOLE_UPDATE_REGION: + callbacks.UpdateRegion(idObject.LoWord(), idObject.HiWord(), idChild.LoWord(), idChild.HiWord()); + break; + case User32.WinEventId.EVENT_CONSOLE_UPDATE_SIMPLE: + callbacks.UpdateSimple(idObject.LoWord(), idObject.HiWord(), idChild.LoWord(), idChild.HiWord()); + break; + case User32.WinEventId.EVENT_CONSOLE_UPDATE_SCROLL: + callbacks.UpdateScroll(idObject, idChild); + break; + case User32.WinEventId.EVENT_CONSOLE_LAYOUT: + callbacks.Layout(); + break; + case User32.WinEventId.EVENT_CONSOLE_START_APPLICATION: + callbacks.StartApplication(idObject, idChild); + break; + case User32.WinEventId.EVENT_CONSOLE_END_APPLICATION: + callbacks.EndApplication(idObject, idChild); + break; + } + } + } + + private void MessagePump(IWinEventCallbacks callbacks, uint pid) + { + Log.Comment("Accessibility message pump thread started"); + + WinEventCallback callback = new WinEventCallback(callbacks, pid); + + IntPtr hWinEventHook = User32.SetWinEventHook( + User32.WinEventId.EVENT_CONSOLE_CARET, + User32.WinEventId.EVENT_CONSOLE_END_APPLICATION, + IntPtr.Zero, // Use our own module + new User32.WinEventDelegate(callback.WinEventProc), // Our callback function + 0, // All processes + 0, // All threads + User32.WinEventFlags.WINEVENT_SKIPOWNPROCESS | User32.WinEventFlags.WINEVENT_SKIPOWNTHREAD); + + NativeMethods.Win32NullHelper(hWinEventHook, "Registering accessibility event hook."); + + Log.Comment("Entering accessibility pump loop."); + while (!messagePumpDone) + { + User32.MSG msg; + if (User32.PeekMessage(out msg, IntPtr.Zero, 0, 0, User32.PM.PM_REMOVE)) + { + User32.DispatchMessage(ref msg); + } + + Thread.Sleep(200); + } + Log.Comment("Exiting accessibility pump loop."); + + NativeMethods.Win32BoolHelper(User32.UnhookWinEvent(hWinEventHook), "Unregistering accessibility event hook."); + Log.Comment("Accessibility message pump thread ended"); + } + } +} diff --git a/src/host/ft_uia/ExperimentalTabTests.cs b/src/host/ft_uia/ExperimentalTabTests.cs new file mode 100644 index 000000000..f0f9c9c39 --- /dev/null +++ b/src/host/ft_uia/ExperimentalTabTests.cs @@ -0,0 +1,501 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the Experimental Tab control in the Console Properties dialog window. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + + using Microsoft.Win32; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using OpenQA.Selenium.Appium; + + [TestClass] + public class ExperimentalTabTests + { + public TestContext TestContext { get; set; } + + public const int timeout = Globals.Timeout; + + [TestMethod] + public void CheckExperimentalDisableState() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); // manipulating the global v1/v2 state can affect the registry so back it up. + + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (PropertiesDialog properties = new PropertiesDialog(app)) + { + properties.Open(OpenTarget.Defaults); + + using (Tabs tabs = new Tabs(properties)) + { + // check everything stays enabled when global is on. + AutoHelpers.LogInvariant("Check that items are all enabled when global is enabled."); + tabs.SetGlobalState(Tabs.GlobalState.ConsoleV2); + + // iterate through each tab + AutoHelpers.LogInvariant("Checking elements on all tabs."); + foreach (TabBase tab in tabs.AllTabs) + { + tab.NavigateToTab(); + + IEnumerable itemsUnaffected = tab.GetObjectsUnaffectedByV1V2Switch(); + IEnumerable itemsThatDisable = tab.GetObjectsDisabledForV1Console(); + + foreach (AppiumWebElement obj in itemsThatDisable.Concat(itemsUnaffected)) + { + Verify.IsTrue(obj.Enabled, AutoHelpers.FormatInvariant("Option: {0}", obj.Text)); + } + } + + // check that relevant boxes are disabled when global is off. + AutoHelpers.LogInvariant("Check that necessary items are disabled when global is disabled."); + tabs.SetGlobalState(Tabs.GlobalState.ConsoleV1); + + foreach (TabBase tab in tabs.AllTabs) + { + tab.NavigateToTab(); + + IEnumerable itemsUnaffected = tab.GetObjectsUnaffectedByV1V2Switch(); + IEnumerable itemsThatDisable = tab.GetObjectsDisabledForV1Console(); + + foreach (AppiumWebElement obj in itemsThatDisable) + { + Verify.IsFalse(obj.Enabled, AutoHelpers.FormatInvariant("Option: {0}", obj.Text)); + } + foreach (AppiumWebElement obj in itemsUnaffected) + { + Verify.IsTrue(obj.Enabled, AutoHelpers.FormatInvariant("Option: {0}", obj.Text)); + } + } + } + } + } + } + } + + [TestMethod] + public void CheckRegistryWritebacks() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + this.CheckRegistryWritebacks(reg, app, OpenTarget.Defaults); + this.CheckRegistryWritebacks(reg, app, OpenTarget.Specifics); + } + } + } + + [TestMethod] + public void CheckShortcutWritebacks() + { + using (RegistryHelper reg = new RegistryHelper()) + { + // The global state changes can still impact the registry, so back up the registry anyway despite this being the shortcut test. + reg.BackupRegistry(); + + using (ShortcutHelper shortcut = new ShortcutHelper()) + { + shortcut.CreateTempCmdShortcut(); + + using (CmdApp app = new CmdApp(CreateType.ShortcutFile, TestContext, shortcut.ShortcutPath)) + { + this.CheckShortcutWritebacks(shortcut, app, OpenTarget.Specifics); + } + } + } + } + + private void CheckRegistryWritebacks(RegistryHelper reg, CmdApp app, OpenTarget target) + { + this.CheckWritebacks(reg, null, app, target); + } + + private void CheckShortcutWritebacks(ShortcutHelper shortcut, CmdApp app, OpenTarget target) + { + this.CheckWritebacks(null, shortcut, app, target); + } + + private void CheckWritebacks(RegistryHelper reg, ShortcutHelper shortcut, CmdApp app, OpenTarget target) + { + // either registry or shortcut are null + if ((reg == null && shortcut == null) || (reg != null && shortcut != null)) + { + throw new NotSupportedException("Must leave either registry or shortcut null. And must supply one of the two."); + } + + bool isRegMode = reg != null; // true is reg mode, false is shortcut mode + + string modeName = isRegMode ? "registry" : "shortcut"; + + AutoHelpers.LogInvariant("Beginning {0} writeback tests for {1}", modeName, target.ToString()); + + using (PropertiesDialog props = new PropertiesDialog(app)) + { + // STEP 1: VERIFY EVERYTHING SAVES IN AN ON/MAX STATE + AutoHelpers.LogInvariant("Open dialog and check boxes."); + props.Open(target); + + using (Tabs tabs = new Tabs(props)) + { + // Set V2 on. + tabs.SetGlobalState(Tabs.GlobalState.ConsoleV2); + + AutoHelpers.LogInvariant("Toggling elements on all tabs."); + foreach (TabBase tab in tabs.AllTabs) + { + tab.NavigateToTab(); + + foreach (CheckBoxMeta obj in tab.GetCheckboxesForVerification()) + { + obj.Check(); + } + + foreach (SliderMeta obj in tab.GetSlidersForVerification()) + { + // adjust slider to the maximum + obj.SetToMaximum(); + } + } + + AutoHelpers.LogInvariant("Hit OK to save."); + props.Close(PropertiesDialog.CloseAction.OK); + + AutoHelpers.LogInvariant("Verify values changed as appropriate."); + CheckWritebacksVerifyValues(isRegMode, reg, shortcut, target, tabs, SliderMeta.ExpectedPosition.Maximum, false, Tabs.GlobalState.ConsoleV2); + } + + // STEP 2: VERIFY EVERYTHING SAVES IN AN OFF/MIN STATE + AutoHelpers.LogInvariant("Open dialog and uncheck boxes."); + props.Open(target); + + using (Tabs tabs = new Tabs(props)) + { + AutoHelpers.LogInvariant("Toggling elements on all tabs."); + foreach (TabBase tab in tabs.AllTabs) + { + tab.NavigateToTab(); + + foreach (SliderMeta slider in tab.GetSlidersForVerification()) + { + // adjust slider to the minimum + slider.SetToMinimum(); + } + + foreach (CheckBoxMeta obj in tab.GetCheckboxesForVerification()) + { + obj.Uncheck(); + } + } + + tabs.SetGlobalState(Tabs.GlobalState.ConsoleV1); + + AutoHelpers.LogInvariant("Hit OK to save."); + props.Close(PropertiesDialog.CloseAction.OK); + + AutoHelpers.LogInvariant("Verify values changed as appropriate."); + CheckWritebacksVerifyValues(isRegMode, reg, shortcut, target, tabs, SliderMeta.ExpectedPosition.Minimum, true, Tabs.GlobalState.ConsoleV1); + } + + // STEP 3: VERIFY CANCEL DOES NOT SAVE + AutoHelpers.LogInvariant("Open dialog and check boxes."); + props.Open(target); + + using (Tabs tabs = new Tabs(props)) + { + tabs.SetGlobalState(Tabs.GlobalState.ConsoleV2); + + AutoHelpers.LogInvariant("Toggling elements on all tabs."); + foreach (TabBase tab in tabs.AllTabs) + { + tab.NavigateToTab(); + + foreach (CheckBoxMeta obj in tab.GetCheckboxesForVerification()) + { + obj.Check(); + } + + foreach (SliderMeta obj in tab.GetSlidersForVerification()) + { + // adjust slider to the maximum + obj.SetToMaximum(); + } + } + + AutoHelpers.LogInvariant("Hit cancel to not save."); + props.Close(PropertiesDialog.CloseAction.Cancel); + + AutoHelpers.LogInvariant("Verify values did not change."); + CheckWritebacksVerifyValues(isRegMode, reg, shortcut, target, tabs, SliderMeta.ExpectedPosition.Minimum, true, Tabs.GlobalState.ConsoleV1); + } + } + } + + private void CheckWritebacksVerifyValues(bool isRegMode, RegistryHelper reg, ShortcutHelper shortcut, OpenTarget target, Tabs tabs, SliderMeta.ExpectedPosition sliderExpected, bool checkboxValue, Tabs.GlobalState consoleVersion) + { + foreach (TabBase tab in tabs.AllTabs) + { + CheckWritebacksVerifyValues(isRegMode, reg, shortcut, target, tab, sliderExpected, checkboxValue, consoleVersion); + } + } + + private void CheckWritebacksVerifyValues(bool isRegMode, RegistryHelper reg, ShortcutHelper shortcut, OpenTarget target, TabBase tab, SliderMeta.ExpectedPosition sliderExpected, bool checkboxValue, Tabs.GlobalState consoleVersion) + { + if (isRegMode) + { + VerifyBoxes(tab, reg, checkboxValue, target, consoleVersion); + VerifySliders(tab, reg, sliderExpected, target, consoleVersion); + } + else + { + // Have to wait for shortcut to get written. + // There isn't really an event to know when this occurs, so just wait. + Globals.WaitForTimeout(); + + VerifyBoxes(tab, shortcut, checkboxValue, consoleVersion); + VerifySliders(tab, shortcut, sliderExpected, consoleVersion); + } + } + + private void VerifyBoxes(TabBase tab, RegistryHelper reg, bool inverse, OpenTarget target, Tabs.GlobalState consoleVersion) + { + // get the key for the current target + RegistryKey consoleKey = reg.GetMatchingKey(target); + + // hold the parent console key in case we need to look things up for specifics. + RegistryKey parentConsoleKey = reg.GetMatchingKey(OpenTarget.Defaults); + + // include the global checkbox in the set for verification purposes + IEnumerable boxes = tab.GetCheckboxesForVerification(); + + AutoHelpers.LogInvariant("Testing target: {0} in inverse {1} mode", target.ToString(), inverse.ToString()); + + // If we're opened as specifics, remove all global only boxes from the test set + if (target == OpenTarget.Specifics) + { + AutoHelpers.LogInvariant("Reducing"); + boxes = boxes.Where(box => !box.IsGlobalOnly); + } + + foreach (CheckBoxMeta meta in boxes) + { + int? storedValue = consoleKey.GetValue(meta.ValueName) as int?; + + string boxName = AutoHelpers.FormatInvariant("Box: {0}", meta.ValueName); + + // if we're in specifics mode, we might have a null and if so, we check the parent value + if (target == OpenTarget.Specifics) + { + if (storedValue == null) + { + AutoHelpers.LogInvariant("Specific setting missing. Checking defaults."); + storedValue = parentConsoleKey.GetValue(meta.ValueName) as int?; + } + } + else + { + Verify.IsNotNull(storedValue, boxName); + } + + if (consoleVersion == Tabs.GlobalState.ConsoleV1 && meta.IsV2Property) + { + AutoHelpers.LogInvariant("Skipping validation of v2 property {0} after switching to v1 console.", meta.ValueName); + } + else + { + // A box can be inverse if checking it means false in the registry. + // This method can be inverse if we're turning off the boxes and expecting it to be on. + // Therefore, a box will be false if it's checked and supposed to be off. Or if it's unchecked and supposed to be on. + if ((meta.IsInverse && !inverse) || (!meta.IsInverse && inverse)) + { + Verify.IsFalse(storedValue.Value.DwordToBool(), boxName); + } + else + { + Verify.IsTrue(storedValue.Value.DwordToBool(), boxName); + } + } + } + } + + private void VerifyBoxes(TabBase tab, ShortcutHelper shortcut, bool inverse, Tabs.GlobalState consoleVersion) + { + IEnumerable boxes = tab.GetCheckboxesForVerification(); + + // collect up properties that we need to retrieve keys for + IEnumerable propBoxes = boxes.Where(box => box.PropKey != null); + IEnumerable keys = propBoxes.Select(box => box.PropKey).Cast(); + + // fetch data for keys + IDictionary propertyData = shortcut.GetFromPropertyStore(keys); + + // enumerate each box and validate the data + foreach (CheckBoxMeta meta in propBoxes) + { + string boxName = AutoHelpers.FormatInvariant("Box: {0}", meta.ValueName); + + Wtypes.PROPERTYKEY key = (Wtypes.PROPERTYKEY)meta.PropKey; + + bool? value = (bool?)propertyData[key]; + + Verify.IsNotNull(value, boxName); + + if (consoleVersion == Tabs.GlobalState.ConsoleV1 && meta.IsV2Property) + { + AutoHelpers.LogInvariant("Skipping validation of v2 property {0} after switching to v1 console.", meta.ValueName); + } + else + { + // A box can be inverse if checking it means false in the registry. + // This method can be inverse if we're turning off the boxes and expecting it to be on. + // Therefore, a box will be false if it's checked and supposed to be off. Or if it's unchecked and supposed to be on. + if ((meta.IsInverse && !inverse) || (!meta.IsInverse && inverse)) + { + Verify.IsFalse(value.Value, boxName); + } + else + { + Verify.IsTrue(value.Value, boxName); + } + } + } + } + + private void VerifySliders(TabBase tab, RegistryHelper reg, SliderMeta.ExpectedPosition expected, OpenTarget target, Tabs.GlobalState consoleVersion) + { + // get the key for the current target + RegistryKey consoleKey = reg.GetMatchingKey(target); + + // hold the parent console key in case we need to look things up for specifics. + RegistryKey parentConsoleKey = reg.GetMatchingKey(OpenTarget.Defaults); + + IEnumerable sliders = tab.GetSlidersForVerification(); + + foreach (SliderMeta meta in sliders) + { + int? storedValue = consoleKey.GetValue(meta.ValueName) as int?; + + string sliderName = AutoHelpers.FormatInvariant("Slider: {0}", meta.ValueName); + + if (target == OpenTarget.Specifics) + { + if (storedValue == null) + { + AutoHelpers.LogInvariant("Specific setting missing. Checking defaults."); + storedValue = parentConsoleKey.GetValue(meta.ValueName) as int?; + } + } + else + { + Verify.IsNotNull(storedValue, sliderName); + } + + int transparency = 0; + + switch (expected) + { + case SliderMeta.ExpectedPosition.Maximum: + transparency = meta.GetMaximum(); + break; + case SliderMeta.ExpectedPosition.Minimum: + transparency = meta.GetMinimum(); + break; + default: + throw new NotImplementedException(); + } + + if (consoleVersion == Tabs.GlobalState.ConsoleV1 && meta.IsV2Property) + { + AutoHelpers.LogInvariant("Skipping validation of v2 property {0} after switching to v1 console.", meta.ValueName); + } + else + { + Verify.AreEqual(storedValue.Value, RescaleSlider(transparency), sliderName); + } + } + } + + private void VerifySliders(TabBase tab, ShortcutHelper shortcut, SliderMeta.ExpectedPosition expected, Tabs.GlobalState consoleVersion) + { + IEnumerable sliders = tab.GetSlidersForVerification(); + + // collect up properties that we need to retrieve keys for + IEnumerable propSliders = sliders.Where(slider => slider.PropKey != null); + IEnumerable keys = propSliders.Select(slider => slider.PropKey).Cast(); + + // fetch data for keys + IDictionary propertyData = shortcut.GetFromPropertyStore(keys); + + // enumerate each slider and validate data + foreach (SliderMeta meta in sliders) + { + string sliderName = AutoHelpers.FormatInvariant("Slider: {0}", meta.ValueName); + + Wtypes.PROPERTYKEY key = (Wtypes.PROPERTYKEY)meta.PropKey; + + short value = (short)propertyData[key]; + + int transparency = 0; + + switch (expected) + { + case SliderMeta.ExpectedPosition.Maximum: + transparency = meta.GetMaximum(); + break; + case SliderMeta.ExpectedPosition.Minimum: + transparency = meta.GetMinimum(); + break; + default: + throw new NotImplementedException(); + } + + if (consoleVersion == Tabs.GlobalState.ConsoleV1 && meta.IsV2Property) + { + AutoHelpers.LogInvariant("Skipping validation of v2 property {0} after switching to v1 console.", meta.ValueName); + } + else + { + Verify.AreEqual(value, RescaleSlider(transparency), sliderName); + } + } + } + + private short RescaleSlider(int inputValue) + { + // we go on a scale from 0x4D to 0xFF. + int minValue = 0x4D; + int maxValue = 0xFF; + + int valueRange = maxValue - minValue; + + double scaleFactor = (double)inputValue / 100.0; + + short finalValue = (short)((valueRange * scaleFactor) + minValue); + + return finalValue; + } + } +} diff --git a/src/host/ft_uia/Host.Tests.UIA.csproj b/src/host/ft_uia/Host.Tests.UIA.csproj new file mode 100644 index 000000000..3c4bf1d3e --- /dev/null +++ b/src/host/ft_uia/Host.Tests.UIA.csproj @@ -0,0 +1,152 @@ + + + + {C17E1BF3-9D34-4779-9458-A8EF98CC5662} + Library + Properties + Conhost.UIA.Tests + Conhost.UIA.Tests + v4.5 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + $(SolutionDir)\bin\$(Platform)\$(Configuration) + prompt + 4 + + + + + ARM64 + + + x64 + + + x86 + + + true + full + false + DEBUG;TRACE + + + pdbonly + true + TRACE + + + + ..\..\..\packages\Appium.WebDriver.3.0.0.2\lib\net45\appium-dotnet-driver.dll + + + ..\..\..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll + + + ..\..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + + + ..\..\..\packages\Taef.Redist.Wlk.10.30.180808002\lib\net45\TE.Managed.dll + + + + + ..\..\..\packages\Selenium.WebDriver.3.5.0\lib\net40\WebDriver.dll + + + ..\..\..\packages\Selenium.Support.3.5.0\lib\net40\WebDriver.Support.dll + + + ..\..\..\packages\Taef.Redist.Wlk.10.30.180808002\lib\net45\Wex.Common.Managed.dll + + + ..\..\..\packages\Taef.Redist.Wlk.10.30.180808002\lib\net45\Wex.Logger.Interop.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + copy $(SolutionDir)\dep\WinAppDriver\* $(OutDir)\ + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/host/ft_uia/Init.cs b/src/host/ft_uia/Init.cs new file mode 100644 index 000000000..d56ca7e70 --- /dev/null +++ b/src/host/ft_uia/Init.cs @@ -0,0 +1,52 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +//---------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; +using WEX.Logging.Interop; +using WEX.TestExecution; +using WEX.TestExecution.Markup; + +namespace Host.Tests.UIA +{ + [TestClass] + class Init + { + static Process appDriver; + + [AssemblyInitialize] + public static void SetupAll(TestContext context) + { + Log.Comment("Searching for WinAppDriver in the same directory where this test was launched from..."); + string winAppDriver = Path.Combine(context.TestDeploymentDir, "WinAppDriver.exe"); + + Log.Comment($"Attempting to launch WinAppDriver at: {winAppDriver}"); + Log.Comment($"Working directory: {Environment.CurrentDirectory}"); + + appDriver = Process.Start(winAppDriver); + } + + [AssemblyCleanup] + public static void CleanupAll() + { + try + { + appDriver.Kill(); + } + catch + { + + } + } + } +} diff --git a/src/host/ft_uia/InputBufferTests.cs b/src/host/ft_uia/InputBufferTests.cs new file mode 100644 index 000000000..27120058a --- /dev/null +++ b/src/host/ft_uia/InputBufferTests.cs @@ -0,0 +1,93 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for input buffer. +//---------------------------------------------------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; +using System.IO; + +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using WEX.Logging.Interop; + +using Conhost.UIA.Tests.Common; +using Conhost.UIA.Tests.Common.NativeMethods; +using Conhost.UIA.Tests.Elements; +using OpenQA.Selenium; + + +namespace Conhost.UIA.Tests +{ + [TestClass] + class InputBufferTests + { + public TestContext TestContext { get; set; } + + private bool IsProgramInPath(string name) + { + string[] pathDirs = Environment.GetEnvironmentVariable("PATH").Split(';'); + foreach (string path in pathDirs) + { + string joined = Path.Combine(path, name); + if (File.Exists(joined)) + { + return true; + } + } + return false; + } + + [TestMethod] + public void VerifyVimInput() + { + if (!IsProgramInPath("vim.exe")) + { + Log.Comment("vim can't be found in path, skipping test."); + return; + } + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + IntPtr hConsole = app.GetStdOutHandle(); + string testText = "hello world"; + Verify.IsNotNull(hConsole, "ensure the stdout handle is valid."); + // start up vim + app.UIRoot.SendKeys("vim"); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys(Keys.Enter); + // go to insert mode + app.UIRoot.SendKeys("i"); + // write some text + app.UIRoot.SendKeys(testText); + Globals.WaitForTimeout(); + // make sure text showed up in the output + Rectangle rect = new Rectangle(0, 0, 20, 20); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + bool foundText = false; + foreach (string line in text) + { + if (line.Contains(testText)) + { + foundText = true; + break; + } + } + Verify.IsTrue(foundText); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/host/ft_uia/KeyPressTests.cs b/src/host/ft_uia/KeyPressTests.cs new file mode 100644 index 000000000..5d7ec72b4 --- /dev/null +++ b/src/host/ft_uia/KeyPressTests.cs @@ -0,0 +1,230 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the certain key presses. +//---------------------------------------------------------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; + +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using WEX.Logging.Interop; + +using Conhost.UIA.Tests.Common; +using Conhost.UIA.Tests.Common.NativeMethods; +using Conhost.UIA.Tests.Elements; +using OpenQA.Selenium; + + +namespace Conhost.UIA.Tests +{ + [TestClass] + class KeyPressTests + { + public TestContext TestContext { get; set; } + + [TestMethod] + public void VerifyCtrlKeysBash() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the STDOUT handle is valid."); + // start up bash, run cat, type some keys with ctrl held down + app.UIRoot.SendKeys("bash"); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys("cat"); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys(Keys.Control + "ahz" + Keys.Control); + Globals.WaitForTimeout(); + // make sure "^A^H^Z" showed up in the output + Rectangle rect = new Rectangle(0, 0, 10, 10); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + bool foundCtrlChars = false; + foreach (string line in text) + { + if (line.Contains("^A^H^Z")) + { + foundCtrlChars = true; + break; + } + } + Verify.IsTrue(foundCtrlChars); + } + } + } + } + + [TestMethod] + public void VerifyCtrlCBash() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the STDOUT handle is valid."); + // start up bash, run cat, type ctrl+c + app.UIRoot.SendKeys("bash"); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys("cat"); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys(Keys.Control + "c" + Keys.Control); + Globals.WaitForTimeout(); + // make sure "^C" showed up in the output + Rectangle rect = new Rectangle(0, 0, 10, 10); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + bool foundCtrlC = false; + foreach (string line in text) + { + if (line.Contains("^C")) + { + foundCtrlC = true; + break; + } + } + Verify.IsTrue(foundCtrlC); + } + } + } + } + + [TestMethod] + public void VerifyCtrlZCmd() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the handle is valid."); + // get cursor location + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenBufferInfo = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref screenBufferInfo); + // send ^Z + app.UIRoot.SendKeys(Keys.Control + "z" + Keys.Control); + Globals.WaitForTimeout(); + // test that "^Z" exists on the screen + Rectangle rect = new Rectangle(0, 0, 200, 20); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + bool foundCtrlZ = false; + foreach (string line in text) + { + if (line.Contains("^Z")) + { + foundCtrlZ = true; + break; + } + } + Verify.IsTrue(foundCtrlZ); + } + } + } + } + + [TestMethod] + public void VerifyCtrlHCmd() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + string testText = "1234blah5678"; + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the handle is valid."); + // get cursor location + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX screenBufferInfo = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref screenBufferInfo); + // send some text and a ^H to remove the last character + app.UIRoot.SendKeys(testText + Keys.Control + "h" + Keys.Control); + Globals.WaitForTimeout(); + // test that we're missing the last character of testText on the line + Rectangle rect = new Rectangle(0, 0, 200, 20); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + bool foundCtrlH = false; + foreach (string line in text) + { + if (line.Contains(testText.Substring(0, testText.Length - 1)) && !line.Contains(testText)) + { + foundCtrlH = true; + break; + } + } + Verify.IsTrue(foundCtrlH); + } + } + } + } + + [TestMethod] + public void VerifyCtrlCCmd() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the handle is valid."); + Globals.WaitForTimeout(); + // send ctrl-c sequence + const int keypressCount = 10; + for (int i = 0; i < keypressCount; ++i) + { + app.UIRoot.SendKeys(Keys.Control + "c" + Keys.Control); + } + Globals.WaitForTimeout(); + // fetch the text + Rectangle rect = new Rectangle(0, 0, 50, 50); + IEnumerable text = area.GetLinesInRectangle(hConsole, rect); + // filter out the blank lines + List possiblePromptLines = new List(); + for (int i = 0; i < text.Count(); ++i) + { + string line = text.ElementAt(i); + line.Trim(' '); + if (!line.Equals("")) + { + possiblePromptLines.Add(line); + } + } + // make sure that the prompt line shows up for each ^C key press + Verify.IsTrue(possiblePromptLines.Count() >= keypressCount); + possiblePromptLines.Reverse(); + for (int i = 0; i < keypressCount; ++i) + { + Verify.AreEqual(possiblePromptLines[0], possiblePromptLines[1]); + possiblePromptLines.RemoveAt(0); + } + } + } + } + } + } +} diff --git a/src/host/ft_uia/MiscTests.cs b/src/host/ft_uia/MiscTests.cs new file mode 100644 index 000000000..0cd650754 --- /dev/null +++ b/src/host/ft_uia/MiscTests.cs @@ -0,0 +1,93 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Miscellaneous UI Automation tests (like things from bugs) +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Drawing; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + + using Microsoft.Win32; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using OpenQA.Selenium.Appium; + + [TestClass] + public class MiscTests + { + public TestContext TestContext { get; set; } + + [TestMethod] + public void DotNetSetWindowPosition() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + VersionSelector.SetConsoleVersion(reg, ConsoleVersion.V2); + + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + // maximize the window + AppiumWebElement titleBar = app.GetTitleBar(); + app.Actions.DoubleClick(titleBar); + + // wait for double click action to react. + Globals.WaitForTimeout(); + + // do the thing that makes it upset. + // MSFT:2490828 called Console.SetWindowPosition, so let's just copy the .NET source for that here... + // see: http://referencesource.microsoft.com/#mscorlib/system/console.cs,fcb364a853d81c57 + SetWindowPosition(0, 0); + } + } + } + + // Adapted from .NET source code... + // See: http://referencesource.microsoft.com/#mscorlib/system/console.cs,fcb364a853d81c57 + private static void SetWindowPosition(int left, int top) + { + AutoHelpers.LogInvariant("Attempt to set console viewport buffer to Left: {0} and Top: {1}", left, top); + + IntPtr hConsole = WinCon.GetStdHandle(WinCon.CONSOLE_STD_HANDLE.STD_OUTPUT_HANDLE); + // Get the size of the current console window + WinCon.CONSOLE_SCREEN_BUFFER_INFO csbi = new WinCon.CONSOLE_SCREEN_BUFFER_INFO(); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfo(hConsole, out csbi), "Get console screen buffer for viewport size information."); + + WinCon.SMALL_RECT srWindow = csbi.srWindow; + AutoHelpers.LogInvariant("Initial viewport position: {0}", srWindow); + + // Check for arithmetic underflows & overflows. + int newRight = left + srWindow.Right - srWindow.Left + 1; + if (left < 0 || newRight > csbi.dwSize.X || newRight < 0) + throw new ArgumentOutOfRangeException("left"); + int newBottom = top + srWindow.Bottom - srWindow.Top + 1; + if (top < 0 || newBottom > csbi.dwSize.Y || newBottom < 0) + throw new ArgumentOutOfRangeException("top"); + + // Preserve the size, but move the position. + srWindow.Bottom -= (short)(srWindow.Top - top); + srWindow.Right -= (short)(srWindow.Left - left); + srWindow.Left = (short)left; + srWindow.Top = (short)top; + + NativeMethods.Win32BoolHelper(WinCon.SetConsoleWindowInfo(hConsole, true, ref srWindow), string.Format("Attempt to update viewport position to {0}.", srWindow)); + } + + } +} diff --git a/src/host/ft_uia/MouseWheelTests.cs b/src/host/ft_uia/MouseWheelTests.cs new file mode 100644 index 000000000..0a1eaa8b6 --- /dev/null +++ b/src/host/ft_uia/MouseWheelTests.cs @@ -0,0 +1,138 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for ensuring mouse wheel functionality. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests +{ + using System; + + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + + [TestClass] + public class MouseWheelTests + { + public TestContext TestContext { get; set; } + + [TestMethod] + public void TestMouseWheel() + { + // Use a registry helper to backup and restore registry state before/after test + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + + // Start our application to test + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + Log.Comment("First ensure that word wrap is off so we can get scroll bars in both directions."); + // Make sure wrap is off + app.SetWrapState(false); + + IntPtr handle = app.GetStdOutHandle(); + Verify.IsNotNull(handle, "Ensure we have the output handle."); + + Log.Comment("Set up the window so the buffer is larger than the window. Retreive existing properties then set the viewport to smaller than the buffer."); + + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX info = app.GetScreenBufferInfo(handle); + + info.srWindow.Left = 0; + info.srWindow.Right = 30; + info.srWindow.Top = 0; + info.srWindow.Bottom = 30; + + info.dwSize.X = 100; + info.dwSize.Y = 100; + app.SetScreenBufferInfo(handle, info); + + Log.Comment("Now retrieve the starting position of the window viewport."); + + Log.Comment("Scroll down one."); + VerifyScroll(app, ScrollDir.Vertical, -1); + + Log.Comment("Scroll right one."); + VerifyScroll(app, ScrollDir.Horizontal, 1); + + Log.Comment("Scroll left one."); + VerifyScroll(app, ScrollDir.Horizontal, -1); + + Log.Comment("Scroll up one."); + VerifyScroll(app, ScrollDir.Vertical, 1); + } + } + } + + enum ScrollDir + { + Vertical, + Horizontal + } + + void VerifyScroll(CmdApp app, ScrollDir dir, int clicks) + { + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX beforeScroll; + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX afterScroll; + int deltaActual; + int deltaExpected; + + beforeScroll = app.GetScreenBufferInfo(); + + switch (dir) + { + case ScrollDir.Vertical: + app.ScrollWindow(clicks); + break; + case ScrollDir.Horizontal: + app.HScrollWindow(clicks); + break; + default: + throw new NotSupportedException(); + } + + // Give the window message a moment to take effect. + Globals.WaitForTimeout(); + + switch (dir) + { + case ScrollDir.Vertical: + deltaExpected = clicks * app.GetRowsPerScroll(); + break; + case ScrollDir.Horizontal: + deltaExpected = clicks * app.GetColsPerScroll(); + break; + default: + throw new NotSupportedException(); + } + + afterScroll = app.GetScreenBufferInfo(); + + switch (dir) + { + case ScrollDir.Vertical: + // Scrolling "negative" vertically is pulling the wheel downward which makes the lines move down. + // This means that if you scroll down from the top, before = 0 and after = 3. 0 - 3 = -3. + // The - sign of the delta here then aligns with the down = negative rule. + deltaActual = beforeScroll.srWindow.Top - afterScroll.srWindow.Top; + break; + case ScrollDir.Horizontal: + // Scrolling "negative" horizontally is pushing the wheel left which makes lines move left. + // This means that if you scroll left, before = 3 and after = 0. 0 - 3 = -3. + // The - sign of the detal here then alighs with the left = negative rule. + deltaActual = afterScroll.srWindow.Left - beforeScroll.srWindow.Left; + break; + default: + throw new NotSupportedException(); + } + + Verify.AreEqual(deltaExpected, deltaActual); + } + } +} diff --git a/src/host/ft_uia/Properties/AssemblyInfo.cs b/src/host/ft_uia/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..867f5b807 --- /dev/null +++ b/src/host/ft_uia/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Conhost.UIA.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b706ab66-8dd1-48eb-a81d-4ee55bc3d6ea")] diff --git a/src/host/ft_uia/SelectionApiTests.cs b/src/host/ft_uia/SelectionApiTests.cs new file mode 100644 index 000000000..2ef8dd98f --- /dev/null +++ b/src/host/ft_uia/SelectionApiTests.cs @@ -0,0 +1,354 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the Selection Information API. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Drawing; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + + using Microsoft.Win32; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using OpenQA.Selenium; + + [TestClass] + public class SelectionApiTests + { + public const int timeout = Globals.Timeout; + + public TestContext TestContext { get; set; } + + + [TestMethod] + public void TestCtrlHomeEnd() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + // Get console handle. + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the STDOUT handle is valid."); + + // Get us to an expected initial state. + app.UIRoot.SendKeys("C:" + Keys.Enter); + app.UIRoot.SendKeys(@"cd C:\" + Keys.Enter); + app.UIRoot.SendKeys("cls" + Keys.Enter); + + // Get initial screen buffer position + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + sbiexOriginal.cbSize = (uint)Marshal.SizeOf(sbiexOriginal); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexOriginal), "Get initial viewport position."); + + // Prep comparison structure + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexCompare = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + sbiexCompare.cbSize = (uint)Marshal.SizeOf(sbiexCompare); + + // Ctrl-End shouldn't move anything yet. + Log.Comment("Attempt Ctrl-End. Nothing should move yet."); + app.UIRoot.SendKeys(Keys.Control + Keys.End + Keys.Control); + + Globals.WaitForTimeout(); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexCompare), "Get comparison position."); + Verify.AreEqual(sbiexOriginal.srWindow, sbiexCompare.srWindow, "Compare viewport positions before and after."); + + // Ctrl-Home shouldn't move anything yet. + Log.Comment("Attempt Ctrl-Home. Nothing should move yet."); + app.UIRoot.SendKeys(Keys.Control + Keys.Home + Keys.Control); + + Globals.WaitForTimeout(); + + Log.Comment("Now test the line with some text in it."); + // Retrieve original position (including cursor) + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexOriginal), "Get position of viewport with nothing on edit line."); + + // Put some text onto the edit line now + Log.Comment("Place some text onto the edit line to ensure behavior will change with edit line full."); + const string testText = "SomeTestText"; + app.UIRoot.SendKeys(testText); + + Globals.WaitForTimeout(); + + // Get the position of the cursor after the text is entered + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexWithText = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + sbiexWithText.cbSize = (uint)Marshal.SizeOf(sbiexWithText); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexWithText), "Get position of viewport with edit line text."); + + // The cursor can't have moved down a line. We're going to verify the text by reading its "rectangle" out of the screen buffer. + // If it moved down a line, the calculation of what to select is more complicated than the simple rectangle assignment below. + Verify.AreEqual(sbiexOriginal.dwCursorPosition.Y, sbiexWithText.dwCursorPosition.Y, "There's an assumption here that the cursor stayed on the same line when we added our bit of text."); + + // Prepare the read rectangle for what we want to get out of the buffer. + Rectangle readRectangle = new Rectangle(sbiexOriginal.dwCursorPosition.X, + sbiexOriginal.dwCursorPosition.Y, + (sbiexWithText.dwCursorPosition.X - sbiexOriginal.dwCursorPosition.X), + 1); + + Log.Comment("Verify that the text we keyed matches what's in the buffer."); + IEnumerable text = area.GetLinesInRectangle(hConsole, readRectangle); + Verify.AreEqual(text.Count(), 1, "We should only have retrieved one line."); + Verify.AreEqual(text.First(), testText, "Verify text matches keyed input."); + + // Move cursor into the middle of the text. + Log.Comment("Move cursor into the middle of the string."); + + const int lefts = 4; + for (int i = 0; i < lefts; i++) + { + app.UIRoot.SendKeys(Keys.Left); + } + + Globals.WaitForTimeout(); + + // Get cursor position now that it's moved. + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexWithText), "Get position of viewport with cursor moved into the middle of the edit line text."); + + Log.Comment("Ctrl-End should trim the end of the input line from the cursor (and not move the cursor.)"); + app.UIRoot.SendKeys(Keys.Control + Keys.End + Keys.Control); + + Globals.WaitForTimeout(); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexCompare), "Get comparison position."); + Verify.AreEqual(sbiexWithText.srWindow, sbiexCompare.srWindow, "Compare viewport positions before and after."); + Verify.AreEqual(sbiexWithText.dwCursorPosition, sbiexCompare.dwCursorPosition, "Compare cursor positions before and after."); + + Log.Comment("Compare actual text visible on screen."); + text = area.GetLinesInRectangle(hConsole, readRectangle); + Verify.AreEqual(text.Count(), 1, "We should only have retrieved one line."); + + // the substring length is the original length of the string minus the number of lefts + int substringCtrlEnd = testText.Length - lefts; + Verify.AreEqual(text.First().Trim(), testText.Substring(0, substringCtrlEnd), "Verify text matches keyed input without the last characters removed by Ctrl+End."); + + Log.Comment("Ctrl-Home should trim the remainder of the edit line from the cursor to the beginning (restoring cursor to position before we entered anything.)"); + app.UIRoot.SendKeys(Keys.Control + Keys.Home + Keys.Control); + + Globals.WaitForTimeout(); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiexCompare), "Get comparison position."); + Verify.AreEqual(sbiexOriginal.srWindow, sbiexCompare.srWindow, "Compare viewport positions before and after."); + Verify.AreEqual(sbiexOriginal.dwCursorPosition, sbiexCompare.dwCursorPosition, "Compare cursor positions before and after."); + + Log.Comment("Compare actual text visible on screen."); + text = area.GetLinesInRectangle(hConsole, readRectangle); + Verify.AreEqual(text.Count(), 1, "We should only have retrieved one line."); + + Verify.AreEqual(text.First().Trim(), string.Empty, "Verify text is now empty after Ctrl+Home from the end of it."); + } + } + } + + [TestMethod] + public void TestKeyboardSelection() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + + VersionSelector.SetConsoleVersion(reg, ConsoleVersion.V2); + + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + WinCon.CONSOLE_SELECTION_INFO csi; + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Get initial selection state."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_NO_SELECTION, "Confirm no selection in progress."); + // ignore rectangle and coords. They're undefined when there is no selection. + + // Get cursor position at the beginning of this operation. The anchor will start at the cursor position for v2 console. + // NOTE: It moved to 0,0 for the v1 console. + IntPtr hConsole = WinCon.GetStdHandle(WinCon.CONSOLE_STD_HANDLE.STD_OUTPUT_HANDLE); + Verify.IsNotNull(hConsole, "Ensure the STDOUT handle is valid."); + + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX cbiex = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + cbiex.cbSize = (uint)Marshal.SizeOf(cbiex); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref cbiex), "Get initial cursor position (from screen buffer info)"); + + // The expected anchor when we're done is this initial cursor position + WinCon.COORD expectedAnchor = new WinCon.COORD(); + expectedAnchor.X = cbiex.dwCursorPosition.X; + expectedAnchor.Y = cbiex.dwCursorPosition.Y; + + // The expected rect is going to start from this cursor position. We'll modify it after we perform some operations. + WinCon.SMALL_RECT expectedRect = new WinCon.SMALL_RECT(); + expectedRect.Top = expectedAnchor.Y; + expectedRect.Left = expectedAnchor.X; + expectedRect.Right = expectedAnchor.X; + expectedRect.Bottom = expectedAnchor.Y; + + // Now set up the keyboard and enter mark mode. + // NOTE: We must wait after every keyboard sequence to give the console time to process before asking it for changes. + area.EnterMode(ViewportArea.ViewportStates.Mark); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Get state on entering mark mode."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_IN_PROGRESS, "Selection should now be in progress since mark mode is started."); + + // Select a small region + Log.Comment("1. Select a small region"); + + app.UIRoot.SendKeys(Keys.Shift + Keys.Right + Keys.Right + Keys.Right + Keys.Down + Keys.Shift); + + Globals.WaitForTimeout(); + + // Adjust the expected rectangle for the commands we just entered. + expectedRect.Right += 3; // same as the number of Rights we put in + expectedRect.Bottom += 1; // same as the number of Downs we put in + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Get state of selected region."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_IN_PROGRESS | WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_NOT_EMPTY, "Selection in progress and is no longer empty now that we've selected a region."); + Verify.AreEqual(csi.Selection, expectedRect, "Verify that the selected rectangle matches the keystrokes we entered."); + Verify.AreEqual(csi.SelectionAnchor, expectedAnchor, "Verify anchor didn't go anywhere since we started in the top left."); + + // End selection by moving + Log.Comment("2. End the selection by moving."); + + app.UIRoot.SendKeys(Keys.Down); + + Globals.WaitForTimeout(); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Move cursor to attempt to clear selection."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_IN_PROGRESS, "Selection should be still running, but empty."); + + // Select another region to ensure anchor moved. + Log.Comment("3. Select one more region from new position to verify anchor"); + + app.UIRoot.SendKeys(Keys.Shift + Keys.Right + Keys.Shift); + + Globals.WaitForTimeout(); + + expectedAnchor.X = expectedRect.Right; + expectedAnchor.Y = expectedRect.Bottom; + expectedAnchor.Y++; // +1 for the Down in step 2. Not incremented in the line above because C# is unhappy with adding +1 to a short while assigning. + + Verify.AreEqual(csi.SelectionAnchor, expectedAnchor, "Verify anchor moved to the new start position."); + + // Exit mark mode + area.EnterMode(ViewportArea.ViewportStates.Normal); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Move cursor to attempt to clear selection."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_NO_SELECTION, "Selection should be empty when mode is exited."); + } + } + } + } + + [TestMethod] + public void TestMouseSelection() + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (ViewportArea area = new ViewportArea(app)) + { + // Set up the area we're going to attempt to select + Point startPoint = new Point(); + Point endPoint = new Point(); + + startPoint.X = 1; + startPoint.Y = 2; + + endPoint.X = 10; + endPoint.Y = 10; + + // Save expected anchor + WinCon.COORD expectedAnchor = new WinCon.COORD(); + expectedAnchor.X = (short)startPoint.X; + expectedAnchor.Y = (short)startPoint.Y; + + // Also save bottom right corner for the end of the selection + WinCon.COORD expectedBottomRight = new WinCon.COORD(); + expectedBottomRight.X = (short)endPoint.X; + expectedBottomRight.Y = (short)endPoint.Y; + + // Prepare the mouse by moving it into the start position. Prepare the structure + WinCon.CONSOLE_SELECTION_INFO csi; + WinCon.SMALL_RECT expectedRect = new WinCon.SMALL_RECT(); + + WinCon.CONSOLE_SELECTION_INFO_FLAGS flagsExpected = WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_NO_SELECTION; + + // 1. Place mouse button down to start selection and check state + area.MouseMove(startPoint); + area.MouseDown(); + + Globals.WaitForTimeout(); // must wait after mouse operation. No good waiters since we have no UI objects + + flagsExpected |= WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_IN_PROGRESS; // a selection is occuring + flagsExpected |= WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_MOUSE_SELECTION; // it's a "Select" mode not "Mark" mode selection + flagsExpected |= WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_MOUSE_DOWN; // the mouse is still down + flagsExpected |= WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_SELECTION_NOT_EMPTY; // mouse selections are never empty. minimum 1x1 + + expectedRect.Top = expectedAnchor.Y; // rectangle is just at the point itself 1x1 size + expectedRect.Left = expectedAnchor.X; + expectedRect.Bottom = expectedRect.Top; + expectedRect.Right = expectedRect.Left; + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Check state on mouse button down to start selection."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, flagsExpected, "Check initial mouse selection with button still down."); + Verify.AreEqual(csi.SelectionAnchor, expectedAnchor, "Check that the anchor is equal to the start point."); + Verify.AreEqual(csi.Selection, expectedRect, "Check that entire rectangle is the size of 1x1 and is just at the anchor point."); + + // 2. Move to end point and release cursor + area.MouseMove(endPoint); + area.MouseUp(); + + Globals.WaitForTimeout(); // must wait after mouse operation. No good waiters since we have no UI objects + + // on button up, remove mouse down flag + flagsExpected &= ~WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_MOUSE_DOWN; + + // anchor remains the same + // bottom right of rectangle now changes to the end point + expectedRect.Bottom = expectedBottomRight.Y; + expectedRect.Right = expectedBottomRight.X; + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Check state after drag and release mouse."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, flagsExpected, "Check selection is still on and valid, but button is up."); + Verify.AreEqual(csi.SelectionAnchor, expectedAnchor, "Check that the anchor is still equal to the start point."); + Verify.AreEqual(csi.Selection, expectedRect, "Check that entire rectangle reaches from start to end point."); + + // 3. Leave mouse selection + area.ExitModes(); + + flagsExpected = WinCon.CONSOLE_SELECTION_INFO_FLAGS.CONSOLE_NO_SELECTION; + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleSelectionInfo(out csi), "Check state after exiting mouse selection."); + Log.Comment("Selection Info: {0}", csi); + + Verify.AreEqual(csi.Flags, flagsExpected, "Check that selection state is reset."); + } + } + } + } +} diff --git a/src/host/ft_uia/VirtualTerminalTests.cs b/src/host/ft_uia/VirtualTerminalTests.cs new file mode 100644 index 000000000..9c0c851b9 --- /dev/null +++ b/src/host/ft_uia/VirtualTerminalTests.cs @@ -0,0 +1,775 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the Virtual Terminal feature. +//---------------------------------------------------------------------------------------------------------------------- +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Drawing; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + + using Microsoft.Win32; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using OpenQA.Selenium; + + [TestClass] + public class VirtualTerminalTests + { + private static string VIRTUAL_TERMINAL_KEY_NAME = "VirtualTerminalLevel"; + private static int VIRTUAL_TERMINAL_ON_VALUE = 100; + + private static string vtAppLocation; + + public const int timeout = Globals.Timeout; + + public TestContext TestContext { get; set; } + + [ClassInitialize] + public static void ClassSetup(TestContext context) + { + Log.Comment("Searching for VtApp.exe in the same directory where this test was launched from..."); + vtAppLocation = Path.Combine(context.TestDeploymentDir, "VtApp.exe"); + Verify.IsFalse(string.IsNullOrEmpty(vtAppLocation)); + Verify.IsTrue(File.Exists(vtAppLocation)); + } + + [TestMethod] + public void RunVtAppTester() + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); // we're going to modify the virtual terminal state for this, so back it up first. + VersionSelector.SetConsoleVersion(reg, ConsoleVersion.V2); + reg.SetDefaultValue(VIRTUAL_TERMINAL_KEY_NAME, VIRTUAL_TERMINAL_ON_VALUE); + + bool haveVtAppPath = !string.IsNullOrEmpty(vtAppLocation); + + Verify.IsTrue(haveVtAppPath, "Ensure that we passed in the location to VtApp.exe"); + + if (haveVtAppPath) + { + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext, vtAppLocation)) + { + using (ViewportArea area = new ViewportArea(app)) + { + // Get console handle. + IntPtr hConsole = app.GetStdOutHandle(); + Verify.IsNotNull(hConsole, "Ensure the STDOUT handle is valid."); + + Log.Comment("Check that the VT test app loaded up properly with its output line and the cursor down one line."); + Rectangle selectRect = new Rectangle(0, 0, 9, 1); + IEnumerable scrapedText = area.GetLinesInRectangle(hConsole, selectRect); + + Verify.AreEqual(scrapedText.Count(), 1, "We should have retrieved one line."); + string testerWelcome = scrapedText.Single(); + + Verify.AreEqual(testerWelcome, "VT Tester"); + + WinCon.COORD cursorPos = app.GetCursorPosition(hConsole); + WinCon.COORD cursorExpected = new WinCon.COORD(); + cursorExpected.X = 0; + cursorExpected.Y = 1; + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved to expected starting position."); + + TestCursorPositioningCommands(app, hConsole, cursorExpected); + + TestCursorVisibilityCommands(app, hConsole); + + TestAreaEraseCommands(app, area, hConsole); + + TestGraphicsCommands(app, area, hConsole); + + TestQueryResponses(app, hConsole); + + TestVtToggle(app, hConsole); + + TestInsertDelete(app, area, hConsole); + } + } + } + } + } + + private static void TestInsertDelete(CmdApp app, ViewportArea area, IntPtr hConsole) + { + Log.Comment("--Insert/Delete Commands"); + ScreenFillHelper(app, area, hConsole); + + Log.Comment("Move cursor to the middle-ish"); + Point cursorExpected = new Point(); + // H is at 5, 1. VT coords are 1-based and buffer is 0-based so adjust. + cursorExpected.Y = 5 - 1; + cursorExpected.X = 1 - 1; + app.UIRoot.SendKeys("H"); + + // Move to middle-ish from here. 10 Bs and 10 Cs should about do it. + for (int i=0; i < 10; i++) + { + app.UIRoot.SendKeys("BC"); + cursorExpected.Y++; + cursorExpected.X++; + } + + WinCon.SMALL_RECT viewport = app.GetViewport(hConsole); + + // The entire buffer should be Zs except for what we're about to insert and delete. + app.UIRoot.SendKeys("O"); // insert + WinCon.CHAR_INFO ciCursor = area.GetCharInfoAt(hConsole, cursorExpected); + Verify.AreEqual(' ', ciCursor.UnicodeChar); + + Point endOfCursorLine = new Point(viewport.Right, cursorExpected.Y); + + app.UIRoot.SendKeys("P"); // delete + WinCon.CHAR_INFO ciEndOfLine = area.GetCharInfoAt(hConsole, endOfCursorLine); + Verify.AreEqual(' ', ciEndOfLine.UnicodeChar); + ciCursor = area.GetCharInfoAt(hConsole, cursorExpected); + Verify.AreEqual('Z', ciCursor.UnicodeChar); + + // Move to end of line and check both insert and delete operations + while (cursorExpected.X < viewport.Right) + { + app.UIRoot.SendKeys("C"); + cursorExpected.X++; + } + + // move up a line to get some fresh Z + app.UIRoot.SendKeys("A"); + cursorExpected.Y--; + + app.UIRoot.SendKeys("O"); // insert at end of line + ciCursor = area.GetCharInfoAt(hConsole, cursorExpected); + Verify.AreEqual(' ', ciCursor.UnicodeChar); + + // move up a line to get some fresh Z + app.UIRoot.SendKeys("A"); + cursorExpected.Y--; + + app.UIRoot.SendKeys("P"); // delete at end of line + ciCursor = area.GetCharInfoAt(hConsole, cursorExpected); + Verify.AreEqual(' ', ciCursor.UnicodeChar); + } + + private static void TestVtToggle(CmdApp app, IntPtr hConsole) + { + WinCon.COORD cursorPos; + Log.Comment("--Test VT Toggle--"); + + Verify.IsTrue(app.IsVirtualTerminalEnabled(hConsole), "Verify we're starting with VT on."); + + app.UIRoot.SendKeys("H-"); // move cursor to top left area H location and then turn off VT. + + cursorPos = app.GetCursorPosition(hConsole); + + Verify.IsFalse(app.IsVirtualTerminalEnabled(hConsole), "Verify VT was turned off."); + + app.UIRoot.SendKeys("-"); + Verify.IsTrue(app.IsVirtualTerminalEnabled(hConsole), "Verify VT was turned back on ."); + } + + private static void TestQueryResponses(CmdApp app, IntPtr hConsole) + { + WinCon.COORD cursorPos; + Log.Comment("---Status Request Commands---"); + Globals.WaitForTimeout(); + app.UIRoot.SendKeys("c"); + string expectedTitle = string.Format("Response Received: {0}", "\x1b[?1;0c"); + + Globals.WaitForTimeout(); + string title = app.GetWindowTitle(); + Verify.AreEqual(expectedTitle, title, "Verify that we received the proper response to the Device Attributes request."); + + app.UIRoot.SendKeys("R"); + cursorPos = app.GetCursorPosition(hConsole); + expectedTitle = string.Format("Response Received: {0}", string.Format("\x1b[{0};{1}R", cursorPos.Y + 1, cursorPos.X + 1)); + + Globals.WaitForTimeout(); + title = app.GetWindowTitle(); + Verify.AreEqual(expectedTitle, title, "Verify that we received the proper response to the Cursor Position request."); + } + + private static void TestGraphicsCommands(CmdApp app, ViewportArea area, IntPtr hConsole) + { + Log.Comment("---Graphics Commands---"); + ScreenFillHelper(app, area, hConsole); + + WinCon.CHAR_INFO ciExpected = new WinCon.CHAR_INFO(); + ciExpected.UnicodeChar = 'z'; + ciExpected.Attributes = app.GetCurrentAttributes(hConsole); + + WinCon.CHAR_INFO ciOriginal = ciExpected; + + WinCon.CHAR_INFO ciActual; + Point pt = new Point(); + + Log.Comment("Set foreground brightness (SGR.1)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("1`"); + + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground brightness got set."); + + Log.Comment("Set foreground green (SGR.32)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("2`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_GREEN; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground green got set."); + + Log.Comment("Set foreground yellow (SGR.33)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("3`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_YELLOW; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground yellow got set."); + + Log.Comment("Set foreground blue (SGR.34)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("4`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_BLUE; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground blue got set."); + + Log.Comment("Set foreground magenta (SGR.35)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("5`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_MAGENTA; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground magenta got set."); + + Log.Comment("Set foreground cyan (SGR.36)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("6`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_CYAN; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground cyan got set."); + + Log.Comment("Set background white (SGR.47)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("W`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_COLORS; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background white got set."); + + Log.Comment("Set background black (SGR.40)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("Q`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background black got set."); + + Log.Comment("Set background red (SGR.41)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("q`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_RED; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background red got set."); + + Log.Comment("Set background yellow (SGR.43)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("w`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_YELLOW; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background yellow got set."); + + Log.Comment("Set foreground bright red (SGR.91)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("!`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_RED | WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground bright red got set."); + + Log.Comment("Set foreground bright blue (SGR.94)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("@`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_BLUE | WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground bright blue got set."); + + Log.Comment("Set foreground bright cyan (SGR.96)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("#`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_CYAN | WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that foreground bright cyan got set."); + + Log.Comment("Set background bright red (SGR.101)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("$`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_RED | WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background bright red got set."); + + Log.Comment("Set background bright blue (SGR.104)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys(Keys.Shift + "5" + Keys.Shift + "`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_BLUE | WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background bright blue got set."); + + Log.Comment("Set background bright cyan (SGR.106)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys(Keys.Shift + "6" + Keys.Shift + "`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_CYAN | WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that background bright cyan got set."); + + Log.Comment("Set underline (SGR.4)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("e`"); + + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that underline got set."); + + Log.Comment("Clear underline (SGR.24)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("d`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that underline got cleared."); + + Log.Comment("Set negative image video (SGR.7)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("r`"); + + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_REVERSE_VIDEO; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that negative video got set."); + + Log.Comment("Set positive image video (SGR.27)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("f`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_REVERSE_VIDEO; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that positive video got set."); + + Log.Comment("Set back to default (SGR.0)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("0`"); + + ciExpected = ciOriginal; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that we got set back to the original state."); + + Log.Comment("Set multiple properties in the same message (SGR.1,37,43,4)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys("9`"); + + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_COLORS; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_YELLOW; + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.COMMON_LVB_UNDERSCORE; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that we set foreground bright white, background yellow, and underscore in the same SGR command."); + + Log.Comment("Set foreground back to original only (SGR.39)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys(Keys.Shift + "9" + Keys.Shift + "`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL; // turn off all foreground flags + ciExpected.Attributes |= (ciOriginal.Attributes & WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_ALL); // turn on only the foreground part of the original attributes + ciExpected.Attributes |= WinCon.CONSOLE_ATTRIBUTES.FOREGROUND_INTENSITY; + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that we set the foreground only back to the default."); + + Log.Comment("Set background back to original only (SGR.49)"); + app.FillCursorPosition(hConsole, ref pt); + app.UIRoot.SendKeys(Keys.Shift + "0" + Keys.Shift + "`"); + + ciExpected.Attributes &= ~WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL; // turn off all foreground flags + ciExpected.Attributes |= (ciOriginal.Attributes & WinCon.CONSOLE_ATTRIBUTES.BACKGROUND_ALL); // turn on only the foreground part of the original attributes + + ciActual = area.GetCharInfoAt(hConsole, pt); + Verify.AreEqual(ciExpected, ciActual, "Verify that we set the background only back to the default."); + } + + private static void TestCursorPositioningCommands(CmdApp app, IntPtr hConsole, WinCon.COORD cursorExpected) + { + WinCon.COORD cursorPos; + Log.Comment("---Cursor Positioning Commands---"); + // Try cursor commands + Log.Comment("Press B key (cursor down)"); + app.UIRoot.SendKeys("B"); + cursorExpected.Y++; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved down by 1."); + + Log.Comment("Press A key (cursor up)"); + app.UIRoot.SendKeys("A"); + cursorExpected.Y--; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved up by 1."); + + Log.Comment("Press C key (cursor right)"); + app.UIRoot.SendKeys("C"); + cursorExpected.X++; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved right by 1."); + + Log.Comment("Press D key (cursor left)"); + app.UIRoot.SendKeys("D"); + cursorExpected.X--; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved left by 1."); + + Log.Comment("Move to the right (C) then move down a line (E)"); + app.UIRoot.SendKeys("CCC"); + cursorExpected.X += 3; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has right by 3."); + + app.UIRoot.SendKeys("E"); + cursorExpected.Y++; + cursorExpected.X = 0; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved down by 1 line and reset X position to 0."); + + Log.Comment("Move to the right (C) then move up a line (F)"); + app.UIRoot.SendKeys("CCC"); + cursorExpected.X += 3; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check curs has right by 3."); + + app.UIRoot.SendKeys("F"); + cursorExpected.Y--; + cursorExpected.X = 0; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved up by 1 line."); + + Log.Comment("Check move directly to position 14 horizontally (G)"); + app.UIRoot.SendKeys("G"); + cursorExpected.X = 14 - 1; // 14 is the VT position which starts at array offset 1. 13 is the buffer position starting at array offset 0. + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved to horizontal position 14."); + + Log.Comment("Check move directly to position 14 vertically (v key mapped to d)"); + app.UIRoot.SendKeys("v"); + cursorExpected.Y = 14 - 1; // 14 is the VT position which starts at array offset 1. 13 is the buffer position starting at array offset 0. + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved to vertical position 14."); + + Log.Comment("Check move directly to row 5, column 1 (H)"); + app.UIRoot.SendKeys("H"); + // Again -1s are to convert index base 1 VT to console base 0 arrays + cursorExpected.Y = 5 - 1; + cursorExpected.X = 1 - 1; + cursorPos = app.GetCursorPosition(hConsole); + Verify.AreEqual(cursorExpected, cursorPos, "Check cursor has moved to row 5, column 1."); + } + + private static void TestCursorVisibilityCommands(CmdApp app, IntPtr hConsole) + { + WinCon.COORD cursorExpected; + Log.Comment("---Cursor Visibility Commands---"); + Log.Comment("Turn cursor display off. (l)"); + app.UIRoot.SendKeys("l"); + Verify.AreEqual(false, app.IsCursorVisible(hConsole), "Check that cursor is invisible."); + + Log.Comment("Turn cursor display on. (h)"); + app.UIRoot.SendKeys("h"); + Verify.AreEqual(true, app.IsCursorVisible(hConsole), "Check that cursor is visible."); + + Log.Comment("---Cursor Save/Restore Commands---"); + Log.Comment("Save current cursor position with DEC save."); + app.UIRoot.SendKeys("7"); + cursorExpected = app.GetCursorPosition(hConsole); + + Log.Comment("Move the cursor a bit away from the saved position."); + app.UIRoot.SendKeys("BBBBCCCCC"); + + Log.Comment("Restore existing position with DEC restore."); + app.UIRoot.SendKeys("8"); + Verify.AreEqual(cursorExpected, app.GetCursorPosition(hConsole), "Check that cursor restored back to the saved position."); + + Log.Comment("Move the cursor a bit away from the saved position."); + app.UIRoot.SendKeys("BBBBCCCCC"); + + Log.Comment("Restore existing position with ANSISYS restore."); + app.UIRoot.SendKeys("u"); + Verify.AreEqual(cursorExpected, app.GetCursorPosition(hConsole), "Check that cursor restored back to the saved position."); + + Log.Comment("Move the cursor a bit away from either position."); + app.UIRoot.SendKeys("BBB"); + + Log.Comment("Save current cursor position with ANSISYS save."); + app.UIRoot.SendKeys("y"); + cursorExpected = app.GetCursorPosition(hConsole); + + Log.Comment("Move the cursor a bit away from the saved position."); + app.UIRoot.SendKeys("CCCBB"); + + Log.Comment("Restore existing position with DEC restore."); + app.UIRoot.SendKeys("8"); + Verify.AreEqual(cursorExpected, app.GetCursorPosition(hConsole), "Check that cursor restored back to the saved position."); + } + + private static void TestAreaEraseCommands(CmdApp app, ViewportArea area, IntPtr hConsole) + { + WinCon.COORD cursorPos; + Log.Comment("---Area Erase Commands---"); + ScreenFillHelper(app, area, hConsole); + Log.Comment("Clear screen after"); + app.UIRoot.SendKeys("J"); + + Globals.WaitForTimeout(); // give buffer time to clear. + + cursorPos = app.GetCursorPosition(hConsole); + + GetExpectedChar expectedCharAlgorithm; + expectedCharAlgorithm = (int rowId, int colId, int height, int width) => + { + if (rowId == (height - 1) && colId == (width - 1)) + { + return ' '; + } + else if (rowId < cursorPos.Y) + { + return 'Z'; + } + else if (rowId > cursorPos.Y) + { + return ' '; + } + else + { + if (colId < cursorPos.X) + { + return 'Z'; + } + else + { + return ' '; + } + } + }; + + BufferVerificationHelper(app, area, hConsole, expectedCharAlgorithm); + + ScreenFillHelper(app, area, hConsole); + Log.Comment("Clear screen before"); + app.UIRoot.SendKeys("j"); + + expectedCharAlgorithm = (int rowId, int colId, int height, int width) => + { + if (rowId == (height - 1) && colId == (width - 1)) + { + return ' '; + } + else if (rowId < cursorPos.Y) + { + return ' '; + } + else if (rowId > cursorPos.Y) + { + return 'Z'; + } + else + { + if (colId <= cursorPos.X) + { + return ' '; + } + else + { + return 'Z'; + } + } + }; + + BufferVerificationHelper(app, area, hConsole, expectedCharAlgorithm); + + ScreenFillHelper(app, area, hConsole); + Log.Comment("Clear line after"); + app.UIRoot.SendKeys("K"); + + expectedCharAlgorithm = (int rowId, int colId, int height, int width) => + { + if (rowId == (height - 1) && colId == (width - 1)) + { + return ' '; + } + else if (rowId != cursorPos.Y) + { + return 'Z'; + } + else + { + if (colId < cursorPos.X) + { + return 'Z'; + } + else + { + return ' '; + } + } + }; + + BufferVerificationHelper(app, area, hConsole, expectedCharAlgorithm); + + ScreenFillHelper(app, area, hConsole); + Log.Comment("Clear line before"); + app.UIRoot.SendKeys("k"); + + expectedCharAlgorithm = (int rowId, int colId, int height, int width) => + { + if (rowId == (height - 1) && colId == (width - 1)) + { + return ' '; + } + else if (rowId != cursorPos.Y) + { + return 'Z'; + } + else + { + if (colId <= cursorPos.X) + { + return ' '; + } + else + { + return 'Z'; + } + } + }; + + BufferVerificationHelper(app, area, hConsole, expectedCharAlgorithm); + } + + delegate char GetExpectedChar(int rowId, int colId, int height, int width); + + private static void ScreenFillHelper(CmdApp app, ViewportArea area, IntPtr hConsole) + { + Log.Comment("Fill screen with junk"); + app.UIRoot.SendKeys(Keys.Shift + "`" + Keys.Shift); + + Globals.WaitForTimeout(); // give the buffer time to fill. + + GetExpectedChar expectedCharAlgorithm = (int rowId, int colId, int height, int width) => + { + // For the very last bottom right corner character, it should be space. Every other character is a Z when filled. + if (rowId == (height - 1) && colId == (width - 1)) + { + return ' '; + } + else + { + return 'Z'; + } + }; + + BufferVerificationHelper(app, area, hConsole, expectedCharAlgorithm); + } + + private static void BufferVerificationHelper(CmdApp app, ViewportArea area, IntPtr hConsole, GetExpectedChar expectedCharAlgorithm) + { + WinCon.SMALL_RECT viewport = app.GetViewport(hConsole); + Rectangle selectRect = new Rectangle(viewport.Left, viewport.Top, viewport.Width, viewport.Height); + IEnumerable scrapedText = area.GetLinesInRectangle(hConsole, selectRect); + + Verify.AreEqual(viewport.Height, scrapedText.Count(), "Verify the rows scraped is equal to the entire viewport height."); + + bool isValidState = true; + string[] rows = scrapedText.ToArray(); + for (int i = 0; i < rows.Length; i++) + { + for (int j = 0; j < viewport.Width; j++) + { + char actual = rows[i][j]; + char expected = expectedCharAlgorithm(i, j, rows.Length, viewport.Width); + + isValidState = actual == expected; + + if (!isValidState) + { + Verify.Fail(string.Format("Text buffer verification failed at Row: {0} Col: {1} Expected: '{2}' Actual: '{3}'", i, j, expected, actual)); + break; + } + } + + if (!isValidState) + { + break; + } + } + } + } +} diff --git a/src/host/ft_uia/WinEventTests.cs b/src/host/ft_uia/WinEventTests.cs new file mode 100644 index 000000000..e796c773d --- /dev/null +++ b/src/host/ft_uia/WinEventTests.cs @@ -0,0 +1,431 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// UI Automation tests for the Accessibility WinEvents feature. +//---------------------------------------------------------------------------------------------------------------------- + +namespace Conhost.UIA.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + using WEX.Common.Managed; + using WEX.Logging.Interop; + using WEX.TestExecution; + using WEX.TestExecution.Markup; + + using Conhost.UIA.Tests.Common; + using Conhost.UIA.Tests.Common.NativeMethods; + using Conhost.UIA.Tests.Elements; + using OpenQA.Selenium; + using System.Threading; + using System.Runtime.InteropServices; + using System.Drawing; + + // Test hooks adapted from C++ WinEvent accessibility sample at https://msdn.microsoft.com/en-us/library/ms971319.aspx#atg_consoleaccessibility_topic05 + [TestClass] + public class WinEventTests : IWinEventCallbacks + { + public TestContext TestContext { get; set; } + + enum EventType + { + CaretSelection, + CaretVisible, + EndApplication, + Layout, + StartApplication, + UpdateRegion, + UpdateScroll, + UpdateSimple + } + + class EventData + { + EventType type; + // Of all the WinEvents that we return in the console, the max number of parameters returned is 4. + int[] data = new int[4]; + + public EventData(EventType type, int a = 0, int b = 0, int c = 0, int d = 0) + { + this.type = type; + data[0] = a; + data[1] = b; + data[2] = c; + data[3] = d; + } + + public override bool Equals(object obj) + { + if (typeof(EventData) == obj.GetType()) + { + return Equals((EventData)obj); + } + else + { + return base.Equals(obj); + } + } + + public bool Equals(EventData other) + { + return type == other.type && + data[0] == other.data[0] && + data[1] == other.data[1] && + data[2] == other.data[2] && + data[3] == other.data[3]; + } + + public override string ToString() + { + return $"\r\nType: {type} Data[0]: {data[0]} Data[1]: {data[1]} Data[2]: {data[2]} Data[3]: {data[3]}\r\n"; + } + + public override int GetHashCode() + { + return type.GetHashCode() ^ data[0].GetHashCode() ^ data[1].GetHashCode() ^ data[2].GetHashCode() ^ data[3].GetHashCode(); + } + } + + Queue received = new Queue(); + + public void CaretSelection(int x, int y) + { + Log.Comment($"Event Console CARET SELECTION! X: {x} Y: {y}"); + received.Enqueue(new EventData(EventType.CaretSelection, x, y)); + } + + public void CaretVisible(int x, int y) + { + Log.Comment($"Event Console CARET VISIBLE! X: {x} Y: {y}"); + received.Enqueue(new EventData(EventType.CaretVisible, x, y)); + } + + public void EndApplication(int processId, int childId) + { + Log.Comment($"Event Console END APPLICATION! Process ID: {processId} - Child ID: {childId}"); + received.Enqueue(new EventData(EventType.EndApplication)); // we don't have a great way of detecting pids now so don't store it for comparison + } + + public void Layout() + { + Log.Comment("Event Console LAYOUT!"); + received.Enqueue(new EventData(EventType.Layout)); + } + + public void StartApplication(int processId, int childId) + { + Log.Comment($"Event Console START APPLICATION! Process ID: {processId} - Child ID: {childId}"); + received.Enqueue(new EventData(EventType.StartApplication)); // we don't have a great way of detecting pids now so don't store it for comparison + } + + public void UpdateRegion(int left, int top, int right, int bottom) + { + Log.Comment($"Event Console UPDATE REGION! Left: {left} - Top: {top} - Right: {right} - Bottom: {bottom}"); + received.Enqueue(new EventData(EventType.UpdateRegion, left, top, right, bottom)); + } + + public void UpdateScroll(int deltaX, int deltaY) + { + Log.Comment($"Event Console UPDATE SCROLL! dx: {deltaX} - dy: {deltaY}"); + received.Enqueue(new EventData(EventType.UpdateScroll, deltaX, deltaY)); + } + + public void UpdateSimple(int x, int y, int character, int attribute) + { + Log.Comment($"Event Console UPDATE SIMPLE! X: {x} - Y: {y} - Char: {character} - Attr: {attribute}"); + received.Enqueue(new EventData(EventType.UpdateSimple, x, y, character, attribute)); + } + + void VerifyQueue(Queue testQueue) + { + while (testQueue.Count > 0) + { + Verify.AreEqual(testQueue.Dequeue(), received.Dequeue()); + } + } + + public void TestTypeStringHelper(string str, CmdApp app, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex) + { + Queue expected = new Queue(); + + foreach (char c in str) + { + // For each character, we expect a pair of messages. + // 1. A Update Simple where the cursor was to represent the letter inserted where the cursor was. + expected.Enqueue(new EventData(EventType.UpdateSimple, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y, c, (int)sbiex.wAttributes)); + // Move cursor right by 1. + sbiex.dwCursorPosition.X++; + // 2. A caret visible to represent where the cursor moved after the letter was inserted. + expected.Enqueue(new EventData(EventType.CaretVisible, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + + // Type in the letter + app.UIRoot.SendKeys($"{c}"); + + Globals.WaitForTimeout(); + } + + // Verify that we saw the right messages + this.VerifyQueue(expected); + } + + delegate void AccessibilityTest(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal); + + + void RunTest(AccessibilityTest test) + { + using (RegistryHelper reg = new RegistryHelper()) + { + reg.BackupRegistry(); + using (CmdApp app = new CmdApp(CreateType.ProcessOnly, TestContext)) + { + using (WinEventSystem sys = app.AttachWinEventSystem(this)) + { + using (ViewportArea area = new ViewportArea(app)) + { + Globals.WaitForTimeout(); // wait for everything to settle with winevents + IntPtr hConsole = app.GetStdOutHandle(); + + // Prep structures to hold cursor and size of buffer. + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex = new WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX(); + sbiex.cbSize = (uint)Marshal.SizeOf(sbiex); + + // this is where we will hold our expected messages + Queue expected = new Queue(); + + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Get initial console data."); + WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal = sbiex; // keep a copy of the original data for later. + + // Run the test + test(app, area, hConsole, sbiex, expected, sbiexOriginal); + } + } + } + } + } + + [TestMethod] + public void TestSelection() + { + RunTest(TestSelectionImpl); + } + + private void TestSelectionImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal) + { + // A. Test single area click + { + // Move mouse pointer to where the cursor is + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + Point pt = new Point(sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y); + area.MouseMove(pt); + + // Click on this area. + expected.Enqueue(new EventData(EventType.CaretSelection, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + area.MouseDown(); + area.MouseUp(); + + Globals.WaitForTimeout(); + + VerifyQueue(expected); + + // We may receive more than one caret and that's OK. Clear it out. + this.received.Clear(); + + // End selection with escape + app.UIRoot.SendKeys(Keys.Escape); + Globals.WaitForTimeout(); + + // Expect to see the caret again after leaving selection mode + expected.Enqueue(new EventData(EventType.CaretVisible, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + VerifyQueue(expected); + } + + // B. Drag area click + { + // Move mouse pointer to where the cursor is + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + Point pt = new Point(sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y); + area.MouseMove(pt); + + // Click on this area. + expected.Enqueue(new EventData(EventType.CaretSelection, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + area.MouseDown(); + + Globals.WaitForTimeout(); + + Point ptDrag = pt; + // Drag down and right for "some" distance. 10 isn't for a specific reason, it's just "some". + ptDrag.X += 10; + ptDrag.Y += 10; + + area.MouseMove(ptDrag); + + Globals.WaitForTimeout(); + + area.MouseUp(); + + Globals.WaitForTimeout(); + + // Verify that the first one in the queue starts with where we put the mouse down. + VerifyQueue(expected); + + // Now we have to take the final message in the queue and make sure it is where we released the mouse + EventData expectedLast = new EventData(EventType.CaretSelection, ptDrag.X, ptDrag.Y); + EventData actualLast = received.Last(); + Verify.AreEqual(expectedLast, actualLast); + + // Empty the received queue. + received.Clear(); + + // End selection with escape + app.UIRoot.SendKeys(Keys.Escape); + Globals.WaitForTimeout(); + + // Expect to see the caret again after leaving selection mode + expected.Enqueue(new EventData(EventType.CaretVisible, sbiex.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + VerifyQueue(expected); + } + } + + [TestMethod] + public void TestLaunchAndExitChild() + { + RunTest(TestLaunchAndExitChildImpl); + } + + private void TestLaunchAndExitChildImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal) + { + // A. We're going to type "cmd" into the prompt to start a command prompt. + { + Log.Comment("Type 'cmd' to get ready to start nested prompt."); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + TestTypeStringHelper("cmd", app, sbiex); + } + + // B. Now we're going to press enter to launch the CMD application + { + Log.Comment("Press enter to launch and observe launch events."); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + expected.Enqueue(new EventData(EventType.StartApplication)); + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, 0, sbiex.dwSize.X - 1, sbiex.dwSize.Y - 1)); + sbiex.dwCursorPosition.Y++; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, sbiex.dwCursorPosition.Y, "Microsoft Windows [Version 10.0.14974.1001]".Length - 1, sbiex.dwCursorPosition.Y)); + sbiex.dwCursorPosition.Y++; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, sbiex.dwCursorPosition.Y, "(c) 2016 Microsoft Corporation. All rights reserved.".Length - 1, sbiex.dwCursorPosition.Y)); + sbiex.dwCursorPosition.Y++; + sbiex.dwCursorPosition.Y++; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, sbiex.dwCursorPosition.Y, sbiexOriginal.dwCursorPosition.X - 1, sbiex.dwCursorPosition.Y)); + expected.Enqueue(new EventData(EventType.CaretVisible, sbiexOriginal.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + Globals.WaitForTimeout(); + VerifyQueue(expected); + } + + // C. Now we're going to type exit to leave the nested CMD application + { + Log.Comment("Type 'exit' to get ready to exit nested prompt."); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + TestTypeStringHelper("exit", app, sbiex); + } + + // D. Now we're going to press enter to exit the CMD application + { + Log.Comment("Press enter to launch and observe exit events."); + NativeMethods.Win32BoolHelper(WinCon.GetConsoleScreenBufferInfoEx(hConsole, ref sbiex), "Update console data."); + expected.Enqueue(new EventData(EventType.EndApplication)); + sbiex.dwCursorPosition.Y++; + sbiex.dwCursorPosition.Y++; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, sbiex.dwCursorPosition.Y, sbiexOriginal.dwCursorPosition.X - 1, sbiex.dwCursorPosition.Y)); + expected.Enqueue(new EventData(EventType.CaretVisible, sbiexOriginal.dwCursorPosition.X, sbiex.dwCursorPosition.Y)); + + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + Globals.WaitForTimeout(); + VerifyQueue(expected); + } + } + + [TestMethod] + public void TestScrollByWheel() + { + RunTest(TestScrollByWheelImpl); + } + + private void TestScrollByWheelImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal) + { + int rowsPerScroll = app.GetRowsPerScroll(); + int scrollDelta; + + // A. Scroll down. + { + scrollDelta = -1; + expected.Enqueue(new EventData(EventType.UpdateScroll, 0, scrollDelta * rowsPerScroll)); + expected.Enqueue(new EventData(EventType.Layout)); + + app.ScrollWindow(scrollDelta); + + Globals.WaitForTimeout(); + VerifyQueue(expected); + } + + // B. Scroll up. + { + scrollDelta = 1; + expected.Enqueue(new EventData(EventType.UpdateScroll, 0, scrollDelta * rowsPerScroll)); + expected.Enqueue(new EventData(EventType.Layout)); + + app.ScrollWindow(scrollDelta); + + Globals.WaitForTimeout(); + VerifyQueue(expected); + } + } + + [TestMethod] + public void TestScrollByOverflow() + { + RunTest(TestScrollByOverflowImpl); + } + + private void TestScrollByOverflowImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal) + { + // Get original screen information + sbiexOriginal = app.GetScreenBufferInfo(); + short promptLineEnd = sbiexOriginal.dwCursorPosition.X; + promptLineEnd--; // prompt line ended one position left of cursor + + // Resize the window to only have two lines left at the bottom to test overflow when we echo some text + sbiex = sbiexOriginal; + sbiex.srWindow.Bottom = sbiex.dwCursorPosition.Y; + sbiex.srWindow.Bottom += 3; + app.SetScreenBufferInfo(sbiex); + + string echoText = "foo"; + string echoCommand = "echo"; + + int echoLine = sbiexOriginal.dwCursorPosition.Y + 1; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, echoLine, echoText.Length - 1, echoLine)); + expected.Enqueue(new EventData(EventType.UpdateScroll, 0, -1)); + int newPromptLine = echoLine + 2; + expected.Enqueue(new EventData(EventType.UpdateRegion, 0, newPromptLine, promptLineEnd, newPromptLine)); + expected.Enqueue(new EventData(EventType.Layout)); + expected.Enqueue(new EventData(EventType.CaretVisible, promptLineEnd + 1, newPromptLine)); + + // type command to echo foo and press enter + app.UIRoot.SendKeys($"{echoCommand} {echoText}"); + Globals.WaitForTimeout(); + received.Clear(); + app.UIRoot.SendKeys(Keys.Enter); + Globals.WaitForTimeout(); + + VerifyQueue(expected); + } + } +} diff --git a/src/host/ft_uia/app.config b/src/host/ft_uia/app.config new file mode 100644 index 000000000..bf061caaf --- /dev/null +++ b/src/host/ft_uia/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/host/ft_uia/packages.config b/src/host/ft_uia/packages.config new file mode 100644 index 000000000..e7a7baf8c --- /dev/null +++ b/src/host/ft_uia/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/host/ft_uia/product.pbxproj b/src/host/ft_uia/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/host/ft_uia/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/host/ft_uia/run.bat b/src/host/ft_uia/run.bat new file mode 100644 index 000000000..f6f06851b --- /dev/null +++ b/src/host/ft_uia/run.bat @@ -0,0 +1,2 @@ +copy %_NTTREE%\vtapp.exe %_NTTREE%\unittests\vtapp.exe +te %_NTTREE%\unittests\conhost.uia.tests.dll %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/host/ft_uia/sources b/src/host/ft_uia/sources new file mode 100644 index 000000000..4cb5e3bd5 --- /dev/null +++ b/src/host/ft_uia/sources @@ -0,0 +1,59 @@ +TARGETNAME = Microsoft.Console.Host.UIAutomationTests +TARGETTYPE = DYNLINK +TARGET_DESTINATION = UnitTests + +UNIVERSAL_TEST = 1 + +# The following lines are required to force PASS2 to occur, which is when the universal test testmd/spkg is generated +!IF 0 +SPKG_SOURCES= +!ENDIF + +MANAGED_CODE = 1 +TEST_CODE = 1 +URT_VER = 4.5 + +USER_CS_FLAGS = $(USER_CS_FLAGS) /define:__INSIDE_WINDOWS + +SOURCES = \ + Properties\AssemblyInfo.cs \ + Common\AutoHelpers.cs \ + Common\CheckBoxMeta.cs \ + Common\Globals.cs \ + Common\NativeMethods.cs \ + Common\RegistryHelper.cs \ + Common\ShortcutHelper.cs \ + Common\SliderMeta.cs \ + Common\VersionSelector.cs \ + Elements\CmdApp.cs \ + Elements\ColorsTab.cs \ + Elements\FontTab.cs \ + Elements\LayoutTab.cs \ + Elements\OptionsTab.cs \ + Elements\PropertiesDialog.cs \ + Elements\TabBase.cs \ + Elements\Tabs.cs \ + Elements\ViewportArea.cs \ + Elements\WinEventSystem.cs \ + ExperimentalTabTests.cs \ + Init.cs \ + MiscTests.cs \ + MouseWheelTests.cs \ + SelectionApiTests.cs \ + VirtualTerminalTests.cs \ + WinEventTests.cs \ + + +REFERENCES = $(CLR_REF_PATH)\System.metadata_dll; \ + $(CLR_REF_PATH)\System.Core.metadata_dll; \ + $(CLR_REF_PATH)\System.Data.metadata_dll; \ + $(CLR_REF_PATH)\System.Drawing.metadata_dll; \ + $(ONECORESDKTOOLS_INTERNAL_REF_PATH_L)\wextest\cue\wex.common.managed.metadata_dll; \ + $(ONECORESDKTOOLS_INTERNAL_REF_PATH_L)\wextest\cue\wex.logger.interop.metadata_dll; \ + $(ONECORESDKTOOLS_INTERNAL_REF_PATH_L)\wextest\cue\te.managed.metadata_dll; \ + $(ONECORESDKTOOLS_PRIVATE_REF_PATH_L)\WinAppDriver\appium-dotnet-driver.metadata_dll; \ + $(ONECORESDKTOOLS_PRIVATE_REF_PATH_L)\WinAppDriver\castle.core.metadata_dll; \ + $(ONECORESDKTOOLS_PRIVATE_REF_PATH_L)\WinAppDriver\newtonsoft.json.metadata_dll; \ + $(ONECORESDKTOOLS_PRIVATE_REF_PATH_L)\WinAppDriver\webdriver.metadata_dll; \ + $(ONECORESDKTOOLS_PRIVATE_REF_PATH_L)\WinAppDriver\webdriver.support.metadata_dll; + diff --git a/src/host/ft_uia/sources.dep b/src/host/ft_uia/sources.dep new file mode 100644 index 000000000..97b44c3be --- /dev/null +++ b/src/host/ft_uia/sources.dep @@ -0,0 +1,13 @@ +PUBLIC_PASS1_CONSUMES= \ + onecore\redist\mspartners\taef\ref\netfx4.5\te.managed|PASS1 \ + onecore\redist\mspartners\taef\ref\netfx4.5\wex.common.managed|PASS1 \ + onecore\redist\mspartners\taef\ref\netfx4.5\wex.logger.interop|PASS1 \ + onecore\sdktools\winappdriver\appium.webdriver|PASS1 \ + onecore\sdktools\winappdriver\castle.core|PASS1 \ + onecore\sdktools\winappdriver\newtonsoft.json|PASS1 \ + onecore\sdktools\winappdriver\selenium.support|PASS1 \ + onecore\sdktools\winappdriver\selenium.webdriver|PASS1 \ + +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ + diff --git a/src/host/ft_uia/testmd.definition b/src/host/ft_uia/testmd.definition new file mode 100644 index 000000000..5fccc01ee --- /dev/null +++ b/src/host/ft_uia/testmd.definition @@ -0,0 +1,23 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "Host-UIAutomationTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ + "Microsoft.Console.Tools.Nihilist", + "Microsoft.Console.Tools.VtApp", + "Microsoft.Test.WinAppDriver.ClientRuntimeLibraries", + "Microsoft.Test.WinAppDriver.Server" + ] + }, + "Logs": [ ], + "Plugins": [ ] +} \ No newline at end of file diff --git a/src/host/getset.cpp b/src/host/getset.cpp new file mode 100644 index 000000000..e36fab96f --- /dev/null +++ b/src/host/getset.cpp @@ -0,0 +1,2172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "getset.h" + +#include "_output.h" +#include "_stream.h" +#include "output.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "cmdline.h" + +#include "../types/inc/convert.hpp" +#include "../types/inc/viewport.hpp" + +#include "ApiRoutines.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// The following mask is used to test for valid text attributes. +#define VALID_TEXT_ATTRIBUTES (FG_ATTRS | BG_ATTRS | META_ATTRS) + +#define INPUT_MODES (ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT) +#define OUTPUT_MODES (ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN | ENABLE_LVB_GRID_WORLDWIDE) +#define PRIVATE_MODES (ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_EXTENDED_FLAGS) + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Retrieves the console input mode (settings that apply when manipulating the input buffer) +// Arguments: +// - context - The input buffer concerned +// - mode - Receives the mode flags set +void ApiRoutines::GetConsoleInputModeImpl(InputBuffer& context, ULONG& mode) noexcept +{ + try + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleMode); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + mode = context.InputMode; + + if (WI_IsFlagSet(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS)) + { + WI_SetFlag(mode, ENABLE_EXTENDED_FLAGS); + WI_SetFlagIf(mode, ENABLE_INSERT_MODE, gci.GetInsertMode()); + WI_SetFlagIf(mode, ENABLE_QUICK_EDIT_MODE, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE)); + WI_SetFlagIf(mode, ENABLE_AUTO_POSITION, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION)); + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves the console output mode (settings that apply when manipulating the output buffer) +// Arguments: +// - context - The output buffer concerned +// - mode - Receives the mode flags set +void ApiRoutines::GetConsoleOutputModeImpl(SCREEN_INFORMATION& context, ULONG& mode) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + mode = context.GetActiveBuffer().OutputMode; + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves the number of console event items in the input queue right now +// Arguments: +// - context - The input buffer concerned +// - event - The count of events in the queue +// Return Value: +// - S_OK or math failure. +[[nodiscard]] +HRESULT ApiRoutines::GetNumberOfConsoleInputEventsImpl(const InputBuffer& context, ULONG& events) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + const auto readyEventCount = context.GetNumberOfReadyEvents(); + RETURN_IF_FAILED(SizeTToULong(readyEventCount, &events)); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves metadata associated with the output buffer (size, default colors, etc.) +// Arguments: +// - context - The output buffer concerned +// - data - Receives structure filled with metadata about the output buffer +void ApiRoutines::GetConsoleScreenBufferInfoExImpl(const SCREEN_INFORMATION& context, + CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + data.bFullscreenSupported = FALSE; // traditional full screen with the driver support is no longer supported. + // see MSFT: 19918103 + // Make sure to use the active buffer here. There are clients that will + // use WINDOW_SIZE_EVENTs as a signal to then query the console + // with GetConsoleScreenBufferInfoEx to get the actual viewport + // size. + // If they're in the alt buffer, then when they query in that way, the + // value they'll get is the main buffer's size, which isn't updated + // until we switch back to it. + context.GetActiveBuffer().GetScreenBufferInformation(&data.dwSize, + &data.dwCursorPosition, + &data.srWindow, + &data.wAttributes, + &data.dwMaximumWindowSize, + &data.wPopupAttributes, + data.ColorTable); + // Callers of this function expect to recieve an exclusive rect, not an inclusive one. + data.srWindow.Right += 1; + data.srWindow.Bottom += 1; + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves information about the console cursor's display state +// Arguments: +// - context - The output buffer concerned +// - size - The size as a percentage of the total possible height (0-100 for percentages). +// - isVisible - Whether the cursor is displayed or hidden +void ApiRoutines::GetConsoleCursorInfoImpl(const SCREEN_INFORMATION& context, + ULONG& size, + bool& isVisible) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + size = context.GetActiveBuffer().GetTextBuffer().GetCursor().GetSize(); + isVisible = context.GetTextBuffer().GetCursor().IsVisible(); + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves information about the selected area in the console +// Arguments: +// - consoleSelectionInfo - contains flags, anchors, and area to describe selection area +void ApiRoutines::GetConsoleSelectionInfoImpl(CONSOLE_SELECTION_INFO& consoleSelectionInfo) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + const auto& selection = Selection::Instance(); + if (selection.IsInSelectingState()) + { + consoleSelectionInfo.dwFlags = selection.GetPublicSelectionFlags(); + + WI_SetFlag(consoleSelectionInfo.dwFlags, CONSOLE_SELECTION_IN_PROGRESS); + + consoleSelectionInfo.dwSelectionAnchor = selection.GetSelectionAnchor(); + consoleSelectionInfo.srSelection = selection.GetSelectionRectangle(); + } + else + { + ZeroMemory(&consoleSelectionInfo, sizeof(consoleSelectionInfo)); + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves the number of buttons on the mouse as reported by the system +// Arguments: +// - buttons - Count of buttons +void ApiRoutines::GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + buttons = ServiceLocator::LocateSystemConfigurationProvider()->GetNumberOfMouseButtons(); + } + CATCH_LOG(); +} + +// Routine Description: +// - Retrieves information about the a known font based on index +// Arguments: +// - context - The output buffer concerned +// - index - We only accept 0 now as we don't keep a list of fonts in memory. +// - size - The X by Y pixel size of the font +// Return Value: +// - S_OK, E_INVALIDARG or code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleFontSizeImpl(const SCREEN_INFORMATION& context, + const DWORD index, + COORD& size) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + if (index == 0) + { + // As of the November 2015 renderer system, we only have a single font at index 0. + size = context.GetActiveBuffer().GetCurrentFont().GetUnscaledSize(); + return S_OK; + } + else + { + // Invalid font is 0,0 with STATUS_INVALID_PARAMETER + size = { 0 }; + return E_INVALIDARG; + } + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves information about the console cursor's display state +// Arguments: +// - context - The output buffer concerned +// - isForMaximumWindowSize - Returns the maximum number of characters in the largest window size if true. Otherwise, it's the size of the font. +// - consoleFontInfoEx - structure containing font information like size, family, weight, etc. +// Return Value: +// - S_OK, string copy failure code or code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetCurrentConsoleFontExImpl(const SCREEN_INFORMATION& context, + const bool isForMaximumWindowSize, + CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + const SCREEN_INFORMATION& activeScreenInfo = context.GetActiveBuffer(); + + COORD WindowSize; + if (isForMaximumWindowSize) + { + WindowSize = activeScreenInfo.GetMaxWindowSizeInCharacters(); + } + else + { + WindowSize = activeScreenInfo.GetCurrentFont().GetUnscaledSize(); + } + consoleFontInfoEx.dwFontSize = WindowSize; + + consoleFontInfoEx.nFont = 0; + + const FontInfo& fontInfo = activeScreenInfo.GetCurrentFont(); + consoleFontInfoEx.FontFamily = fontInfo.GetFamily(); + consoleFontInfoEx.FontWeight = fontInfo.GetWeight(); + + RETURN_IF_FAILED(StringCchCopyW(consoleFontInfoEx.FaceName, ARRAYSIZE(consoleFontInfoEx.FaceName), fontInfo.GetFaceName())); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the current font to be used for drawing +// Arguments: +// - context - The output buffer concerned +// - isForMaximumWindowSize - Obsolete. +// - consoleFontInfoEx - structure containing font information like size, family, weight, etc. +// Return Value: +// - S_OK, string copy failure code or code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetCurrentConsoleFontExImpl(IConsoleOutputObject& context, + const bool /*isForMaximumWindowSize*/, + const CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + SCREEN_INFORMATION& activeScreenInfo = context.GetActiveBuffer(); + + WCHAR FaceName[ARRAYSIZE(consoleFontInfoEx.FaceName)]; + RETURN_IF_FAILED(StringCchCopyW(FaceName, ARRAYSIZE(FaceName), consoleFontInfoEx.FaceName)); + + FontInfo fi(FaceName, + static_cast(consoleFontInfoEx.FontFamily), + consoleFontInfoEx.FontWeight, + consoleFontInfoEx.dwFontSize, + gci.OutputCP); + + // TODO: MSFT: 9574827 - should this have a failure case? + activeScreenInfo.UpdateFont(&fi); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the input mode for the console +// Arguments: +// - context - The input buffer concerned +// - mode - flags that change behavior of the buffer +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleInputModeImpl(InputBuffer& context, const ULONG mode) noexcept +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + if (WI_IsAnyFlagSet(mode, PRIVATE_MODES)) + { + WI_SetFlag(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS); + + WI_UpdateFlag(gci.Flags, CONSOLE_QUICK_EDIT_MODE, WI_IsFlagSet(mode, ENABLE_QUICK_EDIT_MODE)); + WI_UpdateFlag(gci.Flags, CONSOLE_AUTO_POSITION, WI_IsFlagSet(mode, ENABLE_AUTO_POSITION)); + + const bool PreviousInsertMode = gci.GetInsertMode(); + gci.SetInsertMode(WI_IsFlagSet(mode, ENABLE_INSERT_MODE)); + if (gci.GetInsertMode() != PreviousInsertMode) + { + gci.GetActiveOutputBuffer().SetCursorDBMode(false); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().SetInsertMode(gci.GetInsertMode()); + } + } + } + else + { + WI_ClearFlag(gci.Flags, CONSOLE_USE_PRIVATE_FLAGS); + } + + context.InputMode = mode; + WI_ClearAllFlags(context.InputMode, PRIVATE_MODES); + + // NOTE: For compatibility reasons, we need to set the modes and then return the error codes, not the other way around + // as might be expected. + // This is a bug from a long time ago and some applications depend on this functionality to operate properly. + // --- + // A prime example of this is that PSReadline module in Powershell will set the invalid mode 0x1e4 + // which includes 0x4 for ECHO_INPUT but turns off 0x2 for LINE_INPUT. This is invalid, but PSReadline + // relies on it to properly receive the ^C printout and make a new line when the user presses Ctrl+C. + { + // Flags we don't understand are invalid. + RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(mode, ~(INPUT_MODES | PRIVATE_MODES))); + + // ECHO on with LINE off is invalid. + RETURN_HR_IF(E_INVALIDARG, WI_IsFlagSet(mode, ENABLE_ECHO_INPUT) && WI_IsFlagClear(mode, ENABLE_LINE_INPUT)); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the output mode for the console +// Arguments: +// - context - The output buffer concerned +// - mode - flags that change behavior of the buffer +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleOutputModeImpl(SCREEN_INFORMATION& context, const ULONG mode) noexcept +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Flags we don't understand are invalid. + RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(mode, ~OUTPUT_MODES)); + + SCREEN_INFORMATION& screenInfo = context.GetActiveBuffer(); + const DWORD dwOldMode = screenInfo.OutputMode; + const DWORD dwNewMode = mode; + + screenInfo.OutputMode = dwNewMode; + + // if we're moving from VT on->off + if (WI_IsFlagClear(dwNewMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) && WI_IsFlagSet(dwOldMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + // jiggle the handle + screenInfo.GetStateMachine().ResetState(); + screenInfo.ClearTabStops(); + } + // if we're moving from VT off->on + else if (WI_IsFlagSet(dwNewMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) && + WI_IsFlagClear(dwOldMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + screenInfo.SetDefaultVtTabStops(); + } + + gci.SetVirtTermLevel(WI_IsFlagSet(dwNewMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) ? 1 : 0); + gci.SetAutomaticReturnOnNewline(WI_IsFlagSet(screenInfo.OutputMode, DISABLE_NEWLINE_AUTO_RETURN) ? false : true); + gci.SetGridRenderingAllowedWorldwide(WI_IsFlagSet(screenInfo.OutputMode, ENABLE_LVB_GRID_WORLDWIDE)); + + // if we changed rendering modes then redraw the output buffer, + // but only do this if we're not in conpty mode. + if (!gci.IsInVtIoMode() && + (WI_IsFlagSet(dwNewMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) != WI_IsFlagSet(dwOldMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING) || + WI_IsFlagSet(dwNewMode, ENABLE_LVB_GRID_WORLDWIDE) != WI_IsFlagSet(dwOldMode, ENABLE_LVB_GRID_WORLDWIDE)) ) + { + auto* pRender = ServiceLocator::LocateGlobals().pRender; + if (pRender) + { + pRender->TriggerRedrawAll(); + } + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the given output buffer as the active one +// Arguments: +// - context - The output buffer concerned +void ApiRoutines::SetConsoleActiveScreenBufferImpl(SCREEN_INFORMATION& newContext) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + SetActiveScreenBuffer(newContext.GetActiveBuffer()); + } + CATCH_LOG(); +} + +// Routine Description: +// - Clears all items out of the input buffer queue +// Arguments: +// - context - The input buffer concerned +void ApiRoutines::FlushConsoleInputBuffer(InputBuffer& context) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + context.Flush(); + } + CATCH_LOG(); +} + +// Routine Description: +// - Gets the largest possible window size in characters. +// Arguments: +// - context - The output buffer concerned +// - size - receives the size in character count (rows/columns) +void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& context, + COORD& size) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + const SCREEN_INFORMATION& screenInfo = context.GetActiveBuffer(); + + size = screenInfo.GetLargestWindowSizeInCharacters(); + } + CATCH_LOG(); +} + +// Routine Description: +// - Sets the size of the output buffer (screen buffer) in rows/columns +// Arguments: +// - context - The output buffer concerned +// - size - size in character rows and columns +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleScreenBufferSizeImpl(SCREEN_INFORMATION& context, + const COORD size) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + SCREEN_INFORMATION& screenInfo = context.GetActiveBuffer(); + + // see MSFT:17415266 + // We only really care about the minimum window size if we have a head. + if (!ServiceLocator::LocateGlobals().IsHeadless()) + { + COORD const coordMin = screenInfo.GetMinWindowSizeInCharacters(); + // Make sure requested screen buffer size isn't smaller than the window. + RETURN_HR_IF(E_INVALIDARG, (size.X < screenInfo.GetViewport().Width() || + size.Y < screenInfo.GetViewport().Height() || + size.Y < coordMin.Y || + size.X < coordMin.X)); + } + + // Ensure the requested size isn't larger than we can handle in our data type. + RETURN_HR_IF(E_INVALIDARG, (size.X == SHORT_MAX || size.Y == SHORT_MAX)); + + // Only do the resize if we're actually changing one of the dimensions + COORD const coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); + if (size.X != coordScreenBufferSize.X || size.Y != coordScreenBufferSize.Y) + { + RETURN_NTSTATUS(screenInfo.ResizeScreenBuffer(size, TRUE)); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets metadata information on the output buffer +// Arguments: +// - context - The output buffer concerned +// - data - metadata information structure like buffer size, viewport size, colors, and more. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleScreenBufferInfoExImpl(SCREEN_INFORMATION& context, + const CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept +{ + try + { + RETURN_HR_IF(E_INVALIDARG, (data.dwSize.X == 0 || + data.dwSize.Y == 0 || + data.dwSize.X == SHRT_MAX || + data.dwSize.Y == SHRT_MAX)); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + + const COORD coordScreenBufferSize = context.GetBufferSize().Dimensions(); + const COORD requestedBufferSize = data.dwSize; + if (requestedBufferSize.X != coordScreenBufferSize.X || + requestedBufferSize.Y != coordScreenBufferSize.Y) + { + CommandLine& commandLine = CommandLine::Instance(); + + commandLine.Hide(FALSE); + + LOG_IF_FAILED(context.ResizeScreenBuffer(data.dwSize, TRUE)); + + commandLine.Show(); + } + const COORD newBufferSize = context.GetBufferSize().Dimensions(); + + gci.SetColorTable(data.ColorTable, ARRAYSIZE(data.ColorTable)); + + context.SetDefaultAttributes({ data.wAttributes }, { data.wPopupAttributes }); + + const Viewport requestedViewport = Viewport::FromExclusive(data.srWindow); + + COORD NewSize = requestedViewport.Dimensions(); + // If we have a window, clamp the requested viewport to the max window size + if (!ServiceLocator::LocateGlobals().IsHeadless()) + { + NewSize.X = std::min(NewSize.X, data.dwMaximumWindowSize.X); + NewSize.Y = std::min(NewSize.Y, data.dwMaximumWindowSize.Y); + } + + // If wrap text is on, then the window width must be the same size as the buffer width + if (gci.GetWrapText()) + { + NewSize.X = newBufferSize.X; + } + + if (NewSize.X != context.GetViewport().Width() || + NewSize.Y != context.GetViewport().Height()) + { + context.SetViewportSize(&NewSize); + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->UpdateWindowSize(NewSize); + } + } + + // Despite the fact that this API takes in a srWindow for the viewport, it traditionally actually doesn't set + // anything using that member - for moving the viewport, you need SetConsoleWindowInfo + // (see https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx and DoSrvSetConsoleWindowInfo) + // Note that it also doesn't set cursor position. + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the cursor position in the given output buffer +// Arguments: +// - context - The output buffer concerned +// - position - The X/Y (row/column) position in the buffer to place the cursor +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleCursorPositionImpl(SCREEN_INFORMATION& context, + const COORD position) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + const COORD coordScreenBufferSize = context.GetBufferSize().Dimensions(); + RETURN_HR_IF(E_INVALIDARG, (position.X >= coordScreenBufferSize.X || + position.Y >= coordScreenBufferSize.Y || + position.X < 0 || + position.Y < 0)); + + // MSFT: 15813316 - Try to use this SetCursorPosition call to inherit the cursor position. + RETURN_IF_FAILED(gci.GetVtIo()->SetCursorPosition(position)); + + RETURN_IF_NTSTATUS_FAILED(context.SetCursorPosition(position, true)); + + LOG_IF_FAILED(ConsoleImeResizeCompStrView()); + + COORD WindowOrigin; + WindowOrigin.X = 0; + WindowOrigin.Y = 0; + { + const SMALL_RECT currentViewport = context.GetViewport().ToInclusive(); + if (currentViewport.Left > position.X) + { + WindowOrigin.X = position.X - currentViewport.Left; + } + else if (currentViewport.Right < position.X) + { + WindowOrigin.X = position.X - currentViewport.Right; + } + + if (currentViewport.Top > position.Y) + { + WindowOrigin.Y = position.Y - currentViewport.Top; + } + else if (currentViewport.Bottom < position.Y) + { + WindowOrigin.Y = position.Y - currentViewport.Bottom; + } + } + + RETURN_IF_NTSTATUS_FAILED(context.SetViewportOrigin(false, WindowOrigin, true)); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets metadata on the cursor +// Arguments: +// - context - The output buffer concerned +// - size - Height percentage of the displayed cursor (when visible) +// - isVisible - Whether or not the cursor should be displayed +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleCursorInfoImpl(SCREEN_INFORMATION& context, + const ULONG size, + const bool isVisible) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // If more than 100% or less than 0% cursor height, reject it. + RETURN_HR_IF(E_INVALIDARG, (size > 100 || size == 0)); + + context.SetCursorInformation(size, isVisible); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the viewport/window information for displaying a portion of the output buffer visually +// Arguments: +// - context - The output buffer concerned +// - isAbsolute - Coordinates are based on the entire screen buffer (origin 0,0) if true. +// - If false, coordinates are a delta from the existing viewport position +// - windowRect - Updated viewport rectangle information +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleWindowInfoImpl(SCREEN_INFORMATION& context, + const bool isAbsolute, + const SMALL_RECT& windowRect) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + Globals& g = ServiceLocator::LocateGlobals(); + SMALL_RECT Window = windowRect; + + if (!isAbsolute) + { + SMALL_RECT currentViewport = context.GetViewport().ToInclusive(); + Window.Left += currentViewport.Left; + Window.Right += currentViewport.Right; + Window.Top += currentViewport.Top; + Window.Bottom += currentViewport.Bottom; + } + + RETURN_HR_IF(E_INVALIDARG, (Window.Right < Window.Left || Window.Bottom < Window.Top)); + + COORD NewWindowSize; + NewWindowSize.X = (SHORT)(CalcWindowSizeX(Window)); + NewWindowSize.Y = (SHORT)(CalcWindowSizeY(Window)); + + // see MSFT:17415266 + // If we have a actual head, we care about the maximum size the window can be. + // if we're headless, not so much. However, GetMaxWindowSizeInCharacters + // will only return the buffer size, so we can't use that to clip the arg here. + // So only clip the requested size if we're not headless + if (!g.IsHeadless()) + { + COORD const coordMax = context.GetMaxWindowSizeInCharacters(); + RETURN_HR_IF(E_INVALIDARG, (NewWindowSize.X > coordMax.X || NewWindowSize.Y > coordMax.Y)); + + } + else if (g.getConsoleInformation().IsInVtIoMode()) + { + // SetViewportRect doesn't cause the buffer to resize. Manually resize the buffer. + RETURN_IF_NTSTATUS_FAILED(context.ResizeScreenBuffer(Viewport::FromInclusive(Window).Dimensions(), false)); + } + + // Even if it's the same size, we need to post an update in case the scroll bars need to go away. + context.SetViewport(Viewport::FromInclusive(Window), true); + if (context.IsActiveScreenBuffer()) + { + // TODO: MSFT: 9574827 - shouldn't we be looking at or at least logging the failure codes here? (Or making them non-void?) + context.PostUpdateWindowSize(); + WriteToScreen(context, context.GetViewport()); + } + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Moves a portion of text from one part of the output buffer to another +// Arguments: +// - context - The output buffer concerned +// - source - The rectangular region to copy from +// - target - The top left corner of the destination to paste the copy (source) +// - clip - The rectangle inside which all operations should be bounded (or no bounds if not given) +// - fillCharacter - Fills in the region left behind when the source is "lifted" out of its original location. The symbol to display. +// - fillAttribute - Fills in the region left behind when the source is "lifted" out of its original location. The color to use. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::ScrollConsoleScreenBufferAImpl(SCREEN_INFORMATION& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const char fillCharacter, + const WORD fillAttribute) noexcept +{ + try + { + wchar_t const unicodeFillCharacter = CharToWchar(&fillCharacter, 1); + + return ScrollConsoleScreenBufferWImpl(context, source, target, clip, unicodeFillCharacter, fillAttribute); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Moves a portion of text from one part of the output buffer to another +// Arguments: +// - context - The output buffer concerned +// - source - The rectangular region to copy from +// - target - The top left corner of the destination to paste the copy (source) +// - clip - The rectangle inside which all operations should be bounded (or no bounds if not given) +// - fillCharacter - Fills in the region left behind when the source is "lifted" out of its original location. The symbol to display. +// - fillAttribute - Fills in the region left behind when the source is "lifted" out of its original location. The color to use. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::ScrollConsoleScreenBufferWImpl(SCREEN_INFORMATION& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const wchar_t fillCharacter, + const WORD fillAttribute) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + + TextAttribute useThisAttr(fillAttribute); + + // Here we're being a little clever - similar to FillConsoleOutputAttributeImpl + // Because RGB/default color can't roundtrip the API, certain VT + // sequences will forget the RGB color because their first call to + // GetScreenBufferInfo returned a legacy attr. + // If they're calling this with the legacy attrs version of our current + // attributes, they likely wanted to use the full version of + // our current attributes, whether that be RGB or _default_ colored. + // This could create a scenario where someone emitted RGB with VT, + // THEN used the API to ScrollConsoleOutput with the legacy attrs, + // and DIDN'T want the RGB color. As in FillConsoleOutputAttribute, + // this scenario is highly unlikely, and we can reasonably do this + // on their behalf. + // see MSFT:19853701 + if (context.InVTMode()) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto currentAttributes = context.GetAttributes(); + const auto bufferLegacy = gci.GenerateLegacyAttributes(currentAttributes); + if (bufferLegacy == fillAttribute) + { + useThisAttr = currentAttributes; + } + } + + ScrollRegion(context, source, clip, target, fillCharacter, useThisAttr); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Adjusts the default color used for future text written to this output buffer +// Arguments: +// - context - The output buffer concerned +// - attribute - Color information +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleTextAttributeImpl(SCREEN_INFORMATION& context, + const WORD attribute) noexcept +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(attribute, ~VALID_TEXT_ATTRIBUTES)); + + const TextAttribute attr{ attribute }; + context.SetAttributes(attr); + + gci.ConsoleIme.RefreshAreaAttributes(); + + return S_OK; + } + CATCH_RETURN(); +} + +void DoSrvPrivateSetLegacyAttributes(SCREEN_INFORMATION& screenInfo, + const WORD Attribute, + const bool fForeground, + const bool fBackground, + const bool fMeta) +{ + auto& buffer = screenInfo.GetActiveBuffer(); + const TextAttribute OldAttributes = buffer.GetAttributes(); + TextAttribute NewAttributes = OldAttributes; + + NewAttributes.SetLegacyAttributes(Attribute, fForeground, fBackground, fMeta); + + buffer.SetAttributes(NewAttributes); +} + +void DoSrvPrivateSetDefaultAttributes(SCREEN_INFORMATION& screenInfo, + const bool fForeground, + const bool fBackground) +{ + auto& buffer = screenInfo.GetActiveBuffer(); + TextAttribute NewAttributes = buffer.GetAttributes(); + if (fForeground) + { + NewAttributes.SetDefaultForeground(); + } + if (fBackground) + { + NewAttributes.SetDefaultBackground(); + } + buffer.SetAttributes(NewAttributes); +} + +void DoSrvPrivateSetConsoleXtermTextAttribute(SCREEN_INFORMATION& screenInfo, + const int iXtermTableEntry, + const bool fIsForeground) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& buffer = screenInfo.GetActiveBuffer(); + TextAttribute NewAttributes = buffer.GetAttributes(); + + COLORREF rgbColor; + if (iXtermTableEntry < COLOR_TABLE_SIZE) + { + //Convert the xterm index to the win index + WORD iWinEntry = ::XtermToWindowsIndex(iXtermTableEntry); + + rgbColor = gci.GetColorTableEntry(iWinEntry); + } + else + { + rgbColor = gci.GetColorTableEntry(iXtermTableEntry); + } + + NewAttributes.SetColor(rgbColor, fIsForeground); + + buffer.SetAttributes(NewAttributes); +} + +void DoSrvPrivateSetConsoleRGBTextAttribute(SCREEN_INFORMATION& screenInfo, + const COLORREF rgbColor, + const bool fIsForeground) +{ + auto& buffer = screenInfo.GetActiveBuffer(); + + TextAttribute NewAttributes = buffer.GetAttributes(); + NewAttributes.SetColor(rgbColor, fIsForeground); + buffer.SetAttributes(NewAttributes); +} + +void DoSrvPrivateBoldText(SCREEN_INFORMATION& screenInfo, const bool bolded) +{ + auto& buffer = screenInfo.GetActiveBuffer(); + auto attrs = buffer.GetAttributes(); + if (bolded) + { + attrs.Embolden(); + } + else + { + attrs.Debolden(); + } + buffer.SetAttributes(attrs); +} + +// Routine Description: +// - Sets the codepage used for translating text when calling A versions of functions affecting the output buffer. +// Arguments: +// - codepage - The codepage +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleOutputCodePageImpl(const ULONG codepage) noexcept +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Return if it's not known as a valid codepage ID. + RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage))); + + // Do nothing if no change. + if (gci.OutputCP != codepage) + { + // Set new code page + gci.OutputCP = codepage; + + SetConsoleCPInfo(TRUE); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the codepage used for translating text when calling A versions of functions affecting the input buffer. +// Arguments: +// - codepage - The codepage +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleInputCodePageImpl(const ULONG codepage) noexcept +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Return if it's not known as a valid codepage ID. + RETURN_HR_IF(E_INVALIDARG, !(IsValidCodePage(codepage))); + + // Do nothing if no change. + if (gci.CP != codepage) + { + // Set new code page + gci.CP = codepage; + + SetConsoleCPInfo(FALSE); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets the codepage used for translating text when calling A versions of functions affecting the input buffer. +// Arguments: +// - codepage - The codepage +void ApiRoutines::GetConsoleInputCodePageImpl(ULONG& codepage) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + codepage = gci.CP; + } + CATCH_LOG(); +} + +void DoSrvGetConsoleOutputCodePage(_Out_ unsigned int* const pCodePage) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + *pCodePage = gci.OutputCP; +} + +// Routine Description: +// - Gets the codepage used for translating text when calling A versions of functions affecting the output buffer. +// Arguments: +// - codepage - The codepage +void ApiRoutines::GetConsoleOutputCodePageImpl(ULONG& codepage) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + unsigned int uiCodepage; + DoSrvGetConsoleOutputCodePage(&uiCodepage); + codepage = uiCodepage; + } + CATCH_LOG(); +} + +// Routine Description: +// - Gets the window handle ID for the console +// Arguments: +// - hwnd - The window handle ID +void ApiRoutines::GetConsoleWindowImpl(HWND& hwnd) noexcept +{ + try + { + // Set return to null before we do anything in case of failures/errors. + hwnd = nullptr; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + const IConsoleWindow* pWindow = ServiceLocator::LocateConsoleWindow(); + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (pWindow != nullptr) + { + hwnd = pWindow->GetWindowHandle(); + } + else + { + // Some applications will fail silently if this API returns 0 (cygwin) + // If we're in pty mode, we need to return a fake window handle that + // doesn't actually do anything, but is a unique HWND to this + // console, so that they know that this console is in fact a real + // console window. + if (gci.IsInVtIoMode()) + { + hwnd = ServiceLocator::LocatePseudoWindow(); + } + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Gets metadata about the storage of command history for cooked read modes +// Arguments: +// - consoleHistoryInformation - metadata pertaining to the number of history buffers and their size and modes. +void ApiRoutines::GetConsoleHistoryInfoImpl(CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + consoleHistoryInfo.HistoryBufferSize = gci.GetHistoryBufferSize(); + consoleHistoryInfo.NumberOfHistoryBuffers = gci.GetNumberOfHistoryBuffers(); + WI_SetFlagIf(consoleHistoryInfo.dwFlags, HISTORY_NO_DUP_FLAG, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)); + } + CATCH_LOG(); +} + +// Routine Description: +// - Sets metadata about the storage of command history for cooked read modes +// Arguments: +// - consoleHistoryInformation - metadata pertaining to the number of history buffers and their size and modes. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +HRESULT ApiRoutines::SetConsoleHistoryInfoImpl(const CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept +{ + try + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + RETURN_HR_IF(E_INVALIDARG, consoleHistoryInfo.HistoryBufferSize > SHORT_MAX); + RETURN_HR_IF(E_INVALIDARG, consoleHistoryInfo.NumberOfHistoryBuffers > SHORT_MAX); + RETURN_HR_IF(E_INVALIDARG, WI_IsAnyFlagSet(consoleHistoryInfo.dwFlags, ~CHI_VALID_FLAGS)); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + CommandHistory::s_ResizeAll(consoleHistoryInfo.HistoryBufferSize); + gci.SetNumberOfHistoryBuffers(consoleHistoryInfo.NumberOfHistoryBuffers); + + WI_UpdateFlag(gci.Flags, CONSOLE_HISTORY_NODUP, WI_IsFlagSet(consoleHistoryInfo.dwFlags, HISTORY_NO_DUP_FLAG)); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets whether or not the console is full screen +// Arguments: +// - flags - Field contains full screen flag or doesn't. +// NOTE: This was in private.c, but turns out to be a public API: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683164(v=vs.85).aspx +void ApiRoutines::GetConsoleDisplayModeImpl(ULONG& flags) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Initialize flags portion of structure + flags = 0; + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr && pWindow->IsInFullscreen()) + { + WI_SetFlag(flags, CONSOLE_FULLSCREEN_MODE); + } + } + CATCH_LOG(); +} + +// Routine Description: +// - This routine sets the console display mode for an output buffer. +// - This API is only supported on x86 machines. +// Parameters: +// - context - Supplies a console output handle. +// - flags - Specifies the display mode. Options are: +// CONSOLE_FULLSCREEN_MODE - data is displayed fullscreen +// CONSOLE_WINDOWED_MODE - data is displayed in a window +// - newSize - On output, contains the new dimensions of the screen buffer. The dimensions are in rows and columns for textmode screen buffers. +// Return value: +// - TRUE - The operation was successful. +// - FALSE/nullptr - The operation failed. Extended error status is available using GetLastError. +// NOTE: +// - This was in private.c, but turns out to be a public API: +// - See: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686028(v=vs.85).aspx +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleDisplayModeImpl(SCREEN_INFORMATION& context, + const ULONG flags, + COORD& newSize) noexcept +{ + try + { + // SetIsFullscreen() below ultimately calls SetwindowLong, which ultimately calls SendMessage(). If we retain + // the console lock, we'll deadlock since ConsoleWindowProc takes the lock before processing messages. Instead, + // we'll release early. + LockConsole(); + { + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + SCREEN_INFORMATION& screenInfo = context.GetActiveBuffer(); + + newSize = screenInfo.GetBufferSize().Dimensions(); + RETURN_HR_IF(E_INVALIDARG, !(screenInfo.IsActiveScreenBuffer())); + } + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (WI_IsFlagSet(flags, CONSOLE_FULLSCREEN_MODE)) + { + if (pWindow != nullptr) + { + pWindow->SetIsFullscreen(true); + } + } + else if (WI_IsFlagSet(flags, CONSOLE_WINDOWED_MODE)) + { + if (pWindow != nullptr) + { + pWindow->SetIsFullscreen(false); + } + } + else + { + RETURN_HR(E_INVALIDARG); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - A private API call for changing the cursor keys input mode between normal and application mode. +// The cursor keys are the arrows, plus Home and End. +// Parameters: +// - fApplicationMode - set to true to enable Application Mode Input, false for Numeric Mode Input. +// Return value: +// - True if handled successfully. False otherwise. +[[nodiscard]] +NTSTATUS DoSrvPrivateSetCursorKeysMode(_In_ bool fApplicationMode) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.pInputBuffer == nullptr) + { + return STATUS_UNSUCCESSFUL; + } + gci.pInputBuffer->GetTerminalInput().ChangeCursorKeysMode(fApplicationMode); + return STATUS_SUCCESS; +} + +// Routine Description: +// - A private API call for changing the keypad input mode between numeric and application mode. +// This controls what the keys on the numpad translate to. +// Parameters: +// - fApplicationMode - set to true to enable Application Mode Input, false for Numeric Mode Input. +// Return value: +// - True if handled successfully. False otherwise. +[[nodiscard]] +NTSTATUS DoSrvPrivateSetKeypadMode(_In_ bool fApplicationMode) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.pInputBuffer == nullptr) + { + return STATUS_UNSUCCESSFUL; + } + gci.pInputBuffer->GetTerminalInput().ChangeKeypadMode(fApplicationMode); + return STATUS_SUCCESS; +} + +// Routine Description: +// - A private API call for making the cursor visible or not. Does not modify +// blinking state. +// Parameters: +// - show - set to true to make the cursor visible, false to hide. +// Return value: +// - +void DoSrvPrivateShowCursor(SCREEN_INFORMATION& screenInfo, const bool show) noexcept +{ + screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor().SetIsVisible(show); +} + +// Routine Description: +// - A private API call for enabling or disabling the cursor blinking. +// Parameters: +// - fEnable - set to true to enable blinking, false to disable +// Return value: +// - True if handled successfully. False otherwise. +void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool fEnable) +{ + screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor().SetBlinkingAllowed(fEnable); + screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor().SetIsOn(!fEnable); +} + +// Routine Description: +// - A private API call for setting the top and bottom scrolling margins for +// the current page. This creates a subsection of the screen that scrolls +// when input reaches the end of the region, leaving the rest of the screen +// untouched. +// Currently only accessible through the use of ANSI sequence DECSTBM +// Parameters: +// - psrScrollMargins - A rect who's Top and Bottom members will be used to set +// the new values of the top and bottom margins. If (0,0), then the margins +// will be disabled. NOTE: This is a rect in the case that we'll need the +// left and right margins in the future. +// Return value: +// - True if handled successfully. False otherwise. +[[nodiscard]] +NTSTATUS DoSrvPrivateSetScrollingRegion(SCREEN_INFORMATION& screenInfo, const SMALL_RECT* const psrScrollMargins) +{ + NTSTATUS Status = STATUS_SUCCESS; + + if (psrScrollMargins->Top > psrScrollMargins->Bottom) + { + Status = STATUS_INVALID_PARAMETER; + } + if (NT_SUCCESS(Status)) + { + SMALL_RECT srScrollMargins = screenInfo.GetRelativeScrollMargins().ToInclusive(); + srScrollMargins.Top = psrScrollMargins->Top; + srScrollMargins.Bottom = psrScrollMargins->Bottom; + screenInfo.GetActiveBuffer().SetScrollMargins(Viewport::FromInclusive(srScrollMargins)); + } + + return Status; +} + +// Routine Description: +// - A private API call for performing a "Reverse line feed", essentially, the opposite of '\n'. +// Moves the cursor up one line, and tries to keep its position in the line +// Parameters: +// - screenInfo - a pointer to the screen buffer that should perform the reverse line feed +// Return value: +// - True if handled successfully. False otherwise. +[[nodiscard]] +NTSTATUS DoSrvPrivateReverseLineFeed(SCREEN_INFORMATION& screenInfo) +{ + NTSTATUS Status = STATUS_SUCCESS; + + const SMALL_RECT viewport = screenInfo.GetActiveBuffer().GetViewport().ToInclusive(); + const COORD oldCursorPosition = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + const COORD newCursorPosition = { oldCursorPosition.X, oldCursorPosition.Y - 1 }; + + // If the cursor is at the top of the viewport, we don't want to shift the viewport up. + // We want it to stay exactly where it is. + // In that case, shift the buffer contents down, to emulate inserting a line + // at the top of the buffer. + if (oldCursorPosition.Y > viewport.Top) + { + // Cursor is below the top line of the viewport + Status = AdjustCursorPosition(screenInfo, newCursorPosition, TRUE, nullptr); + } + else + { + const auto margins = screenInfo.GetAbsoluteScrollMargins(); + const bool marginsSet = margins.BottomInclusive() > margins.Top(); + + // If we don't have margins, or the cursor is within the boundaries of the margins + // It's important to check if the cursor is in the margins, + // If it's not, but the margins are set, then we don't want to scroll anything + if (!marginsSet || margins.IsInBounds(oldCursorPosition)) + { + // Cursor is at the top of the viewport + const COORD bufferSize = screenInfo.GetBufferSize().Dimensions(); + // Rectangle to cut out of the existing buffer + SMALL_RECT srScroll; + srScroll.Left = 0; + srScroll.Right = bufferSize.X; + srScroll.Top = viewport.Top; + srScroll.Bottom = viewport.Bottom - 1; + // Paste coordinate for cut text above + COORD coordDestination; + coordDestination.X = 0; + coordDestination.Y = viewport.Top + 1; + + SMALL_RECT srClip = viewport; + + Status = NTSTATUS_FROM_HRESULT(ServiceLocator::LocateGlobals().api.ScrollConsoleScreenBufferWImpl(screenInfo, + srScroll, + coordDestination, + srClip, + UNICODE_SPACE, + screenInfo.GetAttributes().GetLegacyAttributes())); + } + } + return Status; +} + +// Routine Description: +// - A private API call for moving the cursor vertically in the buffer. This is +// because the vertical cursor movements in VT are constrained by the +// scroll margins, while the absolute positioning is not. +// Parameters: +// - screenInfo - a reference to the screen buffer we should move the cursor for +// - lines - The number of lines to move the cursor. Up is negative, down positive. +// Return value: +// - S_OK if handled successfully. Otherwise an appropriate HRESULT for failing to clamp. +[[nodiscard]] +HRESULT DoSrvMoveCursorVertically(SCREEN_INFORMATION& screenInfo, const short lines) +{ + auto& cursor = screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor(); + const int currentCursorY = cursor.GetPosition().Y; + SMALL_RECT margins = screenInfo.GetAbsoluteScrollMargins().ToInclusive(); + const auto marginsSet = margins.Bottom > margins.Top; + const auto cursorInMargins = currentCursorY <= margins.Bottom && currentCursorY >= margins.Top; + COORD clampedPos = { cursor.GetPosition().X, cursor.GetPosition().Y + lines }; + + // Make sure the cursor doesn't move outside the viewport. + screenInfo.GetViewport().Clamp(clampedPos); + + // Make sure the cursor stays inside the margins, but only if it started there + if (marginsSet && cursorInMargins) + { + try + { + const auto v = clampedPos.Y; + const auto lo = margins.Top; + const auto hi = margins.Bottom; + clampedPos.Y = std::clamp(v, lo, hi); + } + CATCH_RETURN(); + } + cursor.SetPosition(clampedPos); + + return S_OK; +} + +// Routine Description: +// - A private API call for swaping to the alternate screen buffer. In virtual terminals, there exists both a "main" +// screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already +// existing alternate, it is discarded. +// Parameters: +// - screenInfo - a reference to the screen buffer that should use an alternate buffer +// Return value: +// - True if handled successfully. False otherwise. +[[nodiscard]] +NTSTATUS DoSrvPrivateUseAlternateScreenBuffer(SCREEN_INFORMATION& screenInfo) +{ + return screenInfo.GetActiveBuffer().UseAlternateScreenBuffer(); +} + +// Routine Description: +// - A private API call for swaping to the main screen buffer. From the +// alternate buffer, returns to the main screen buffer. From the main +// screen buffer, does nothing. The alternate is discarded. +// Parameters: +// - screenInfo - a reference to the screen buffer that should use the main buffer +// Return value: +// - True if handled successfully. False otherwise. +void DoSrvPrivateUseMainScreenBuffer(SCREEN_INFORMATION& screenInfo) +{ + screenInfo.GetActiveBuffer().UseMainScreenBuffer(); +} + +// Routine Description: +// - A private API call for setting a VT tab stop in the cursor's current column. +// Parameters: +// +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS DoSrvPrivateHorizontalTabSet() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& _screenBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer(); + + const COORD cursorPos = _screenBuffer.GetTextBuffer().GetCursor().GetPosition(); + try + { + _screenBuffer.AddTabStop(cursorPos.X); + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + return STATUS_SUCCESS; +} + +// Routine Description: +// - A private helper for excecuting a number of tabs. +// Parameters: +// sNumTabs - The number of tabs to execute +// fForward - whether to tab forward or backwards +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS DoPrivateTabHelper(const SHORT sNumTabs, _In_ bool fForward) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& _screenBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer(); + + NTSTATUS Status = STATUS_SUCCESS; + FAIL_FAST_IF(!(sNumTabs >= 0)); + for (SHORT sTabsExecuted = 0; sTabsExecuted < sNumTabs && NT_SUCCESS(Status); sTabsExecuted++) + { + const COORD cursorPos = _screenBuffer.GetTextBuffer().GetCursor().GetPosition(); + COORD cNewPos = (fForward) ? _screenBuffer.GetForwardTab(cursorPos) : _screenBuffer.GetReverseTab(cursorPos); + // GetForwardTab is smart enough to move the cursor to the next line if + // it's at the end of the current one already. AdjustCursorPos shouldn't + // to be doing anything funny, just moving the cursor to the location GetForwardTab returns + Status = AdjustCursorPosition(_screenBuffer, cNewPos, TRUE, nullptr); + } + return Status; +} + +// Routine Description: +// - A private API call for performing a forwards tab. This will take the +// cursor to the tab stop following its current location. If there are no +// more tabs in this row, it will take it to the right side of the window. +// If it's already in the last column of the row, it will move it to the next line. +// Parameters: +// - sNumTabs - The number of tabs to perform. +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS DoSrvPrivateForwardTab(const SHORT sNumTabs) +{ + return DoPrivateTabHelper(sNumTabs, true); +} + +// Routine Description: +// - A private API call for performing a backwards tab. This will take the +// cursor to the tab stop previous to its current location. It will not reverse line feed. +// Parameters: +// - sNumTabs - The number of tabs to perform. +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS DoSrvPrivateBackwardsTab(const SHORT sNumTabs) +{ + return DoPrivateTabHelper(sNumTabs, false); +} + +// Routine Description: +// - A private API call for clearing the VT tabs that have been set. +// Parameters: +// - fClearAll - If false, only clears the tab in the current column (if it exists) +// otherwise clears all set tabs. (and reverts to lecacy 8-char tabs behavior.) +// Return value: +// - None +void DoSrvPrivateTabClear(const bool fClearAll) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer(); + if (fClearAll) + { + screenBuffer.ClearTabStops(); + } + else + { + const COORD cursorPos = screenBuffer.GetTextBuffer().GetCursor().GetPosition(); + screenBuffer.ClearTabStop(cursorPos.X); + } +} + +// Routine Description: +// - A private API call for enabling VT200 style mouse mode. +// Parameters: +// - fEnable - true to enable default tracking mode, false to disable mouse mode. +// Return value: +// - None +void DoSrvPrivateEnableVT200MouseMode(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.EnableDefaultTracking(fEnable); +} + +// Routine Description: +// - A private API call for enabling utf8 style mouse mode. +// Parameters: +// - fEnable - true to enable, false to disable. +// Return value: +// - None +void DoSrvPrivateEnableUTF8ExtendedMouseMode(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.SetUtf8ExtendedMode(fEnable); +} + +// Routine Description: +// - A private API call for enabling SGR style mouse mode. +// Parameters: +// - fEnable - true to enable, false to disable. +// Return value: +// - None +void DoSrvPrivateEnableSGRExtendedMouseMode(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.SetSGRExtendedMode(fEnable); +} + +// Routine Description: +// - A private API call for enabling button-event mouse mode. +// Parameters: +// - fEnable - true to enable button-event mode, false to disable mouse mode. +// Return value: +// - None +void DoSrvPrivateEnableButtonEventMouseMode(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.EnableButtonEventTracking(fEnable); +} + +// Routine Description: +// - A private API call for enabling any-event mouse mode. +// Parameters: +// - fEnable - true to enable any-event mode, false to disable mouse mode. +// Return value: +// - None +void DoSrvPrivateEnableAnyEventMouseMode(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.EnableAnyEventTracking(fEnable); +} + +// Routine Description: +// - A private API call for enabling alternate scroll mode +// Parameters: +// - fEnable - true to enable alternate scroll mode, false to disable. +// Return value: +// None +void DoSrvPrivateEnableAlternateScroll(const bool fEnable) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.terminalMouseInput.EnableAlternateScroll(fEnable); +} + +// Routine Description: +// - A private API call for performing a VT-style erase all operation on the buffer. +// See SCREEN_INFORMATION::VtEraseAll's description for details. +// Parameters: +// The ScreenBuffer to perform the erase on. +// Return value: +// - STATUS_SUCCESS if we succeeded, otherwise the NTSTATUS version of the failure. +[[nodiscard]] +NTSTATUS DoSrvPrivateEraseAll(SCREEN_INFORMATION& screenInfo) +{ + return NTSTATUS_FROM_HRESULT(screenInfo.GetActiveBuffer().VtEraseAll()); +} + +void DoSrvSetCursorStyle(SCREEN_INFORMATION& screenInfo, + const CursorType cursorType) +{ + screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor().SetType(cursorType); +} + +void DoSrvSetCursorColor(SCREEN_INFORMATION& screenInfo, + const COLORREF cursorColor) +{ + screenInfo.GetActiveBuffer().GetTextBuffer().GetCursor().SetColor(cursorColor); +} + +// Routine Description: +// - A private API call to get only the default color attributes of the screen buffer. +// - This is used as a performance optimization by the VT adapter in SGR (Set Graphics Rendition) instead +// of calling for this information through the public API GetConsoleScreenBufferInfoEx which returns a lot +// of extra unnecessary data and takes a lot of extra processing time. +// Parameters +// - screenInfo - The screen buffer to retrieve default color attributes information from +// - pwAttributes - Pointer to space that will receive color attributes data +// Return Value: +// - STATUS_SUCCESS if we succeeded or STATUS_INVALID_PARAMETER for bad params (nullptr). +[[nodiscard]] +NTSTATUS DoSrvPrivateGetConsoleScreenBufferAttributes(_In_ const SCREEN_INFORMATION& screenInfo, _Out_ WORD* const pwAttributes) +{ + NTSTATUS Status = STATUS_SUCCESS; + + if (pwAttributes == nullptr) + { + Status = STATUS_INVALID_PARAMETER; + } + + if (NT_SUCCESS(Status)) + { + *pwAttributes = screenInfo.GetActiveBuffer().GetAttributes().GetLegacyAttributes(); + } + + return Status; +} + +// Routine Description: +// - A private API call for forcing the renderer to repaint the screen. If the +// input screen buffer is not the active one, then just do nothing. We only +// want to redraw the screen buffer that requested the repaint, and +// switching screen buffers will already force a repaint. +// Parameters: +// The ScreenBuffer to perform the repaint for. +// Return value: +// - None +void DoSrvPrivateRefreshWindow(_In_ const SCREEN_INFORMATION& screenInfo) +{ + Globals& g = ServiceLocator::LocateGlobals(); + if (&screenInfo == &g.getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer()) + { + g.pRender->TriggerRedrawAll(); + } +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// - isOriginal - If true, gets the title when we booted up. If false, gets whatever it is set to right now. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT GetConsoleTitleWImplHelper(std::optional> title, + size_t& written, + size_t& needed, + const bool isOriginal) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Ensure output variables are initialized. + written = 0; + needed = 0; + + if (title.has_value() && title.value().size() > 0) + { + title.value().at(0) = ANSI_NULL; + } + + // Get the appropriate title and length depending on the mode. + const wchar_t* pwszTitle; + size_t cchTitleLength; + + if (isOriginal) + { + pwszTitle = gci.GetOriginalTitle().c_str(); + cchTitleLength = gci.GetOriginalTitle().length(); + } + else + { + pwszTitle = gci.GetTitle().c_str(); + cchTitleLength = gci.GetTitle().length(); + } + + // Always report how much space we would need. + needed = cchTitleLength; + + // If we have a pointer to receive the data, then copy it out. + if (title.has_value()) + { + HRESULT const hr = StringCchCopyNW(title.value().data(), title.value().size(), pwszTitle, cchTitleLength); + + // Insufficient buffer is allowed. If we return a partial string, that's still OK by historical/compat standards. + // Just say how much we managed to return. + if (SUCCEEDED(hr) || STRSAFE_E_INSUFFICIENT_BUFFER == hr) + { + written = std::min(gsl::narrow(title.value().size()), cchTitleLength); + } + } + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// - isOriginal - If true, gets the title when we booted up. If false, gets whatever it is set to right now. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception + +[[nodiscard]] +HRESULT GetConsoleTitleAImplHelper(gsl::span title, + size_t& written, + size_t& needed, + const bool isOriginal) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Ensure output variables are initialized. + written = 0; + needed = 0; + + if (title.size() > 0) + { + title.at(0) = ANSI_NULL; + } + + // Figure out how big our temporary Unicode buffer must be to get the title. + size_t unicodeNeeded; + size_t unicodeWritten; + RETURN_IF_FAILED(GetConsoleTitleWImplHelper(std::nullopt, unicodeWritten, unicodeNeeded, isOriginal)); + + // If there's nothing to get, then simply return. + RETURN_HR_IF(S_OK, 0 == unicodeNeeded); + + // Allocate a unicode buffer of the right size. + size_t const unicodeSize = unicodeNeeded + 1; // add one for null terminator space + std::unique_ptr unicodeBuffer = std::make_unique(unicodeSize); + RETURN_IF_NULL_ALLOC(unicodeBuffer); + + const gsl::span unicodeSpan(unicodeBuffer.get(), unicodeSize); + + // Retrieve the title in Unicode. + RETURN_IF_FAILED(GetConsoleTitleWImplHelper(unicodeSpan, unicodeWritten, unicodeNeeded, isOriginal)); + + // Convert result to A + const auto converted = ConvertToA(gci.CP, { unicodeBuffer.get(), unicodeWritten }); + + // The legacy A behavior is a bit strange. If the buffer given doesn't have enough space to hold + // the string without null termination (e.g. the title is 9 long, 10 with null. The buffer given isn't >= 9). + // then do not copy anything back and do not report how much space we need. + if (gsl::narrow(title.size()) >= converted.size()) + { + // Say how many characters of buffer we would need to hold the entire result. + needed = converted.size(); + + // Copy safely to output buffer + HRESULT const hr = StringCchCopyNA(title.data(), title.size(), converted.data(), converted.size()); + + + // Insufficient buffer is allowed. If we return a partial string, that's still OK by historical/compat standards. + // Just say how much we managed to return. + if (SUCCEEDED(hr) || STRSAFE_E_INSUFFICIENT_BUFFER == hr) + { + // And return the size copied (either the size of the buffer or the null terminated length of the string we filled it with.) + written = std::min(gsl::narrow(title.size()), converted.size() + 1); + + // Another compatibility fix... If we had exactly the number of bytes needed for an unterminated string, + // then replace the terminator left behind by StringCchCopyNA with the final character of the title string. + if (gsl::narrow(title.size()) == converted.size()) + { + title.at(title.size() - 1) = converted.data()[converted.size() - 1]; + } + } + } + else + { + // If we didn't copy anything back and there is space, null terminate the given buffer and return. + if (title.size() > 0) + { + title.at(0) = ANSI_NULL; + written = 1; + } + } + + return S_OK; + } + CATCH_RETURN(); + +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleTitleAImplHelper(title, written, needed, false); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleTitleWImplHelper(title, written, needed, false); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleOriginalTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleTitleAImplHelper(title, written, needed, true); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Gets title information from the console. It can be truncated if the buffer is too small. +// Arguments: +// - title - If given, this buffer is filled with the title information requested. +// - Use nullopt to request buffer size required. +// - written - The number of characters filled in the title buffer. +// - needed - The number of characters we would need to completely write out the title. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleOriginalTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return GetConsoleTitleWImplHelper(title, written, needed, true); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets title information from the console. +// Arguments: +// - title - The new title to store and display on the console window. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleTitleAImpl(const std::string_view title) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + const auto titleW = ConvertToW(gci.CP, title); + + return SetConsoleTitleWImpl(titleW); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets title information from the console. +// Arguments: +// - title - The new title to store and display on the console window. +// Return Value: +// - S_OK, E_INVALIDARG, or failure code from thrown exception +[[nodiscard]] +HRESULT ApiRoutines::SetConsoleTitleWImpl(const std::wstring_view title) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + return DoSrvSetConsoleTitleW(title); +} + +[[nodiscard]] +HRESULT DoSrvSetConsoleTitleW(const std::wstring_view title) noexcept +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // Sanitize the input if we're in pty mode. No control chars - this string + // will get emitted back to the TTY in a VT sequence, and we don't want + // to embed control characters in that string. + if (gci.IsInVtIoMode()) + { + std::wstring sanitized; + sanitized.reserve(title.size()); + for (size_t i = 0; i < title.size(); i++) + { + if (title.at(i) >= UNICODE_SPACE) + { + sanitized.push_back(title.at(i)); + } + } + + gci.SetTitle({ sanitized }); + } + else + { + // SetTitle will trigger the renderer to update the titlebar for us. + gci.SetTitle(title); + } + + return S_OK; +} + +// Routine Description: +// - A private API call for forcing the VT Renderer to NOT paint the next resize +// event. This is used by InteractDispatch, to prevent resizes from echoing +// between terminal and host. +// Parameters: +// +// Return value: +// - STATUS_SUCCESS if we succeeded, otherwise the NTSTATUS version of the failure. +[[nodiscard]] +NTSTATUS DoSrvPrivateSuppressResizeRepaint() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + FAIL_FAST_IF(!(gci.IsInVtIoMode())); + return NTSTATUS_FROM_HRESULT(gci.GetVtIo()->SuppressResizeRepaint()); +} + +// Routine Description: +// - An API call for checking if the console host is acting as a pty. +// Parameters: +// - isPty: recieves the bool indicating whether or not we're in pty mode. +// Return value: +// +void DoSrvIsConsolePty(_Out_ bool* const pIsPty) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + *pIsPty = gci.IsInVtIoMode(); +} + +// Routine Description: +// - a private API call for setting the default tab stops in the active screen buffer. +void DoSrvPrivateSetDefaultTabStops() +{ + ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer().SetDefaultVtTabStops(); +} + +// Routine Description: +// - internal logic for adding or removing lines in the active screen buffer +// Parameters: +// - count - the number of lines to modify +// - insert - true if inserting lines, false if deleting lines +void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert) +{ + auto& screenInfo = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetActiveBuffer(); + auto& textBuffer = screenInfo.GetTextBuffer(); + const auto cursorPosition = textBuffer.GetCursor().GetPosition(); + const auto margins = screenInfo.GetAbsoluteScrollMargins(); + if (margins.IsInBounds(cursorPosition)) + { + const auto screenEdges = screenInfo.GetBufferSize().ToInclusive(); + // Rectangle to cut out of the existing buffer + SMALL_RECT srScroll; + srScroll.Left = 0; + srScroll.Right = screenEdges.Right - screenEdges.Left; + srScroll.Top = cursorPosition.Y; + srScroll.Bottom = screenEdges.Bottom; + // Paste coordinate for cut text above + COORD coordDestination; + coordDestination.X = 0; + if (insert) + { + coordDestination.Y = (cursorPosition.Y) + gsl::narrow(count); + } + else + { + coordDestination.Y = (cursorPosition.Y) - gsl::narrow(count); + } + + SMALL_RECT srClip = screenEdges; + srClip.Top = cursorPosition.Y; + + LOG_IF_FAILED(ServiceLocator::LocateGlobals().api.ScrollConsoleScreenBufferWImpl(screenInfo, + srScroll, + coordDestination, + srClip, + UNICODE_SPACE, + screenInfo.GetAttributes().GetLegacyAttributes())); + } +} + +// Routine Description: +// - a private API call for deleting lines in the active screen buffer. +// Parameters: +// - count - the number of lines to delete +void DoSrvPrivateDeleteLines(const unsigned int count) +{ + DoSrvPrivateModifyLinesImpl(count, false); +} + +// Routine Description: +// - a private API call for inserting lines in the active screen buffer. +// Parameters: +// - count - the number of lines to insert +void DoSrvPrivateInsertLines(const unsigned int count) +{ + DoSrvPrivateModifyLinesImpl(count, true); +} + +// Method Description: +// - Snaps the screen buffer's viewport to the "virtual bottom", the last place +//the viewport was before the user scrolled it (with the mouse or scrollbar) +// Arguments: +// - screenInfo: the buffer to move the viewport for. +// Return Value: +// - +void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo) +{ + screenInfo.GetActiveBuffer().MoveToBottom(); +} + +// Method Description: +// - Sets the color table value in index to the color specified in value. +// Can be used to set the 256-color table as well as the 16-color table. +// Arguments: +// - index: the index in the table to change. +// - value: the new RGB value to use for that index in the color table. +// Return Value: +// - E_INVALIDARG if index is >= 256, else S_OK +// Notes: +// Does not take a buffer paramenter. The color table for a console and for +// terminals as well is global, not per-screen-buffer. +[[nodiscard]] +HRESULT DoSrvPrivateSetColorTableEntry(const short index, const COLORREF value) noexcept +{ + RETURN_HR_IF(E_INVALIDARG, index >= 256); + try + { + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + + gci.SetColorTableEntry(index, value); + + // Update the screen colors if we're not a pty + // No need to force a redraw in pty mode. + if (g.pRender && !gci.IsInVtIoMode()) + { + g.pRender->TriggerRedrawAll(); + } + + return S_OK; + } + CATCH_RETURN(); + +} diff --git a/src/host/getset.h b/src/host/getset.h new file mode 100644 index 000000000..1676c8b63 --- /dev/null +++ b/src/host/getset.h @@ -0,0 +1,101 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- getset.h + +Abstract: +- This file implements the NT console server console state API. + +Author: +- Therese Stowell (ThereseS) 5-Dec-1990 + +Revision History: +--*/ + +#pragma once +#include "../inc/conattrs.hpp" +class SCREEN_INFORMATION; + + +void DoSrvPrivateSetLegacyAttributes(SCREEN_INFORMATION& screenInfo, + const WORD Attribute, + const bool fForeground, + const bool fBackground, + const bool fMeta); + +void DoSrvPrivateSetDefaultAttributes(SCREEN_INFORMATION& screenInfo, const bool fForeground, const bool fBackground); + +[[nodiscard]] +NTSTATUS DoSrvPrivateSetCursorKeysMode(_In_ bool fApplicationMode); +[[nodiscard]] +NTSTATUS DoSrvPrivateSetKeypadMode(_In_ bool fApplicationMode); + +void DoSrvPrivateShowCursor(SCREEN_INFORMATION& screenInfo, const bool show) noexcept; +void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool fEnable); + +[[nodiscard]] +NTSTATUS DoSrvPrivateSetScrollingRegion(SCREEN_INFORMATION& screenInfo, const SMALL_RECT* const psrScrollMargins); +[[nodiscard]] +NTSTATUS DoSrvPrivateReverseLineFeed(SCREEN_INFORMATION& screenInfo); +[[nodiscard]] +HRESULT DoSrvMoveCursorVertically(SCREEN_INFORMATION& screenInfo, const short lines); + +[[nodiscard]] +NTSTATUS DoSrvPrivateUseAlternateScreenBuffer(SCREEN_INFORMATION& screenInfo); +void DoSrvPrivateUseMainScreenBuffer(SCREEN_INFORMATION& screenInfo); + +[[nodiscard]] +NTSTATUS DoSrvPrivateHorizontalTabSet(); +[[nodiscard]] +NTSTATUS DoSrvPrivateForwardTab(const SHORT sNumTabs); +[[nodiscard]] +NTSTATUS DoSrvPrivateBackwardsTab(const SHORT sNumTabs); +void DoSrvPrivateTabClear(const bool fClearAll); + +void DoSrvPrivateEnableVT200MouseMode(const bool fEnable); +void DoSrvPrivateEnableUTF8ExtendedMouseMode(const bool fEnable); +void DoSrvPrivateEnableSGRExtendedMouseMode(const bool fEnable); +void DoSrvPrivateEnableButtonEventMouseMode(const bool fEnable); +void DoSrvPrivateEnableAnyEventMouseMode(const bool fEnable); +void DoSrvPrivateEnableAlternateScroll(const bool fEnable); + +void DoSrvPrivateSetConsoleXtermTextAttribute(SCREEN_INFORMATION& screenInfo, + const int iXtermTableEntry, + const bool fIsForeground); +void DoSrvPrivateSetConsoleRGBTextAttribute(SCREEN_INFORMATION& screenInfo, + const COLORREF rgbColor, + const bool fIsForeground); + +void DoSrvPrivateBoldText(SCREEN_INFORMATION& screenInfo, const bool bolded); + +[[nodiscard]] +NTSTATUS DoSrvPrivateEraseAll(SCREEN_INFORMATION& screenInfo); + +void DoSrvSetCursorStyle(SCREEN_INFORMATION& screenInfo, + const CursorType cursorType); +void DoSrvSetCursorColor(SCREEN_INFORMATION& screenInfo, + const COLORREF cursorColor); + +[[nodiscard]] +NTSTATUS DoSrvPrivateGetConsoleScreenBufferAttributes(const SCREEN_INFORMATION& screenInfo, + _Out_ WORD* const pwAttributes); + +void DoSrvPrivateRefreshWindow(const SCREEN_INFORMATION& screenInfo); + +void DoSrvGetConsoleOutputCodePage(_Out_ unsigned int* const pCodePage); + +[[nodiscard]] +NTSTATUS DoSrvPrivateSuppressResizeRepaint(); + +void DoSrvIsConsolePty(_Out_ bool* const pIsPty); + +void DoSrvPrivateSetDefaultTabStops(); +void DoSrvPrivateDeleteLines(const unsigned int count); +void DoSrvPrivateInsertLines(const unsigned int count); + +void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo); + +[[nodiscard]] +HRESULT DoSrvPrivateSetColorTableEntry(const short index, const COLORREF value) noexcept; diff --git a/src/host/globals.cpp b/src/host/globals.cpp new file mode 100644 index 000000000..b9f82175c --- /dev/null +++ b/src/host/globals.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "globals.h" + +#pragma hdrstop + +CONSOLE_INFORMATION& Globals::getConsoleInformation() +{ + return ciConsoleInformation; +} + +bool Globals::IsHeadless() const +{ + return launchArgs.IsHeadless(); +} diff --git a/src/host/globals.h b/src/host/globals.h new file mode 100644 index 000000000..b8d912272 --- /dev/null +++ b/src/host/globals.h @@ -0,0 +1,76 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- globals.h + +Abstract: +- This module contains the global variables used by the console server DLL. + +Author: +- Jerry Shea (jerrysh) 21-Sep-1993 + +Revision History: +- Modified to reduce globals over Console V2 project (MiNiksa/PaulCam, 2014) +--*/ + +#pragma once + +#include "selection.hpp" +#include "server.h" +#include "ConsoleArguments.hpp" +#include "ApiRoutines.h" + +#include "..\renderer\inc\IRenderData.hpp" +#include "..\renderer\inc\IRenderEngine.hpp" +#include "..\renderer\inc\IRenderer.hpp" +#include "..\renderer\inc\IFontDefaultList.hpp" + +#include "..\server\DeviceComm.h" + +#include +#include +TRACELOGGING_DECLARE_PROVIDER(g_hConhostV2EventTraceProvider); + +using namespace Microsoft::Console::Render; + +class Globals +{ +public: + UINT uiOEMCP = GetOEMCP(); + UINT uiWindowsCP = GetACP(); + HINSTANCE hInstance; + UINT uiDialogBoxCount; + + ConsoleArguments launchArgs; + + CONSOLE_INFORMATION& getConsoleInformation(); + + DeviceComm* pDeviceComm; + + wil::unique_event_nothrow hInputEvent; + + SHORT sVerticalScrollSize; + SHORT sHorizontalScrollSize; + + int dpi = USER_DEFAULT_SCREEN_DPI; + ULONG cursorPixelWidth = 1; + + NTSTATUS ntstatusConsoleInputInitStatus; + wil::unique_event_nothrow hConsoleInputInitEvent; + DWORD dwInputThreadId; + + std::vector WordDelimiters; + + IRenderer* pRender; + + IFontDefaultList* pFontDefaultList; + + bool IsHeadless() const; + + ApiRoutines api; + +private: + CONSOLE_INFORMATION ciConsoleInformation; +}; diff --git a/src/host/handle.cpp b/src/host/handle.cpp new file mode 100644 index 000000000..47e65df1a --- /dev/null +++ b/src/host/handle.cpp @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "handle.h" +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +void LockConsole() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); +} + +void UnlockConsole() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.GetCSRecursionCount() == 1) + { + ProcessCtrlEvents(); + } + else + { + gci.UnlockConsole(); + } +} diff --git a/src/host/handle.h b/src/host/handle.h new file mode 100644 index 000000000..73d2b2677 --- /dev/null +++ b/src/host/handle.h @@ -0,0 +1,21 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- handle.h + +Abstract: +- This file manages console and I/O handles. +- Mainly related to process management/interprocess communication. + +Author: +- Therese Stowell (ThereseS) 16-Nov-1990 + +Revision History: +--*/ + +#pragma once + +void LockConsole(); +void UnlockConsole(); diff --git a/src/host/history.cpp b/src/host/history.cpp new file mode 100644 index 000000000..f8de3462a --- /dev/null +++ b/src/host/history.cpp @@ -0,0 +1,922 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "history.h" + +#include "_output.h" +#include "output.h" +#include "stream.h" +#include "_stream.h" +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "../types/inc/convert.hpp" +#include "srvinit.h" +#include "resource.h" + +#include "ApiRoutines.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// I need to be a list because we rearrange elements inside to maintain a +// "least recently used" state. Doing many rearrangement operations with +// a list will maintain the iterator pointers as valid to the elements +// (where other collections like deque do not.) +// If CommandHistory::s_Allocate and friends stop shuffling elements +// for maintaining LRU, then this datatype can be changed. +std::list CommandHistory::s_historyLists; + +CommandHistory* CommandHistory::s_Find(const HANDLE processHandle) +{ + for (auto& historyList : s_historyLists) + { + if (historyList._processHandle == processHandle) + { + FAIL_FAST_IF(WI_IsFlagClear(historyList.Flags, CLE_ALLOCATED)); + return &historyList; + } + } + + return nullptr; +} + +// Routine Description: +// - This routine marks the command history buffer freed. +// Arguments: +// - processHandle - handle to client process. +void CommandHistory::s_Free(const HANDLE processHandle) +{ + CommandHistory* const History = CommandHistory::s_Find(processHandle); + if (History) + { + WI_ClearFlag(History->Flags, CLE_ALLOCATED); + History->_processHandle = nullptr; + } +} + +void CommandHistory::s_ResizeAll(const size_t commands) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + FAIL_FAST_IF(commands > SHORT_MAX); + gci.SetHistoryBufferSize(gsl::narrow(commands)); + + for (auto& historyList : s_historyLists) + { + historyList.Realloc(commands); + } +} + +static bool CaseInsensitiveEquality(wchar_t a, wchar_t b) +{ + return ::towlower(a) == ::towlower(b); +} + +bool CommandHistory::IsAppNameMatch(const std::wstring_view other) const +{ + return std::equal(_appName.cbegin(), _appName.cend(), other.cbegin(), other.cend(), CaseInsensitiveEquality); +} + +// Routine Description: +// - This routine is called when escape is entered or a command is added. +void CommandHistory::_Reset() +{ + LastDisplayed = gsl::narrow(_commands.size()) - 1; + WI_SetFlag(Flags, CLE_RESET); +} + +[[nodiscard]] +HRESULT CommandHistory::Add(const std::wstring_view newCommand, + const bool suppressDuplicates) +{ + RETURN_HR_IF(E_OUTOFMEMORY, _maxCommands == 0); + FAIL_FAST_IF(WI_IsFlagClear(Flags, CLE_ALLOCATED)); + + if (newCommand.size() == 0) + { + return S_OK; + } + + try + { + if (_commands.size() == 0 || + _commands.back().size() != newCommand.size() || + !std::equal(_commands.back().cbegin(), _commands.back().cbegin() + newCommand.size(), + newCommand.cbegin(), newCommand.cend())) + { + std::wstring reuse{}; + + if (suppressDuplicates) + { + SHORT index; + if (FindMatchingCommand(newCommand, LastDisplayed, index, CommandHistory::MatchOptions::ExactMatch)) + { + reuse = Remove(index); + } + } + + // find free record. if all records are used, free the lru one. + if ((SHORT)_commands.size() == _maxCommands) + { + _commands.erase(_commands.cbegin()); + // move LastDisplayed back one in order to stay synced with the + // command it referred to before erasing the lru one + --LastDisplayed; + } + + // add newCommand to array + if (!reuse.empty()) + { + _commands.emplace_back(reuse); + } + else + { + _commands.emplace_back(newCommand); + } + + if (LastDisplayed == -1 || + _commands.at(LastDisplayed).size() != newCommand.size() || + !std::equal(_commands.at(LastDisplayed).cbegin(), _commands.at(LastDisplayed).cbegin() + newCommand.size(), + newCommand.cbegin(), newCommand.cend())) + { + _Reset(); + } + } + } + CATCH_RETURN(); + WI_SetFlag(Flags, CLE_RESET); // remember that we've returned a cmd + + return S_OK; +} + +std::wstring_view CommandHistory::GetNth(const SHORT index) const +{ + try + { + return _commands.at(index); + } + CATCH_LOG(); + + return {}; +} + +[[nodiscard]] +HRESULT CommandHistory::RetrieveNth(const SHORT index, + gsl::span buffer, + size_t& commandSize) +{ + LastDisplayed = index; + + try + { + const auto& cmd = _commands.at(index); + if (cmd.size() > (size_t)buffer.size()) + { + commandSize = buffer.size(); // room for CRLF? + } + else + { + commandSize = cmd.size(); + } + + std::copy_n(cmd.cbegin(), commandSize, buffer.begin()); + + commandSize *= sizeof(wchar_t); + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT CommandHistory::Retrieve(const SearchDirection searchDirection, + const gsl::span buffer, + size_t& commandSize) +{ + FAIL_FAST_IF(!(WI_IsFlagSet(Flags, CLE_ALLOCATED))); + + if (_commands.size() == 0) + { + return E_FAIL; + } + + if (_commands.size() == 1) + { + LastDisplayed = 0; + } + else if (searchDirection == SearchDirection::Previous) + { + // if this is the first time for this read that a command has + // been retrieved, return the current command. otherwise, return + // the previous command. + if (WI_IsFlagSet(Flags, CLE_RESET)) + { + WI_ClearFlag(Flags, CLE_RESET); + } + else + { + _Prev(LastDisplayed); + } + } + else + { + _Next(LastDisplayed); + } + + return RetrieveNth(LastDisplayed, buffer, commandSize); +} + +std::wstring_view CommandHistory::GetLastCommand() const +{ + if (_commands.size() != 0) + { + try + { + return _commands.at(LastDisplayed); + } + CATCH_LOG(); + } + + return {}; +} + +void CommandHistory::Empty() +{ + _commands.clear(); + LastDisplayed = -1; + Flags = CLE_RESET; +} + +bool CommandHistory::AtFirstCommand() const +{ + if (WI_IsFlagSet(Flags, CLE_RESET)) + { + return FALSE; + } + + SHORT i = (SHORT)(LastDisplayed - 1); + if (i == -1) + { + i = ((SHORT)_commands.size()) - 1i16; + } + + return (i == ((SHORT)_commands.size()) - 1i16); +} + +bool CommandHistory::AtLastCommand() const +{ + return LastDisplayed == ((SHORT)_commands.size()) - 1i16; +} + +void CommandHistory::Realloc(const size_t commands) +{ + // To protect ourselves from overflow and general arithmetic errors, a limit of SHORT_MAX is put on the size of the command history. + if (_maxCommands == (SHORT)commands || commands > SHORT_MAX) + { + return; + } + + const auto oldCommands = _commands; + const auto newNumberOfCommands = gsl::narrow(std::min(_commands.size(), commands)); + + _commands.clear(); + for (SHORT i = 0; i < newNumberOfCommands; i++) + { + _commands.emplace_back(oldCommands[i]); + } + + WI_SetFlag(Flags, CLE_RESET); + LastDisplayed = gsl::narrow(_commands.size()) - 1; + _maxCommands = (SHORT)commands; +} + +void CommandHistory::s_ReallocExeToFront(const std::wstring_view appName, const size_t commands) +{ + for (auto it = s_historyLists.begin(); it != s_historyLists.end(); it++) + { + if (WI_IsFlagSet(it->Flags, CLE_ALLOCATED) && it->IsAppNameMatch(appName)) + { + CommandHistory backup = *it; + backup.Realloc(commands); + + s_historyLists.erase(it); + s_historyLists.push_front(backup); + + return; + } + } +} + +CommandHistory* CommandHistory::s_FindByExe(const std::wstring_view appName) +{ + for (auto& historyList : s_historyLists) + { + if (WI_IsFlagSet(historyList.Flags, CLE_ALLOCATED) && historyList.IsAppNameMatch(appName)) + { + return &historyList; + } + } + return nullptr; +} + +size_t CommandHistory::s_CountOfHistories() +{ + return s_historyLists.size(); +} + +// Routine Description: +// - This routine returns the LRU command history buffer, or the command history buffer that corresponds to the app name. +// Arguments: +// - Console - pointer to console. +// Return Value: +// - Pointer to command history buffer. if none are available, returns nullptr. +CommandHistory* CommandHistory::s_Allocate(const std::wstring_view appName, const HANDLE processHandle) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Reuse a history buffer. The buffer must be !CLE_ALLOCATED. + // If possible, the buffer should have the same app name. + std::optional BestCandidate; + bool SameApp = false; + + for (auto it = s_historyLists.cbegin(); it != s_historyLists.cend(); it++) + { + if (WI_IsFlagClear(it->Flags, CLE_ALLOCATED)) + { + // use LRU history buffer with same app name + if (it->IsAppNameMatch(appName)) + { + BestCandidate = *it; + SameApp = true; + s_historyLists.erase(it); + break; + } + } + } + + // if there isn't a free buffer for the app name and the maximum number of + // command history buffers hasn't been allocated, allocate a new one. + if (!SameApp && s_historyLists.size() < gci.GetNumberOfHistoryBuffers()) + { + CommandHistory History; + + History._appName = appName; + History.Flags = CLE_ALLOCATED; + History.LastDisplayed = -1; + History._maxCommands = gsl::narrow(gci.GetHistoryBufferSize()); + History._processHandle = processHandle; + return &s_historyLists.emplace_front(History); + } + else if (!BestCandidate.has_value() && s_historyLists.size() > 0) + { + // If we have no candidate already and we need one, take the LRU (which is the back/last one) which isn't allocated. + for (auto it = s_historyLists.crbegin(); it != s_historyLists.crend(); it++) + { + if (WI_IsFlagClear(it->Flags, CLE_ALLOCATED)) + { + BestCandidate = *it; + s_historyLists.erase(std::next(it).base()); // trickery to turn reverse iterator into forward iterator for erase. + break; + } + } + + } + + // If the app name doesn't match, copy in the new app name and free the old commands. + if (BestCandidate.has_value()) + { + if (!SameApp) + { + BestCandidate->_commands.clear(); + BestCandidate->LastDisplayed = -1; + BestCandidate->_appName = appName; + } + + BestCandidate->_processHandle = processHandle; + WI_SetFlag(BestCandidate->Flags, CLE_ALLOCATED); + + return &s_historyLists.emplace_front(BestCandidate.value()); + } + + return nullptr; +} + +size_t CommandHistory::GetNumberOfCommands() const +{ + return _commands.size(); +} + +void CommandHistory::_Prev(SHORT& ind) const +{ + if (ind <= 0) + { + ind = gsl::narrow(_commands.size()); + } + ind--; +} + +void CommandHistory::_Next(SHORT& ind) const +{ + ++ind; + if (ind >= (SHORT)_commands.size()) + { + ind = 0; + } +} + +void CommandHistory::_Dec(SHORT& ind) const +{ + if (ind <= 0) + { + ind = _maxCommands; + } + ind--; +} + +void CommandHistory::_Inc(SHORT& ind) const +{ + ++ind; + if (ind >= _maxCommands) + { + ind = 0; + } +} + +std::wstring CommandHistory::Remove(const SHORT iDel) +{ + SHORT iFirst = 0; + SHORT iLast = gsl::narrow(_commands.size() - 1); + SHORT iDisp = LastDisplayed; + + if (_commands.size() == 0) + { + return {}; + } + + SHORT const nDel = iDel; + if ((nDel < iFirst) || (nDel > iLast)) + { + return {}; + } + + if (iDisp == iDel) + { + LastDisplayed = -1; + } + + try + { + const auto str = _commands.at(iDel); + + if (iDel < iLast) + { + _commands.erase(_commands.cbegin() + iDel); + if ((iDisp > iDel) && (iDisp <= iLast)) + { + _Dec(iDisp); + } + _Dec(iLast); + } + else if (iFirst <= iDel) + { + _commands.erase(_commands.cbegin() + iDel); + if ((iDisp >= iFirst) && (iDisp < iDel)) + { + _Inc(iDisp); + } + _Inc(iFirst); + } + + LastDisplayed = iDisp; + return str; + } + CATCH_LOG(); + + return {}; +} + + +// Routine Description: +// - this routine finds the most recent command that starts with the letters already in the current command. it returns the array index (no mod needed). +[[nodiscard]] +bool CommandHistory::FindMatchingCommand(const std::wstring_view givenCommand, + const SHORT startingIndex, + SHORT& indexFound, + const MatchOptions options) +{ + indexFound = startingIndex; + + if (_commands.size() == 0) + { + return false; + } + + if (WI_IsFlagClear(options, MatchOptions::JustLooking) && WI_IsFlagSet(Flags, CLE_RESET)) + { + WI_ClearFlag(Flags, CLE_RESET); + } + else + { + _Prev(indexFound); + } + + if (givenCommand.empty()) + { + return true; + } + + try + { + for (size_t i = 0; i < _commands.size(); i++) + { + const auto& storedCommand = _commands.at(indexFound); + if ((WI_IsFlagClear(options, MatchOptions::ExactMatch) && (givenCommand.size() <= storedCommand.size())) || (givenCommand.size() == storedCommand.size())) + { + if (std::equal(storedCommand.begin(), storedCommand.begin() + givenCommand.size(), + givenCommand.begin(), givenCommand.end(), + CaseInsensitiveEquality)) + { + return true; + } + } + + _Prev(indexFound); + } + } + CATCH_LOG(); + + return false; +} + +#ifdef UNIT_TESTING +void CommandHistory::s_ClearHistoryListStorage() +{ + s_historyLists.clear(); +} +#endif + +// Routine Description: +// - swaps the locations of two history items +// Arguments: +// - indexA - index of one history item to swap +// - indexB - index of one history item to swap +void CommandHistory::Swap(const short indexA, const short indexB) +{ + std::swap(_commands.at(indexA), _commands.at(indexB)); +} + +// Routine Description: +// - Clears all command history for the given EXE name +// - Will convert input parameters and call the W version of this method +// Arguments: +// - exeName - The client EXE application attached to the host whose history we should clear +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::ExpungeConsoleCommandHistoryAImpl(const std::string_view exeName) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + const auto exeNameW = ConvertToW(gci.CP, exeName); + + return ExpungeConsoleCommandHistoryWImpl(exeNameW); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Clears all command history for the given EXE name +// Arguments: +// - exeName - The client EXE application attached to the host whose history we should clear +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::ExpungeConsoleCommandHistoryWImpl(const std::wstring_view exeName) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto history = CommandHistory::s_FindByExe(exeName); + if (history) + { + history->Empty(); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the number of commands that will be stored in history for a given EXE name +// - Will convert input parameters and call the W version of this method +// Arguments: +// - exeName - A client EXE application attached to the host +// - numberOfCommands - Specifies the maximum length of the associated history buffer +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::SetConsoleNumberOfCommandsAImpl(const std::string_view exeName, + const size_t numberOfCommands) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + const auto exeNameW = ConvertToW(gci.CP, exeName); + + return SetConsoleNumberOfCommandsWImpl(exeNameW, numberOfCommands); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Sets the number of commands that will be stored in history for a given EXE name +// Arguments: +// - exeName - A client EXE application attached to the host +// - numberOfCommands - Specifies the maximum length of the associated history buffer +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::SetConsoleNumberOfCommandsWImpl(const std::wstring_view exeName, + const size_t numberOfCommands) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + CommandHistory::s_ReallocExeToFront(exeName, numberOfCommands); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves the amount of space needed to retrieve all command history for a given EXE name +// - Works for both Unicode and Multibyte text. +// - This method configuration is called for both A/W routines to allow us an efficient way of asking the system +// the lengths of how long each conversion would be without actually performing the full allocations/conversions. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - countInUnicode - True for W version (UCS-2 Unicode) calls. False for A version calls (all multibyte formats.) +// - codepage - Set to valid Windows Codepage for A version calls. Ignored for W (but typically just set to 0.) +// - historyLength - Receives the length of buffer that would be required to retrieve all history for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT GetConsoleCommandHistoryLengthImplHelper(const std::wstring_view exeName, + const bool countInUnicode, + const UINT codepage, + size_t& historyLength) +{ + // Ensure output variables are initialized + historyLength = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + CommandHistory* const pCommandHistory = CommandHistory::s_FindByExe(exeName); + if (nullptr != pCommandHistory) + { + size_t cchNeeded = 0; + + // Every command history item is made of a string length followed by 1 null character. + size_t const cchNull = 1; + + for (SHORT i = 0; i < gsl::narrow(pCommandHistory->GetNumberOfCommands()); i++) + { + const auto command = pCommandHistory->GetNth(i); + size_t cchCommand = command.size(); + + // This is the proposed length of the whole string. + size_t cchProposed; + RETURN_IF_FAILED(SizeTAdd(cchCommand, cchNull, &cchProposed)); + + // If we're counting how much multibyte space will be needed, trial convert the command string before we add. + if (!countInUnicode) + { + cchCommand = GetALengthFromW(codepage, command); + } + + // Accumulate the result + RETURN_IF_FAILED(SizeTAdd(cchNeeded, cchProposed, &cchNeeded)); + } + + historyLength = cchNeeded; + } + + return S_OK; +} + +// Routine Description: +// - Retrieves the amount of space needed to retrieve all command history for a given EXE name +// - Converts input text from A to W then makes the call to the W implementation. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - length - Receives the length of buffer that would be required to retrieve all history for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::GetConsoleCommandHistoryLengthAImpl(const std::string_view exeName, + size_t& length) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + length = 0; + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + const auto exeNameW = ConvertToW(codepage, exeName); + return GetConsoleCommandHistoryLengthImplHelper(exeNameW, false, codepage, length); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves the amount of space needed to retrieve all command history for a given EXE name +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - length - Receives the length of buffer that would be required to retrieve all history for the given exe. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::GetConsoleCommandHistoryLengthWImpl(const std::wstring_view exeName, + size_t& length) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + return GetConsoleCommandHistoryLengthImplHelper(exeName, true, 0, length); + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves a the full command history for a given EXE name known to the console. +// - It is permitted to call this function without having a target buffer. Use the result to allocate +// the appropriate amount of space and call again. +// - This behavior exists to allow the A version of the function to help allocate the right temp buffer for conversion of +// the output/result data. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - historyBuffer - The target buffer for data we are attempting to retrieve. Optionally empty to retrieve needed space. +// - writtenOrNeeded - Will specify how many characters were written (if buffer is valid) +// or how many characters would have been consumed. +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT GetConsoleCommandHistoryWImplHelper(const std::wstring_view exeName, + gsl::span historyBuffer, + size_t& writtenOrNeeded) +{ + // Ensure output variables are initialized + writtenOrNeeded = 0; + if (historyBuffer.size() > 0) + { + historyBuffer.at(0) = UNICODE_NULL; + } + + CommandHistory* const CommandHistory = CommandHistory::s_FindByExe(exeName); + + if (nullptr != CommandHistory) + { + PWCHAR CommandBufferW = historyBuffer.data(); + + size_t cchTotalLength = 0; + + size_t const cchNull = 1; + + for (SHORT i = 0; i < gsl::narrow(CommandHistory->GetNumberOfCommands()); i++) + { + const auto command = CommandHistory->GetNth(i); + + size_t const cchCommand = command.size(); + + size_t cchNeeded; + RETURN_IF_FAILED(SizeTAdd(cchCommand, cchNull, &cchNeeded)); + + // If we can return the data, attempt to do so until we're done or it overflows. + // If we cannot return data, we're just going to loop anyway and count how much space we'd need. + if (historyBuffer.size() > 0) + { + // Calculate what the new total would be after we add what we need. + size_t cchNewTotal; + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchNewTotal)); + + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), cchNewTotal > gsl::narrow(historyBuffer.size())); + + size_t cchRemaining; + RETURN_IF_FAILED(SizeTSub(historyBuffer.size(), + cchTotalLength, + &cchRemaining)); + + RETURN_IF_FAILED(StringCchCopyNW(CommandBufferW, + cchRemaining, + command.data(), + command.size())); + + CommandBufferW += cchNeeded; + } + + RETURN_IF_FAILED(SizeTAdd(cchTotalLength, cchNeeded, &cchTotalLength)); + } + + writtenOrNeeded = cchTotalLength; + } + + return S_OK; +} + +// Routine Description: +// - Retrieves a the full command history for a given EXE name known to the console. +// - Converts inputs from A to W, calls the W version of this method, and then converts the resulting text W to A. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - commandHistory - The target buffer for data we are attempting to retrieve. +// - written - Will specify how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::GetConsoleCommandHistoryAImpl(const std::string_view exeName, + gsl::span commandHistory, + size_t& written) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + UINT const codepage = gci.CP; + + // Ensure output variables are initialized + written = 0; + + try + { + if (commandHistory.size() > 0) + { + commandHistory.at(0) = ANSI_NULL; + } + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Convert our input parameters to Unicode. + const auto exeNameW = ConvertToW(codepage, exeName); + + // Figure out how big our temporary Unicode buffer must be to retrieve output + size_t bufferNeeded; + RETURN_IF_FAILED(GetConsoleCommandHistoryWImplHelper(exeNameW, {}, bufferNeeded)); + + // If there's nothing to get, then simply return. + RETURN_HR_IF(S_OK, 0 == bufferNeeded); + + // Allocate a unicode buffer of the right size. + std::unique_ptr buffer = std::make_unique(bufferNeeded); + RETURN_IF_NULL_ALLOC(buffer); + + // Call the Unicode version of this method + size_t bufferWritten; + RETURN_IF_FAILED(GetConsoleCommandHistoryWImplHelper(exeNameW, gsl::span(buffer.get(), bufferNeeded), bufferWritten)); + + // Convert result to A + const auto converted = ConvertToA(codepage, { buffer.get(), bufferWritten }); + + // Copy safely to output buffer + // - CommandHistory are a series of null terminated strings. We cannot use a SafeString function to copy. + // So instead, validate and use raw memory copy. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW), converted.size() > gsl::narrow(commandHistory.size())); + memcpy_s(commandHistory.data(), commandHistory.size(), converted.data(), converted.size()); + + // And return the size copied. + written = converted.size(); + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Retrieves a the full command history for a given EXE name known to the console. +// - Converts inputs from A to W, calls the W version of this method, and then converts the resulting text W to A. +// Arguments: +// - exeName - The client EXE application attached to the host whose set we should check +// - commandHistory - The target buffer for data we are attempting to retrieve. +// - written - Will specify how many characters were written +// Return Value: +// - Check HRESULT with SUCCEEDED. Can return memory, safe math, safe string, or locale conversion errors. +HRESULT ApiRoutines::GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, + gsl::span commandHistory, + size_t& written) noexcept +{ + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + try + { + return GetConsoleCommandHistoryWImplHelper(exeName, commandHistory, written); + } + CATCH_RETURN(); +} diff --git a/src/host/history.h b/src/host/history.h new file mode 100644 index 000000000..64afda8f2 --- /dev/null +++ b/src/host/history.h @@ -0,0 +1,108 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- history.h + +Abstract: +- Encapsulates the cmdline functions and structures specifically related to + command history functionality. +--*/ + +#pragma once + +// CommandHistory Flags +#define CLE_ALLOCATED 0x00000001 +#define CLE_RESET 0x00000002 + +class CommandHistory +{ +public: + static CommandHistory* s_Allocate(const std::wstring_view appName, const HANDLE processHandle); + static CommandHistory* s_Find(const HANDLE processHandle); + static CommandHistory* s_FindByExe(const std::wstring_view appName); + static void s_ReallocExeToFront(const std::wstring_view appName, const size_t commands); + static void s_Free(const HANDLE processHandle); + static void s_ResizeAll(const size_t commands); + static size_t s_CountOfHistories(); + + enum class MatchOptions + { + None = 0x0, + ExactMatch = 0x1, + JustLooking = 0x2 + }; + + enum class SearchDirection + { + Previous, + Next + }; + + bool FindMatchingCommand(const std::wstring_view command, + const SHORT startingIndex, + SHORT& indexFound, + const MatchOptions options); + bool IsAppNameMatch(const std::wstring_view other) const; + + [[nodiscard]] + HRESULT Add(const std::wstring_view command, + const bool suppressDuplicates); + + [[nodiscard]] + HRESULT Retrieve(const SearchDirection searchDirection, + const gsl::span buffer, + size_t& commandSize); + + [[nodiscard]] + HRESULT RetrieveNth(const SHORT index, + const gsl::span buffer, + size_t& commandSize); + + size_t GetNumberOfCommands() const; + std::wstring_view GetNth(const SHORT index) const; + + void Realloc(const size_t commands); + void Empty(); + + std::wstring Remove(const SHORT iDel); + + bool AtFirstCommand() const; + bool AtLastCommand() const; + + std::wstring_view GetLastCommand() const; + + void Swap(const short indexA, const short indexB); + +private: + void _Reset(); + + // _Next and _Prev go to the next and prev command + // _Inc and _Dec go to the next and prev slots + // Don't get the two confused - it matters when the cmd history is not full! + void _Prev(SHORT& ind) const; + void _Next(SHORT& ind) const; + void _Dec(SHORT& ind) const; + void _Inc(SHORT& ind) const; + + + std::vector _commands; + SHORT _maxCommands; + + std::wstring _appName; + HANDLE _processHandle; + + static std::list s_historyLists; + +public: + DWORD Flags; + SHORT LastDisplayed; + +#ifdef UNIT_TESTING + static void s_ClearHistoryListStorage(); + friend class HistoryTests; +#endif +}; + +DEFINE_ENUM_FLAG_OPERATORS(CommandHistory::MatchOptions); diff --git a/src/host/host-common.vcxproj b/src/host/host-common.vcxproj new file mode 100644 index 000000000..b5a740e86 --- /dev/null +++ b/src/host/host-common.vcxproj @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %(AdditionalIncludeDirectories) + + + diff --git a/src/host/init.cpp b/src/host/init.cpp new file mode 100644 index 000000000..98ac57fbc --- /dev/null +++ b/src/host/init.cpp @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "init.hpp" +#include "resource.h" + +#pragma hdrstop + +// Routine Description: +// - Ensures the SxS initialization for the process. +void InitSideBySide(_Out_writes_(ScratchBufferSize) PWSTR ScratchBuffer, __range(MAX_PATH, MAX_PATH) DWORD ScratchBufferSize) +{ + ACTCTXW actctx = { 0 }; + + // Account for the fact that sidebyside stuff happens in CreateProcess + // but conhost is run with RtlCreateUserProcess. + + // If conhost is at some future date + // launched with CreateProcess or SideBySide support moved + // into the kernel and SideBySide setup moved to textmode, at which + // time this code block will not be needed. + + // Until then, this code block is needed when activated as the default console in the OS by the loader. + // If the console is changed to be invoked a different way (for example if we add a main method that takes + // a parameter to a client application instead), then this code would be unnecessary but not likely harmful. + + // Having SxS not initialized is a problem when 3rd party IMEs attempt to inject into the process and then + // make references to DLLs in the system that are in the SxS cache (ex. a 3rd party IME is loaded and asks for + // comctl32.dll. The load will fail if SxS wasn't initialized.) This was bug# WIN7:681280. + + // We look at the first few chars without being careful about a terminal nul, so init them. + ScratchBuffer[0] = 0; + ScratchBuffer[1] = 0; + ScratchBuffer[2] = 0; + ScratchBuffer[3] = 0; + ScratchBuffer[4] = 0; + ScratchBuffer[5] = 0; + ScratchBuffer[6] = 0; + + // GetModuleFileNameW truncates its result to fit in the buffer, so to detect if we fit, we have to do this. + ScratchBuffer[ScratchBufferSize - 2] = 0; + DWORD const dwModuleFileNameLength = GetModuleFileNameW(nullptr, ScratchBuffer, ScratchBufferSize); + if (dwModuleFileNameLength == 0) + { + RIPMSG1(RIP_ERROR, "GetModuleFileNameW failed %d.\n", GetLastError()); + goto Exit; + } + if (ScratchBuffer[ScratchBufferSize - 2] != 0) + { + RIPMSG1(RIP_ERROR, "GetModuleFileNameW requires more than ScratchBufferSize(%d) - 1.\n", ScratchBufferSize); + goto Exit; + } + + // We get an NT path from the Win32 api. Fix it to be Win32. + UINT NtToWin32PathOffset = 0; + if (ScratchBuffer[0] == '\\' && ScratchBuffer[1] == '?' && ScratchBuffer[2] == '?' && ScratchBuffer[3] == '\\' + //&& ScratchBuffer[4] == a drive letter + && ScratchBuffer[5] == ':' && ScratchBuffer[6] == '\\') + { + NtToWin32PathOffset = 4; + } + + actctx.cbSize = sizeof(actctx); + actctx.dwFlags = (ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_SET_PROCESS_DEFAULT); + actctx.lpResourceName = MAKEINTRESOURCE(IDR_SYSTEM_MANIFEST); + actctx.lpSource = ScratchBuffer + NtToWin32PathOffset; + + HANDLE const hActCtx = CreateActCtxW(&actctx); + + // The error value is INVALID_HANDLE_VALUE. + // ACTCTX_FLAG_SET_PROCESS_DEFAULT has nothing to return upon success, so it returns nullptr. + // There is nothing to cleanup upon ACTCTX_FLAG_SET_PROCESS_DEFAULT success, the data + // is referenced in the PEB, and lasts till process shutdown. + if (hActCtx == INVALID_HANDLE_VALUE) + { + auto const error = GetLastError(); + + // Don't log if it's already set. This whole ordeal is to make sure one is set if there isn't one already. + // If one is already set... good! + if (ERROR_SXS_PROCESS_DEFAULT_ALREADY_SET != error) + { + RIPMSG1(RIP_WARNING, "InitSideBySide failed create an activation context. Error: %d\r\n", error); + } + goto Exit; + } + +Exit: + ScratchBuffer[0] = 0; +} + +// Routine Description: +// - Sets the program files environment variables for the process, if missing. +void InitEnvironmentVariables() +{ + struct + { + LPCWSTR szRegValue; + LPCWSTR szVariable; + } EnvProgFiles[] = + { + { L"ProgramFilesDir", L"ProgramFiles" }, + { L"CommonFilesDir", L"CommonProgramFiles" }, +#if BUILD_WOW64_ENABLED + { L"ProgramFilesDir (x86)", L"ProgramFiles(x86)" }, + { L"CommonFilesDir (x86)", L"CommonProgramFiles(x86)" }, + { L"ProgramW6432Dir", L"ProgramW6432" }, + { L"CommonW6432Dir", L"CommonProgramW6432" } +#endif + }; + + WCHAR wchValue[MAX_PATH]; + for (UINT i = 0; i < ARRAYSIZE(EnvProgFiles); i++) + { + if (!GetEnvironmentVariable(EnvProgFiles[i].szVariable, nullptr, 0)) + { + DWORD dwMaxBufferSize = sizeof(wchValue); + if (RegGetValue(HKEY_LOCAL_MACHINE, + L"Software\\Microsoft\\Windows\\CurrentVersion", + EnvProgFiles[i].szRegValue, + RRF_RT_REG_SZ, + nullptr, + (LPBYTE)wchValue, + &dwMaxBufferSize) == ERROR_SUCCESS) + { + wchValue[(dwMaxBufferSize / sizeof(wchValue[0])) - 1] = 0; + SetEnvironmentVariable(EnvProgFiles[i].szVariable, wchValue); + } + } + } + + // Initialize SxS for the process. + InitSideBySide(wchValue, ARRAYSIZE(wchValue)); +} diff --git a/src/host/init.hpp b/src/host/init.hpp new file mode 100644 index 000000000..4537e4601 --- /dev/null +++ b/src/host/init.hpp @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "precomp.h" + +void InitSideBySide(_Out_writes_(ScratchBufferSize) PWSTR ScratchBuffer, __range(MAX_PATH, MAX_PATH) DWORD ScratchBufferSize); +void InitEnvironmentVariables(); diff --git a/src/host/input.cpp b/src/host/input.cpp new file mode 100644 index 000000000..6ce904b7b --- /dev/null +++ b/src/host/input.cpp @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "input.h" + +#include "dbcs.h" +#include "stream.h" + +#include "..\terminal\input\terminalInput.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +#define KEY_ENHANCED 0x01000000 + +bool IsInProcessedInputMode() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return (gci.pInputBuffer->InputMode & ENABLE_PROCESSED_INPUT) != 0; +} + +bool IsInVirtualTerminalInputMode() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return WI_IsFlagSet(gci.pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT); +} + +BOOL IsSystemKey(const WORD wVirtualKeyCode) +{ + switch (wVirtualKeyCode) + { + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + case VK_PAUSE: + case VK_CAPITAL: + case VK_LWIN: + case VK_RWIN: + case VK_NUMLOCK: + case VK_SCROLL: + return TRUE; + } + return FALSE; +} + +ULONG GetControlKeyState(const LPARAM lParam) +{ + ULONG ControlKeyState = 0; + + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_LMENU) & KEY_PRESSED) + { + ControlKeyState |= LEFT_ALT_PRESSED; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_RMENU) & KEY_PRESSED) + { + ControlKeyState |= RIGHT_ALT_PRESSED; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_LCONTROL) & KEY_PRESSED) + { + ControlKeyState |= LEFT_CTRL_PRESSED; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_RCONTROL) & KEY_PRESSED) + { + ControlKeyState |= RIGHT_CTRL_PRESSED; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_SHIFT) & KEY_PRESSED) + { + ControlKeyState |= SHIFT_PRESSED; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_NUMLOCK) & KEY_TOGGLED) + { + ControlKeyState |= NUMLOCK_ON; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_SCROLL) & KEY_TOGGLED) + { + ControlKeyState |= SCROLLLOCK_ON; + } + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_CAPITAL) & KEY_TOGGLED) + { + ControlKeyState |= CAPSLOCK_ON; + } + if (lParam & KEY_ENHANCED) + { + ControlKeyState |= ENHANCED_KEY; + } + + ControlKeyState |= (lParam & ALTNUMPAD_BIT); + + return ControlKeyState; +} + +// Routine Description: +// - returns true if we're in a mode amenable to us taking over keyboard shortcuts +bool ShouldTakeOverKeyboardShortcuts() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return !gci.GetCtrlKeyShortcutsDisabled() && IsInProcessedInputMode(); +} + +// Routine Description: +// - handles key events without reference to Win32 elements. +void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + bool ContinueProcessing = true; + + if (keyEvent.IsCtrlPressed() && + !keyEvent.IsAltPressed() && + keyEvent.IsKeyDown()) + { + // check for ctrl-c, if in line input mode. + if (keyEvent.GetVirtualKeyCode() == 'C' && IsInProcessedInputMode()) + { + HandleCtrlEvent(CTRL_C_EVENT); + if (gci.PopupCount == 0) + { + gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlC); + } + + if (!(WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED))) + { + ContinueProcessing = false; + } + } + + + // Check for ctrl-break. + else if (keyEvent.GetVirtualKeyCode() == VK_CANCEL) + { + gci.pInputBuffer->Flush(); + HandleCtrlEvent(CTRL_BREAK_EVENT); + if (gci.PopupCount == 0) + { + gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlBreak); + } + + if (!(WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED))) + { + ContinueProcessing = false; + } + } + + // don't write ctrl-esc to the input buffer + else if (keyEvent.GetVirtualKeyCode() == VK_ESCAPE) + { + ContinueProcessing = false; + } + } + else if (keyEvent.IsAltPressed() && + keyEvent.IsKeyDown() && + keyEvent.GetVirtualKeyCode() == VK_ESCAPE) + { + ContinueProcessing = false; + } + + if (ContinueProcessing) + { + size_t EventsWritten = 0; + try + { + EventsWritten = gci.pInputBuffer->Write(std::make_unique(keyEvent)); + if (EventsWritten && generateBreak) + { + keyEvent.SetKeyDown(false); + EventsWritten = gci.pInputBuffer->Write(std::make_unique(keyEvent)); + } + } + catch(...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } +} + +#ifdef DBG +// set to true with a debugger to temporarily disable focus events getting written to the InputBuffer +volatile bool DisableFocusEvents = false; +#endif + +void HandleFocusEvent(const BOOL fSetFocus) +{ +#ifdef DBG + if (DisableFocusEvents) + { + return; + } +#endif + + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + const size_t EventsWritten = gci.pInputBuffer->Write(std::make_unique(!!fSetFocus)); + FAIL_FAST_IF(EventsWritten != 1); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +void HandleMenuEvent(const DWORD wParam) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + size_t EventsWritten = 0; + try + { + EventsWritten = gci.pInputBuffer->Write(std::make_unique(wParam)); + if (EventsWritten != 1) + { + RIPMSG0(RIP_WARNING, "PutInputInBuffer: EventsWritten != 1, 1 expected"); + } + + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +void HandleCtrlEvent(const DWORD EventType) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + switch (EventType) + { + case CTRL_C_EVENT: + gci.CtrlFlags |= CONSOLE_CTRL_C_FLAG; + break; + case CTRL_BREAK_EVENT: + gci.CtrlFlags |= CONSOLE_CTRL_BREAK_FLAG; + break; + case CTRL_CLOSE_EVENT: + gci.CtrlFlags |= CONSOLE_CTRL_CLOSE_FLAG; + break; + default: + RIPMSG1(RIP_ERROR, "Invalid EventType: 0x%x", EventType); + } +} + +void ProcessCtrlEvents() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.CtrlFlags == 0) + { + gci.UnlockConsole(); + return; + } + + // Make our own copy of the console process handle list. + DWORD const LimitingProcessId = gci.LimitingProcessId; + gci.LimitingProcessId = 0; + + ConsoleProcessTerminationRecord* rgProcessHandleList; + size_t cProcessHandleList; + + HRESULT hr = gci.ProcessHandleList + .GetTerminationRecordsByGroupId(LimitingProcessId, + WI_IsFlagSet(gci.CtrlFlags, + CONSOLE_CTRL_CLOSE_FLAG), + &rgProcessHandleList, + &cProcessHandleList); + + if (FAILED(hr) || cProcessHandleList == 0) + { + gci.UnlockConsole(); + return; + } + + // Copy ctrl flags. + ULONG CtrlFlags = gci.CtrlFlags; + FAIL_FAST_IF(!(!((CtrlFlags & (CONSOLE_CTRL_CLOSE_FLAG | CONSOLE_CTRL_BREAK_FLAG | CONSOLE_CTRL_C_FLAG)) && (CtrlFlags & (CONSOLE_CTRL_LOGOFF_FLAG | CONSOLE_CTRL_SHUTDOWN_FLAG))))); + + gci.CtrlFlags = 0; + + gci.UnlockConsole(); + + // the ctrl flags could be a combination of the following + // values: + // + // CONSOLE_CTRL_C_FLAG + // CONSOLE_CTRL_BREAK_FLAG + // CONSOLE_CTRL_CLOSE_FLAG + // CONSOLE_CTRL_LOGOFF_FLAG + // CONSOLE_CTRL_SHUTDOWN_FLAG + + DWORD EventType = (DWORD) - 1; + switch (CtrlFlags & (CONSOLE_CTRL_CLOSE_FLAG | CONSOLE_CTRL_BREAK_FLAG | CONSOLE_CTRL_C_FLAG | CONSOLE_CTRL_LOGOFF_FLAG | CONSOLE_CTRL_SHUTDOWN_FLAG)) + { + + case CONSOLE_CTRL_CLOSE_FLAG: + EventType = CTRL_CLOSE_EVENT; + break; + + case CONSOLE_CTRL_BREAK_FLAG: + EventType = CTRL_BREAK_EVENT; + break; + + case CONSOLE_CTRL_C_FLAG: + EventType = CTRL_C_EVENT; + break; + + case CONSOLE_CTRL_LOGOFF_FLAG: + EventType = CTRL_LOGOFF_EVENT; + break; + + case CONSOLE_CTRL_SHUTDOWN_FLAG: + EventType = CTRL_SHUTDOWN_EVENT; + break; + } + + NTSTATUS Status = STATUS_SUCCESS; + for (size_t i = 0; i < cProcessHandleList; i++) + { + /* + * Status will be non-successful if a process attached to this console + * vetos shutdown. In that case, we don't want to try to kill any more + * processes, but we do need to make sure we continue looping so we + * can close any remaining process handles. The exception is if the + * process is inaccessible, such that we can't even open a handle for + * query. In this case, use best effort to send the close event but + * ignore any errors. + */ + if (NT_SUCCESS(Status)) + { + Status = ServiceLocator::LocateConsoleControl() + ->EndTask((HANDLE)rgProcessHandleList[i].dwProcessID, + EventType, + CtrlFlags); + if (rgProcessHandleList[i].hProcess == nullptr) { + Status = STATUS_SUCCESS; + } + } + + if (rgProcessHandleList[i].hProcess != nullptr) + { + CloseHandle(rgProcessHandleList[i].hProcess); + } + } + + delete[] rgProcessHandleList; +} diff --git a/src/host/input.h b/src/host/input.h new file mode 100644 index 000000000..27b77f505 --- /dev/null +++ b/src/host/input.h @@ -0,0 +1,85 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- input.h + +Abstract: +- This module contains the internal structures and definitions used + by the input (keyboard and mouse) component of the NT console subsystem. + +Author: +- Therese Stowell (Thereses) 12-Nov-1990. Adapted from OS/2 subsystem server\srvpipe.c + +Revision History: +--*/ + +#pragma once + +#include "conapi.h" +#include "screenInfo.hpp" +#include "server.h" // potentially circular include reference + +#include "inputReadHandleData.h" +#include "inputBuffer.hpp" + +#include "../inc/unicode.hpp" + +// indicates how much to change the opacity at each mouse/key toggle +// Opacity is defined as 0-255. 12 is therefore approximately 5% per tick. +#define OPACITY_DELTA_INTERVAL 12 + +#define MAX_CHARS_FROM_1_KEYSTROKE 6 + +#define KEY_TRANSITION_UP 0x80000000 + +class INPUT_KEY_INFO +{ +public: + INPUT_KEY_INFO(const WORD wVirtualKeyCode, const ULONG ulControlKeyState); + ~INPUT_KEY_INFO(); + + const WORD GetVirtualKey() const; + + const bool IsCtrlPressed() const; + const bool IsAltPressed() const; + const bool IsShiftPressed() const; + + const bool IsCtrlOnly() const; + const bool IsShiftOnly() const; + const bool IsShiftAndCtrlOnly() const; + const bool IsAltOnly() const; + + const bool HasNoModifiers() const; + +private: + WORD _wVirtualKeyCode; + ULONG _ulControlKeyState; +}; + +#define TAB_SIZE 8 +#define TAB_MASK (TAB_SIZE - 1) +// WHY IS THIS NOT POSITION % TAB_SIZE?! +#define NUMBER_OF_SPACES_IN_TAB(POSITION) (TAB_SIZE - ((POSITION) & TAB_MASK)) + +// these values are related to GetKeyboardState +#define KEY_PRESSED 0x8000 +#define KEY_TOGGLED 0x01 + +void ClearKeyInfo(const HWND hWnd); + +ULONG GetControlKeyState(const LPARAM lParam); + +bool IsInProcessedInputMode(); +bool IsInVirtualTerminalInputMode(); +bool ShouldTakeOverKeyboardShortcuts(); + +void HandleMenuEvent(const DWORD wParam); +void HandleFocusEvent(const BOOL fSetFocus); +void HandleCtrlEvent(const DWORD EventType); +void HandleGenericKeyEvent(_In_ KeyEvent keyEvent, const bool generateBreak); + +void ProcessCtrlEvents(); + +BOOL IsSystemKey(const WORD wVirtualKeyCode); diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp new file mode 100644 index 000000000..c4565fd9a --- /dev/null +++ b/src/host/inputBuffer.cpp @@ -0,0 +1,869 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inputBuffer.hpp" +#include "dbcs.h" +#include "stream.h" +#include "../types/inc/GlyphWidth.hpp" + +#include + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#define INPUT_BUFFER_DEFAULT_INPUT_MODE (ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_ECHO_INPUT | ENABLE_MOUSE_INPUT) + +// Routine Description: +// - This method creates an input buffer. +// Arguments: +// - None +// Return Value: +// - A new instance of InputBuffer +InputBuffer::InputBuffer() : + InputMode{ INPUT_BUFFER_DEFAULT_INPUT_MODE }, + WaitQueue{}, + _termInput(std::bind(&InputBuffer::_HandleTerminalInputCallback, this, std::placeholders::_1)) +{ + // 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; +} + +// Routine Description: +// - This routine frees the resources associated with an input buffer. +// Arguments: +// - None +// Return Value: +InputBuffer::~InputBuffer() +{ +} + +// Routine Description: +// - checks if any partial char data is available for reading operation +// Arguments: +// - None +// Return Value: +// - true if partial char data is available, false otherwise +bool InputBuffer::IsReadPartialByteSequenceAvailable() +{ + return _readPartialByteSequence.get() != nullptr; +} + +// Routine Description: +// - reads any read partial char data available +// Arguments: +// - peek - if true, data will not be removed after being fetched +// Return Value: +// - the partial char data. may be nullptr if no data is available +std::unique_ptr InputBuffer::FetchReadPartialByteSequence(_In_ bool peek) +{ + if (!IsReadPartialByteSequenceAvailable()) + { + return std::unique_ptr(); + } + + if (peek) + { + return IInputEvent::Create(_readPartialByteSequence->ToInputRecord()); + } + else + { + std::unique_ptr outEvent; + outEvent.swap(_readPartialByteSequence); + return outEvent; + } +} + +// Routine Description: +// - stores partial read char data for a later read. will overwrite +// any previously stored data. +// Arguments: +// - event - The event to store +// Return Value: +// - None +void InputBuffer::StoreReadPartialByteSequence(std::unique_ptr event) +{ + _readPartialByteSequence.swap(event); +} + +// Routine Description: +// - checks if any partial char data is available for writing +// operation. +// Arguments: +// - None +// Return Value: +// - true if partial char data is available, false otherwise +bool InputBuffer::IsWritePartialByteSequenceAvailable() +{ + return _writePartialByteSequence.get() != nullptr; +} + +// Routine Description: +// - writes any write partial char data available +// Arguments: +// - peek - if true, data will not be removed after being fetched +// Return Value: +// - the partial char data. may be nullptr if no data is available +std::unique_ptr InputBuffer::FetchWritePartialByteSequence(_In_ bool peek) +{ + if (!IsWritePartialByteSequenceAvailable()) + { + return std::unique_ptr(); + } + + if (peek) + { + return IInputEvent::Create(_writePartialByteSequence->ToInputRecord()); + } + else + { + std::unique_ptr outEvent; + outEvent.swap(_writePartialByteSequence); + return outEvent; + } +} + +// Routine Description: +// - stores partial write char data. will overwrite any previously +// stored data. +// Arguments: +// - event - The event to store +// Return Value: +// - None +void InputBuffer::StoreWritePartialByteSequence(std::unique_ptr event) +{ + _writePartialByteSequence.swap(event); +} + +// Routine Description: +// - This routine resets the input buffer information fields to their initial values. +// Arguments: +// Return Value: +// Note: +// - The console lock must be held when calling this routine. +void InputBuffer::ReinitializeInputBuffer() +{ + ServiceLocator::LocateGlobals().hInputEvent.ResetEvent(); + InputMode = INPUT_BUFFER_DEFAULT_INPUT_MODE; + _storage.clear(); +} + +// Routine Description: +// - Wakes up readers waiting for data to read. +// Arguments: +// - None +// Return Value: +// - None +void InputBuffer::WakeUpReadersWaitingForData() +{ + WaitQueue.NotifyWaiters(false); +} + +// Routine Description: +// - Wakes up any readers waiting for data when a ctrl-c or ctrl-break is input. +// Arguments: +// - Flag - Indicates reason to terminate the readers. +// Return Value: +// - None +void InputBuffer::TerminateRead(_In_ WaitTerminationReason Flag) +{ + WaitQueue.NotifyWaiters(true, Flag); +} + +// Routine Description: +// - Returns the number of events in the input buffer. +// Arguments: +// - None +// Return Value: +// - The number of events currently in the input buffer. +// Note: +// - The console lock must be held when calling this routine. +size_t InputBuffer::GetNumberOfReadyEvents() const noexcept +{ + return _storage.size(); +} + +// Routine Description: +// - This routine empties the input buffer +// Arguments: +// - None +// Return Value: +// - None +// Note: +// - The console lock must be held when calling this routine. +void InputBuffer::Flush() +{ + _storage.clear(); + ServiceLocator::LocateGlobals().hInputEvent.ResetEvent(); +} + +// Routine Description: +// - This routine removes all but the key events from the buffer. +// Arguments: +// - None +// Return Value: +// - None +// Note: +// - The console lock must be held when calling this routine. +void InputBuffer::FlushAllButKeys() +{ + auto newEnd = std::remove_if(_storage.begin(), _storage.end(), [](const std::unique_ptr& event) + { + return event->EventType() != InputEventType::KeyEvent; + }); + _storage.erase(newEnd, _storage.end()); +} + +// Routine Description: +// - This routine reads from the input buffer. +// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition +// if there isn't enough data in the buffer, and it can be set to not remove records as it reads them out. +// Note: +// - The console lock must be held when calling this routine. +// Arguments: +// - OutEvents - deque to store the read events +// - AmountToRead - the amount of events to try to read +// - Peek - If true, copy events to pInputRecord but don't remove them from the input buffer. +// - WaitForData - if true, wait until an event is input (if there aren't enough to fill client buffer). if false, return immediately +// - Unicode - true if the data in key events should be treated as unicode. false if they should be converted by the current input CP. +// - Stream - true if read should unpack KeyEvents that have a >1 repeat count. AmountToRead must be 1 if Stream is true. +// Return Value: +// - STATUS_SUCCESS if records were read into the client buffer and everything is OK. +// - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed) +// - otherwise a suitable memory/math/string error in NTSTATUS form. +[[nodiscard]] +NTSTATUS InputBuffer::Read(_Out_ std::deque>& OutEvents, + const size_t AmountToRead, + const bool Peek, + const bool WaitForData, + const bool Unicode, + const bool Stream) +{ + try + { + if (_storage.empty()) + { + if (!WaitForData) + { + return STATUS_SUCCESS; + } + return CONSOLE_STATUS_WAIT; + } + + // read from buffer + std::deque> events; + size_t eventsRead; + bool resetWaitEvent; + _ReadBuffer(events, + AmountToRead, + eventsRead, + Peek, + resetWaitEvent, + Unicode, + Stream); + + // copy events to outEvents + while (!events.empty()) + { + OutEvents.push_back(std::move(events.front())); + events.pop_front(); + } + + if (resetWaitEvent) + { + ServiceLocator::LocateGlobals().hInputEvent.ResetEvent(); + } + return STATUS_SUCCESS; + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } +} + +// Routine Description: +// - This routine reads a single event from the input buffer. +// - It can convert returned data to through the currently set Input CP, it can optionally return a wait condition +// if there isn't enough data in the buffer, and it can be set to not remove records as it reads them out. +// Note: +// - The console lock must be held when calling this routine. +// Arguments: +// - outEvent - where the read event is stored +// - Peek - If true, copy events to pInputRecord but don't remove them from the input buffer. +// - WaitForData - if true, wait until an event is input (if there aren't enough to fill client buffer). if false, return immediately +// - Unicode - true if the data in key events should be treated as unicode. false if they should be converted by the current input CP. +// - Stream - true if read should unpack KeyEvents that have a >1 repeat count. +// Return Value: +// - STATUS_SUCCESS if records were read into the client buffer and everything is OK. +// - CONSOLE_STATUS_WAIT if there weren't enough records to satisfy the request (and waits are allowed) +// - otherwise a suitable memory/math/string error in NTSTATUS form. +[[nodiscard]] +NTSTATUS InputBuffer::Read(_Out_ std::unique_ptr& outEvent, + const bool Peek, + const bool WaitForData, + const bool Unicode, + const bool Stream) +{ + NTSTATUS Status; + try + { + std::deque> outEvents; + Status = Read(outEvents, + 1, + Peek, + WaitForData, + Unicode, + Stream); + if (!outEvents.empty()) + { + outEvent.swap(outEvents.front()); + } + } + catch (...) + { + Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + return Status; +} + +// Routine Description: +// - This routine reads from a buffer. It does the buffer manipulation. +// Arguments: +// - outEvents - where read events are placed +// - readCount - amount of events to read +// - eventsRead - where to store number of events read +// - peek - if true , don't remove data from buffer, just copy it. +// - resetWaitEvent - on exit, true if buffer became empty. +// - unicode - true if read should be done in unicode mode +// - streamRead - true if read should unpack KeyEvents that have a >1 repeat count. readCount must be 1 if streamRead is true. +// Return Value: +// - +// Note: +// - The console lock must be held when calling this routine. +void InputBuffer::_ReadBuffer(_Out_ std::deque>& outEvents, + const size_t readCount, + _Out_ size_t& eventsRead, + const bool peek, + _Out_ bool& resetWaitEvent, + const bool unicode, + const bool streamRead) +{ + // when stream reading, the previous behavior was to only allow reading of a single + // event at a time. + FAIL_FAST_IF(streamRead && readCount != 1); + + resetWaitEvent = false; + + std::deque> readEvents; + // we need another var to keep track of how many we've read + // because dbcs records count for two when we aren't doing a + // unicode read but the eventsRead count should return the number + // of events actually put into outRecords. + size_t virtualReadCount = 0; + + while (!_storage.empty() && virtualReadCount < readCount) + { + bool performNormalRead = true; + // for stream reads we need to split any key events that have been coalesced + if (streamRead) + { + if (_storage.front()->EventType() == InputEventType::KeyEvent) + { + KeyEvent* const pKeyEvent = static_cast(_storage.front().get()); + if (pKeyEvent->GetRepeatCount() > 1) + { + // split the key event + std::unique_ptr streamKeyEvent = std::make_unique(*pKeyEvent); + streamKeyEvent->SetRepeatCount(1); + readEvents.push_back(std::move(streamKeyEvent)); + pKeyEvent->SetRepeatCount(pKeyEvent->GetRepeatCount() - 1); + performNormalRead = false; + } + } + } + + if (performNormalRead) + { + readEvents.push_back(std::move(_storage.front())); + _storage.pop_front(); + } + + ++virtualReadCount; + if (!unicode) + { + if (readEvents.back()->EventType() == InputEventType::KeyEvent) + { + const KeyEvent* const pKeyEvent = static_cast(readEvents.back().get()); + if (IsGlyphFullWidth(pKeyEvent->GetCharData())) + { + ++virtualReadCount; + } + } + } + } + + // the amount of events that were actually read + eventsRead = readEvents.size(); + + // copy the events back if we were supposed to peek + if (peek) + { + if (streamRead) + { + // we need to check and see if the event was split from a coalesced key event + // or if it was unrelated to the current front event in storage + if (!readEvents.empty() && + !_storage.empty() && + readEvents.back()->EventType() == InputEventType::KeyEvent && + _storage.front()->EventType() == InputEventType::KeyEvent && + _CanCoalesce(static_cast(*readEvents.back()), + static_cast(*_storage.front()))) + { + KeyEvent& keyEvent = static_cast(*_storage.front()); + keyEvent.SetRepeatCount(keyEvent.GetRepeatCount() + 1); + } + else + { + _storage.push_front(IInputEvent::Create(readEvents.back()->ToInputRecord())); + } + } + else + { + for (auto it = readEvents.crbegin(); it != readEvents.crend(); ++it) + { + _storage.push_front(IInputEvent::Create((*it)->ToInputRecord())); + } + } + } + + // move events read to proper deque + while (!readEvents.empty()) + { + outEvents.push_back(std::move(readEvents.front())); + readEvents.pop_front(); + } + + // signal if we emptied the buffer + if (_storage.empty()) + { + resetWaitEvent = true; + } +} + +// Routine Description: +// - Writes events to the beginning of the input buffer. +// Arguments: +// - inEvents - events to write to buffer. +// - eventsWritten - The number of events written to the buffer on exit. +// Return Value: +// S_OK if successful. +// Note: +// - The console lock must be held when calling this routine. +size_t InputBuffer::Prepend(_Inout_ std::deque>& inEvents) +{ + try + { + _HandleConsoleSuspensionEvents(inEvents); + if (inEvents.empty()) + { + return STATUS_SUCCESS; + } + // read all of the records out of the buffer, then write the + // prepend ones, then write the original set. We need to do it + // this way to handle any coalescing that might occur. + + // get all of the existing records, "emptying" the buffer + std::deque> existingStorage; + existingStorage.swap(_storage); + + // We will need this variable to pass to _WriteBuffer so it can attempt to determine wait status. + // However, because we swapped the storage out from under it with an empty deque, it will always + // return true after the first one (as it is filling the newly emptied backing deque.) + // Then after the second one, because we've inserted some input, it will always say false. + bool unusedWaitStatus = false; + + // write the prepend records + size_t prependEventsWritten; + _WriteBuffer(inEvents, prependEventsWritten, unusedWaitStatus); + FAIL_FAST_IF(!(unusedWaitStatus)); + + // write all previously existing records + size_t existingEventsWritten; + _WriteBuffer(existingStorage, existingEventsWritten, unusedWaitStatus); + FAIL_FAST_IF(!(!unusedWaitStatus)); + + // We need to set the wait event if there were 0 events in the + // input queue when we started. + // Because we did interesting manipulation of the wait queue + // in order to prepend, we can't trust what _WriteBuffer said + // and instead need to set the event if the original backing + // buffer (the one we swapped out at the top) was empty + // when this whole thing started. + if (existingStorage.empty()) + { + ServiceLocator::LocateGlobals().hInputEvent.SetEvent(); + } + WakeUpReadersWaitingForData(); + + return prependEventsWritten; + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return 0; + } +} + +// Routine Description: +// - Writes event to the input buffer. Wakes up any readers that are +// waiting for additional input events. +// Arguments: +// - inEvent - input event to store in the buffer. +// Return Value: +// - The number of events that were written to input buffer. +// Note: +// - The console lock must be held when calling this routine. +// - any outside references to inEvent will ben invalidated after +// calling this method. +size_t InputBuffer::Write(_Inout_ std::unique_ptr inEvent) +{ + try + { + std::deque> inEvents; + inEvents.push_back(std::move(inEvent)); + return Write(inEvents); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return 0; + } +} + +// Routine Description: +// - Writes events to the input buffer. Wakes up any readers that are +// waiting for additional input events. +// Arguments: +// - inEvents - input events to store in the buffer. +// Return Value: +// - The number of events that were written to input buffer. +// Note: +// - The console lock must be held when calling this routine. +size_t InputBuffer::Write(_Inout_ std::deque>& inEvents) +{ + try + { + _HandleConsoleSuspensionEvents(inEvents); + if (inEvents.empty()) + { + return 0; + } + + // Write to buffer. + size_t EventsWritten; + bool SetWaitEvent; + _WriteBuffer(inEvents, EventsWritten, SetWaitEvent); + + if (SetWaitEvent) + { + ServiceLocator::LocateGlobals().hInputEvent.SetEvent(); + } + + // Alert any writers waiting for space. + WakeUpReadersWaitingForData(); + return EventsWritten; + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return 0; + } +} + +// Routine Description: +// - Coalesces input events and transfers them to storage queue. +// Arguments: +// - inRecords - The events to store. +// - eventsWritten - The number of events written since this function +// was called. +// - setWaitEvent - on exit, true if buffer became non-empty. +// Return Value: +// - None +// Note: +// - The console lock must be held when calling this routine. +// - will throw on failure +void InputBuffer::_WriteBuffer(_Inout_ std::deque>& inEvents, + _Out_ size_t& eventsWritten, + _Out_ bool& setWaitEvent) +{ + eventsWritten = 0; + setWaitEvent = false; + const bool initiallyEmptyQueue = _storage.empty(); + const size_t initialInEventsSize = inEvents.size(); + const bool vtInputMode = IsInVirtualTerminalInputMode(); + + while(!inEvents.empty()) + { + // Pop the next event. + // If we're in vt mode, try and handle it with the vt input module. + // If it was handled, do nothing else for it. + // If there was one event passed in, try coalescing it with the previous event currently in the buffer. + // If it's not coalesced, append it to the buffer. + std::unique_ptr inEvent = std::move(inEvents.front()); + inEvents.pop_front(); + if (vtInputMode) + { + const bool handled = _termInput.HandleKey(inEvent.get()); + if (handled) + { + eventsWritten++; + continue; + } + } + + // we only check for possible coalescing when storing one + // record at a time because this is the original behavior of + // the input buffer. Changing this behavior may break stuff + // that was depending on it. + if (initialInEventsSize == 1 && !_storage.empty()) + { + // coalescing requires a deque of events, so push it back onto the front. + inEvents.push_front(std::move(inEvent)); + + bool coalesced = false; + // this looks kinda weird but we don't want to coalesce a + // mouse event and then try to coalesce a key event right after. + // + // we also pass the whole deque to the coalescing methods + // even though they only want one event because it should + // be their responsibility to maintain the correct state + // of the deque if they process any records in it. + if (_CoalesceMouseMovedEvents(inEvents)) + { + coalesced = true; + } + else if (_CoalesceRepeatedKeyPressEvents(inEvents)) + { + coalesced = true; + } + if (coalesced) + { + eventsWritten = 1; + return; + } + else + { + // We didn't coalesce the event. pull it from the queue again, + // to keep the state consistent with the start of this block. + inEvent = std::move(inEvents.front()); + inEvents.pop_front(); + } + } + // At this point, the event was neither coalesced, nor processed by VT. + _storage.push_back(std::move(inEvent)); + ++eventsWritten; + } + if (initiallyEmptyQueue && !_storage.empty()) + { + setWaitEvent = true; + } +} + +// Routine Description: +// - Checks if the last saved event and the first event of inRecords are +// both MOUSE_MOVED events. If they are, the last saved event is +// updated with the new mouse position and the first event of inRecords is +// dropped. +// Arguments: +// - inRecords - The incoming records to process. +// Return Value: +// true if events were coalesced, false if they were not. +// Note: +// - The size of inRecords must be 1. +// - Coalescing here means updating a record that already exists in +// the buffer with updated values from an incoming event, instead of +// storing the incoming event (which would make the original one +// redundant/out of date with the most current state). +bool InputBuffer::_CoalesceMouseMovedEvents(_Inout_ std::deque>& inEvents) +{ + FAIL_FAST_IF(!(inEvents.size() == 1)); + FAIL_FAST_IF(_storage.empty()); + const IInputEvent* const pFirstInEvent = inEvents.front().get(); + const IInputEvent* const pLastStoredEvent = _storage.back().get(); + if (pFirstInEvent->EventType() == InputEventType::MouseEvent && + pLastStoredEvent->EventType() == InputEventType::MouseEvent) + { + const MouseEvent* const pInMouseEvent = static_cast(pFirstInEvent); + const MouseEvent* const pLastMouseEvent = static_cast(pLastStoredEvent); + + if (pInMouseEvent->IsMouseMoveEvent() && + pLastMouseEvent->IsMouseMoveEvent()) + { + // update mouse moved position + MouseEvent* const pMouseEvent = static_cast(_storage.back().release()); + pMouseEvent->SetPosition(pInMouseEvent->GetPosition()); + std::unique_ptr tempPtr(pMouseEvent); + tempPtr.swap(_storage.back()); + + inEvents.pop_front(); + return true; + } + } + return false; +} + +// Routine Description: +// - checks two KeyEvents to see if they're similiar enough to be coalesced +// Arguments: +// - a - the first KeyEvent +// - b - the other KeyEvent +// Return Value: +// - true if the events could be coalesced, false otherwise +bool InputBuffer::_CanCoalesce(const KeyEvent& a, const KeyEvent& b) const noexcept +{ + if (WI_IsFlagSet(a.GetActiveModifierKeys(), NLS_IME_CONVERSION) && + a.GetCharData() == b.GetCharData() && + a.GetActiveModifierKeys() == b.GetActiveModifierKeys()) + { + return true; + } + // other key events check + else if (a.GetVirtualScanCode() == b.GetVirtualScanCode() && + a.GetCharData() == b.GetCharData() && + a.GetActiveModifierKeys() == b.GetActiveModifierKeys()) + { + return true; + } + return false; +} + +// Routine Description:: +// - If the last input event saved and the first input event in inRecords +// are both a keypress down event for the same key, update the repeat +// count of the saved event and drop the first from inRecords. +// Arguments: +// - inRecords - The incoming records to process. +// Return Value: +// true if events were coalesced, false if they were not. +// Note: +// - The size of inRecords must be 1. +// - Coalescing here means updating a record that already exists in +// the buffer with updated values from an incoming event, instead of +// storing the incoming event (which would make the original one +// redundant/out of date with the most current state). +bool InputBuffer::_CoalesceRepeatedKeyPressEvents(_Inout_ std::deque>& inEvents) +{ + FAIL_FAST_IF(!(inEvents.size() == 1)); + FAIL_FAST_IF(_storage.empty()); + const IInputEvent* const pFirstInEvent = inEvents.front().get(); + const IInputEvent* const pLastStoredEvent = _storage.back().get(); + if (pFirstInEvent->EventType() == InputEventType::KeyEvent && + pLastStoredEvent->EventType() == InputEventType::KeyEvent) + { + const KeyEvent* const pInKeyEvent = static_cast(pFirstInEvent); + const KeyEvent* const pLastKeyEvent = static_cast(pLastStoredEvent); + + if (pInKeyEvent->IsKeyDown() && + pLastKeyEvent->IsKeyDown() && + !IsGlyphFullWidth(pInKeyEvent->GetCharData()) && + _CanCoalesce(*pInKeyEvent, *pLastKeyEvent)) + { + // increment repeat count + KeyEvent* const pKeyEvent = static_cast(_storage.back().release()); + WORD repeatCount = pKeyEvent->GetRepeatCount() + pInKeyEvent->GetRepeatCount(); + pKeyEvent->SetRepeatCount(repeatCount); + std::unique_ptr tempPtr(pKeyEvent); + tempPtr.swap(_storage.back()); + + inEvents.pop_front(); + return true; + } + } + return false; +} + +// Routine Description: +// - Handles records that suspend/resume the console. +// Arguments: +// - records - records to check for pause/unpause events +// Return Value: +// - None +// Note: +// - The console lock must be held when calling this routine. +// - will throw exception on error +void InputBuffer::_HandleConsoleSuspensionEvents(_Inout_ std::deque>& inEvents) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + std::deque> outEvents; + while (!inEvents.empty()) + { + std::unique_ptr currEvent = std::move(inEvents.front()); + inEvents.pop_front(); + if (currEvent->EventType() == InputEventType::KeyEvent) + { + const KeyEvent* const pKeyEvent = static_cast(currEvent.get()); + if (pKeyEvent->IsKeyDown()) + { + if (WI_IsFlagSet(gci.Flags, CONSOLE_SUSPENDED) && + !IsSystemKey(pKeyEvent->GetVirtualKeyCode())) + { + UnblockWriteConsole(CONSOLE_OUTPUT_SUSPENDED); + continue; + } + else if (WI_IsFlagSet(InputMode, ENABLE_LINE_INPUT) && pKeyEvent->IsPauseKey()) + { + WI_SetFlag(gci.Flags, CONSOLE_SUSPENDED); + continue; + } + } + } + outEvents.push_back(std::move(currEvent)); + } + inEvents.swap(outEvents); +} + +// Routine Description: +// - Returns true if this input buffer is in VT Input mode. +// Arguments: +// +// Return Value: +// - Returns true if this input buffer is in VT Input mode. +bool InputBuffer::IsInVirtualTerminalInputMode() const +{ + return WI_IsFlagSet(InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT); +} + +// Routine Description: +// - Handler for inserting key sequences into the buffer when the terminal emulation layer +// has determined a key can be converted appropriately into a sequence of inputs +// Arguments: +// - rgInput - Series of input records to insert into the buffer +// - cInput - Length of input records array +// Return Value: +// - +void InputBuffer::_HandleTerminalInputCallback(std::deque>& inEvents) +{ + try + { + // add all input events to the storage queue + while (!inEvents.empty()) + { + std::unique_ptr inEvent = std::move(inEvents.front()); + inEvents.pop_front(); + _storage.push_back(std::move(inEvent)); + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +TerminalInput& InputBuffer::GetTerminalInput() +{ + return _termInput; +} diff --git a/src/host/inputBuffer.hpp b/src/host/inputBuffer.hpp new file mode 100644 index 000000000..9a38a06d2 --- /dev/null +++ b/src/host/inputBuffer.hpp @@ -0,0 +1,110 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- inputBuffer.hpp + +Abstract: +- storage area for incoming input events. + +Author: +- Therese Stowell (Thereses) 12-Nov-1990. Adapted from OS/2 subsystem server\srvpipe.c + +Revision History: +- Moved from input.h/input.cpp. (AustDi, 2017) +- Refactored to class, added stl container usage (AustDi, 2017) +--*/ + +#pragma once + +#include "inputReadHandleData.h" +#include "readData.hpp" +#include "../types/inc/IInputEvent.hpp" + +#include "../server/ObjectHandle.h" +#include "../server/ObjectHeader.h" +#include "../terminal/input/terminalInput.hpp" + +#include + +class InputBuffer final : public ConsoleObjectHeader +{ +public: + DWORD InputMode; + ConsoleWaitQueue WaitQueue; // formerly ReadWaitQueue + bool fInComposition; // specifies if there's an ongoing text composition + + InputBuffer(); + ~InputBuffer(); + + // storage API for partial dbcs bytes being read from the buffer + bool IsReadPartialByteSequenceAvailable(); + std::unique_ptr FetchReadPartialByteSequence(_In_ bool peek); + void StoreReadPartialByteSequence(std::unique_ptr event); + + // storage API for partial dbcs bytes being written to the buffer + bool IsWritePartialByteSequenceAvailable(); + std::unique_ptr FetchWritePartialByteSequence(_In_ bool peek); + void StoreWritePartialByteSequence(std::unique_ptr event); + + void ReinitializeInputBuffer(); + void WakeUpReadersWaitingForData(); + void TerminateRead(_In_ WaitTerminationReason Flag); + size_t GetNumberOfReadyEvents() const noexcept; + void Flush(); + void FlushAllButKeys(); + + [[nodiscard]] + NTSTATUS Read(_Out_ std::deque>& OutEvents, + const size_t AmountToRead, + const bool Peek, + const bool WaitForData, + const bool Unicode, + const bool Stream); + + [[nodiscard]] + NTSTATUS Read(_Out_ std::unique_ptr& inEvent, + const bool Peek, + const bool WaitForData, + const bool Unicode, + const bool Stream); + + + size_t Prepend(_Inout_ std::deque>& inEvents); + + size_t Write(_Inout_ std::unique_ptr inEvent); + size_t Write(_Inout_ std::deque>& inEvents); + + bool IsInVirtualTerminalInputMode() const; + Microsoft::Console::VirtualTerminal::TerminalInput& GetTerminalInput(); + +private: + std::deque> _storage; + std::unique_ptr _readPartialByteSequence; + std::unique_ptr _writePartialByteSequence; + Microsoft::Console::VirtualTerminal::TerminalInput _termInput; + + void _ReadBuffer(_Out_ std::deque>& outEvents, + const size_t readCount, + _Out_ size_t& eventsRead, + const bool peek, + _Out_ bool& resetWaitEvent, + const bool unicode, + const bool streamRead); + + void _WriteBuffer(_Inout_ std::deque>& inRecords, + _Out_ size_t& eventsWritten, + _Out_ bool& setWaitEvent); + + bool _CanCoalesce(const KeyEvent& a, const KeyEvent& b) const noexcept; + bool _CoalesceMouseMovedEvents(_Inout_ std::deque>& inEvents); + bool _CoalesceRepeatedKeyPressEvents(_Inout_ std::deque>& inEvents); + void _HandleConsoleSuspensionEvents(_Inout_ std::deque>& inEvents); + + void _HandleTerminalInputCallback(_In_ std::deque>& inEvents); + +#ifdef UNIT_TESTING + friend class InputBufferTests; +#endif +}; diff --git a/src/host/inputKeyInfo.cpp b/src/host/inputKeyInfo.cpp new file mode 100644 index 000000000..8566e32be --- /dev/null +++ b/src/host/inputKeyInfo.cpp @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "input.h" + +INPUT_KEY_INFO::INPUT_KEY_INFO(const WORD wVirtualKeyCode, const ULONG ulControlKeyState) : + _wVirtualKeyCode(wVirtualKeyCode), + _ulControlKeyState(ulControlKeyState) +{ +} + +INPUT_KEY_INFO::~INPUT_KEY_INFO() +{ +} + +// Routine Description: +// - Gets the keyboard virtual key that was pressed. +// This represents the actual keyboard key, not the modifiers (unless only the modifier was pressed). +// Arguments: +// - +// Return Value: +// - WORD value of key which can be matched to VK_ constants. +const WORD INPUT_KEY_INFO::GetVirtualKey() const +{ + return _wVirtualKeyCode; +} + +// Routine Description: +// - Specifies that the ctrl key was held when this particular input was received. +// Arguments: +// - +// Return Value: +// - True if ctrl was held. False otherwise. +const bool INPUT_KEY_INFO::IsCtrlPressed() const +{ + return (_ulControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) != 0; +} + +// Routine Description: +// - Specifies that the alt key was held when this particular input was received. +// Arguments: +// - +// Return Value: +// - True if alt was held. False otherwise. +const bool INPUT_KEY_INFO::IsAltPressed() const +{ + return (_ulControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED)) != 0; +} + +// Routine Description: +// - Specifies that the shift key was held when this particular input was received. +// Arguments: +// - +// Return Value: +// - True if shift was held. False otherwise. +const bool INPUT_KEY_INFO::IsShiftPressed() const +{ + return (_ulControlKeyState & (SHIFT_PRESSED)) != 0; +} + +// Routine Description: +// - Helps determine if this key input represents a ctrl+KEY combo +// Arguments: +// - +// Return Value: +// - True if control only, not shift nor alt. False otherwise. +const bool INPUT_KEY_INFO::IsCtrlOnly() const +{ + return IsCtrlPressed() && !IsAltPressed() && !IsShiftPressed(); +} + +// Routine Description: +// - Helps determine if this key input represents a shift+KEY combo +// Arguments: +// - +// Return Value: +// - True if shift only, not control nor alt. False otherwise. +const bool INPUT_KEY_INFO::IsShiftOnly() const +{ + return !IsCtrlPressed() && !IsAltPressed() && IsShiftPressed(); +} + +// Routine Description: +// - Helps determine if this key input represents a shift+ctrl+KEY combo +// Arguments: +// - +// Return Value: +// - True if shift and control but not alt. False otherwise. +const bool INPUT_KEY_INFO::IsShiftAndCtrlOnly() const +{ + return IsCtrlPressed() && !IsAltPressed() && IsShiftPressed(); +} + +// Routine Description: +// - Helps determine if this key input represents a alt+KEY combo +// Arguments: +// - +// Return Value: +// - True if alt but not shift or control. False otherwise. +const bool INPUT_KEY_INFO::IsAltOnly() const +{ + return IsAltPressed() && !IsCtrlPressed() && !IsShiftPressed(); +} + +// Routine Description: +// - Determines if there's any modifier for this key +// Arguments: +// - +// Return Value: +// - True if no Alt, Ctrl, or Shift modifier is in place. +const bool INPUT_KEY_INFO::HasNoModifiers() const +{ + return !IsAltPressed() && !IsCtrlPressed() && !IsShiftPressed(); +} diff --git a/src/host/inputReadHandleData.cpp b/src/host/inputReadHandleData.cpp new file mode 100644 index 000000000..670042e42 --- /dev/null +++ b/src/host/inputReadHandleData.cpp @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "inputReadHandleData.h" + +INPUT_READ_HANDLE_DATA::INPUT_READ_HANDLE_DATA() : + _isInputPending{ false }, + _isMultilineInput{ false }, + _readCount{ 0 }, + _buffer{} +{ +} + +bool INPUT_READ_HANDLE_DATA::IsInputPending() const +{ + return _isInputPending; +} + +bool INPUT_READ_HANDLE_DATA::IsMultilineInput() const +{ + FAIL_FAST_IF(!_isInputPending); // we shouldn't have multiline input without a pending input. + return _isMultilineInput; +} + +void INPUT_READ_HANDLE_DATA::SaveMultilinePendingInput(std::wstring_view pending) +{ + _isMultilineInput = true; + SavePendingInput(pending); +} + +void INPUT_READ_HANDLE_DATA::SavePendingInput(std::wstring_view pending) +{ + _isInputPending = true; + UpdatePending(pending); +} + +void INPUT_READ_HANDLE_DATA::UpdatePending(std::wstring_view pending) +{ + _buffer = pending; +} + +void INPUT_READ_HANDLE_DATA::CompletePending() +{ + _isInputPending = false; + _isMultilineInput = false; + _buffer.clear(); +} + +std::wstring_view INPUT_READ_HANDLE_DATA::GetPendingInput() const +{ + return _buffer; +} + +void INPUT_READ_HANDLE_DATA::IncrementReadCount() +{ + _readCount++; +} + +void INPUT_READ_HANDLE_DATA::DecrementReadCount() +{ + const size_t prevCount = _readCount.fetch_sub(1); + FAIL_FAST_IF(prevCount == 0); // we just underflowed, that's a programming error. +} + +size_t INPUT_READ_HANDLE_DATA::GetReadCount() +{ + return _readCount; +} diff --git a/src/host/inputReadHandleData.h b/src/host/inputReadHandleData.h new file mode 100644 index 000000000..efa86722f --- /dev/null +++ b/src/host/inputReadHandleData.h @@ -0,0 +1,57 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- InputReadHandleData.h + +Abstract: +- This file defines counters and state information related to reading input from the internal buffers + when called from a particular input handle that has been given to a client application. + It's used to know where the next bit of continuation should be if the same input handle doesn't have a big + enough buffer and must split data over multiple returns. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + + +#pragma once + +class INPUT_READ_HANDLE_DATA +{ +public: + INPUT_READ_HANDLE_DATA(); + + ~INPUT_READ_HANDLE_DATA() = default; + INPUT_READ_HANDLE_DATA(const INPUT_READ_HANDLE_DATA&) = delete; + INPUT_READ_HANDLE_DATA(INPUT_READ_HANDLE_DATA&&) = delete; + INPUT_READ_HANDLE_DATA& operator=(const INPUT_READ_HANDLE_DATA&) & = delete; + INPUT_READ_HANDLE_DATA& operator=(INPUT_READ_HANDLE_DATA&&) & = delete; + + void IncrementReadCount(); + void DecrementReadCount(); + size_t GetReadCount(); + + bool IsInputPending() const; + bool IsMultilineInput() const; + + void SavePendingInput(std::wstring_view pending); + void SaveMultilinePendingInput(std::wstring_view pending); + void UpdatePending(std::wstring_view pending); + void CompletePending(); + + std::wstring_view GetPendingInput() const; + +private: + + bool _isInputPending; + bool _isMultilineInput; + + std::wstring _buffer; + + std::atomic _readCount; +}; diff --git a/src/host/lib/hostlib.vcxproj b/src/host/lib/hostlib.vcxproj new file mode 100644 index 000000000..83a0e5545 --- /dev/null +++ b/src/host/lib/hostlib.vcxproj @@ -0,0 +1,16 @@ + + + + + + + {06EC74CB-9A12-429C-B551-8562EC954746} + Win32Proj + hostlib + Host + ConhostV2Lib + + + + + \ No newline at end of file diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters new file mode 100644 index 000000000..76ef60a67 --- /dev/null +++ b/src/host/lib/hostlib.vcxproj.filters @@ -0,0 +1,363 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/host/lib/sources b/src/host/lib/sources new file mode 100644 index 000000000..ea0addf01 --- /dev/null +++ b/src/host/lib/sources @@ -0,0 +1,9 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConhostV2 +TARGETTYPE = LIBRARY +UMTYPE = windows diff --git a/src/host/misc.cpp b/src/host/misc.cpp new file mode 100644 index 000000000..912f45a70 --- /dev/null +++ b/src/host/misc.cpp @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "misc.h" +#include "dbcs.h" + +#include "../types/inc/convert.hpp" +#include "../types/inc/GlyphWidth.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +#define CHAR_NULL ((char)0) + + +WCHAR CharToWchar(_In_reads_(cch) const char * const pch, const UINT cch) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WCHAR wc = L'\0'; + + FAIL_FAST_IF(!(IsDBCSLeadByteConsole(*pch, &gci.OutputCPInfo) || cch == 1)); + + ConvertOutputToUnicode(gci.OutputCP, pch, cch, &wc, 1); + + return wc; +} + +void SetConsoleCPInfo(const BOOL fOutput) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (fOutput) + { + // If we're changing the output codepage, we want to update the font as well to give the engine an opportunity + // to pick a more appropriate font should the current one be unable to render in the new codepage. + // To do this, we create a copy of the existing font but we change the codepage value to be the new one that was just set in the global structures. + // NOTE: We need to do this only if everything is set up. This can get called while we're still initializing, so carefully check things for nullptr. + if (gci.HasActiveOutputBuffer()) + { + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + const FontInfo& fiOld = screenInfo.GetCurrentFont(); + + // Use the desired face name when updating the font. + // This ensures that if we had a fall back operation last time (the desired + // face name didn't support the code page and we have a different less-desirable font currently) + // that we'll now give it another shot to use the desired face name in the new code page. + FontInfo fiNew(screenInfo.GetDesiredFont().GetFaceName(), + fiOld.GetFamily(), + fiOld.GetWeight(), + fiOld.GetUnscaledSize(), + gci.OutputCP); + screenInfo.UpdateFont(&fiNew); + } + + if (!GetCPInfo(gci.OutputCP, &gci.OutputCPInfo)) + { + gci.OutputCPInfo.LeadByte[0] = 0; + } + } + else + { + if (!GetCPInfo(gci.CP, &gci.CPInfo)) + { + gci.CPInfo.LeadByte[0] = 0; + } + } +} + +// Routine Description: +// - This routine check bisected on Unicode string end. +// Arguments: +// - pwchBuffer - Pointer to Unicode string buffer. +// - cWords - Number of Unicode string. +// - cBytes - Number of bisect position by byte counts. +// Return Value: +// - TRUE - Bisected character. +// - FALSE - Correctly. +BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR * pwchBuffer, + _In_ size_t cWords, + _In_ size_t cBytes) noexcept +{ + while (cWords && cBytes) + { + if (IsGlyphFullWidth(*pwchBuffer)) + { + if (cBytes < 2) + { + return TRUE; + } + else + { + cWords--; + cBytes -= 2; + pwchBuffer++; + } + } + else + { + cWords--; + cBytes--; + pwchBuffer++; + } + } + + return FALSE; +} + +// Routine Description: +// - This routine check bisected on Unicode string end. +// Arguments: +// - ScreenInfo - reference to screen information structure. +// - pwchBuffer - Pointer to Unicode string buffer. +// - cWords - Number of Unicode string. +// - cBytes - Number of bisect position by byte counts. +// - fEcho - TRUE if called by Read (echoing characters) +// Return Value: +// - TRUE - Bisected character. +// - FALSE - Correctly. +BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, + _In_reads_bytes_(cBytes) const WCHAR * pwchBuffer, + _In_ size_t cWords, + _In_ size_t cBytes, + _In_ SHORT sOriginalXPosition, + _In_ BOOL fEcho) +{ + if (WI_IsFlagSet(ScreenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) + { + while (cWords && cBytes) + { + WCHAR const Char = *pwchBuffer; + if (Char >= UNICODE_SPACE) + { + if (IsGlyphFullWidth(Char)) + { + if (cBytes < 2) + { + return TRUE; + } + else + { + cWords--; + cBytes -= 2; + pwchBuffer++; + sOriginalXPosition += 2; + } + } + else + { + cWords--; + cBytes--; + pwchBuffer++; + sOriginalXPosition++; + } + } + else + { + cWords--; + pwchBuffer++; + switch (Char) + { + case UNICODE_BELL: + if (fEcho) + goto CtrlChar; + break; + case UNICODE_BACKSPACE: + case UNICODE_LINEFEED: + case UNICODE_CARRIAGERETURN: + break; + case UNICODE_TAB: + { + size_t TabSize = NUMBER_OF_SPACES_IN_TAB(sOriginalXPosition); + sOriginalXPosition = (SHORT)(sOriginalXPosition + TabSize); + if (cBytes < TabSize) + return TRUE; + cBytes -= TabSize; + break; + } + default: + if (fEcho) + { + CtrlChar: + if (cBytes < 2) + return TRUE; + cBytes -= 2; + sOriginalXPosition += 2; + } + else + { + cBytes--; + sOriginalXPosition++; + } + } + } + } + return FALSE; + } + else + { + return CheckBisectStringW(pwchBuffer, cWords, cBytes); + } +} + + +// Routine Description: +// - Converts all key events in the deque to the oem char data and adds +// them back to events. +// Arguments: +// - events - on input the IInputEvents to convert. on output, the +// converted input events +// Note: may throw on error +void SplitToOem(std::deque>& events) +{ + const UINT codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP; + + // convert events to oem codepage + std::deque> convertedEvents; + while (!events.empty()) + { + std::unique_ptr currentEvent = std::move(events.front()); + events.pop_front(); + if (currentEvent->EventType() == InputEventType::KeyEvent) + { + const KeyEvent* const pKeyEvent = static_cast(currentEvent.get()); + // convert from wchar to char + std::wstring wstr{ pKeyEvent->GetCharData() }; + const auto str = ConvertToA(codepage, wstr); + + for (auto& ch : str) + { + std::unique_ptr tempEvent = std::make_unique(*pKeyEvent); + tempEvent->SetCharData(ch); + convertedEvents.push_back(std::move(tempEvent)); + } + } + else + { + convertedEvents.push_back(std::move(currentEvent)); + } + } + // move all events back + while (!convertedEvents.empty()) + { + events.push_back(std::move(convertedEvents.front())); + convertedEvents.pop_front(); + } +} + +// Routine Description: +// - Converts unicode characters to ANSI given a destination codepage +// Arguments: +// - uiCodePage - codepage for use in conversion +// - pwchSource - unicode string to convert +// - cchSource - length of pwchSource in characters +// - pchTarget - pointer to destination buffer to receive converted ANSI string +// - cchTarget - size of destination buffer in characters +// Return Value: +// - Returns the number characters written to pchTarget, or 0 on failure +int ConvertToOem(const UINT uiCodePage, + _In_reads_(cchSource) const WCHAR * const pwchSource, + const UINT cchSource, + _Out_writes_(cchTarget) CHAR * const pchTarget, + const UINT cchTarget) noexcept +{ + FAIL_FAST_IF(!(pwchSource != (LPWSTR) pchTarget)); + DBGCHARS(("ConvertToOem U->%d %.*ls\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pwchSource)); + #pragma prefast(suppress:__WARNING_W2A_BEST_FIT, "WC_NO_BEST_FIT_CHARS doesn't work in many codepages. Retain old behavior.") + return LOG_IF_WIN32_BOOL_FALSE(WideCharToMultiByte(uiCodePage, 0, pwchSource, cchSource, pchTarget, cchTarget, nullptr, nullptr)); +} + +// Data in the output buffer is the true unicode value. +int ConvertInputToUnicode(const UINT uiCodePage, + _In_reads_(cchSource) const CHAR * const pchSource, + const UINT cchSource, + _Out_writes_(cchTarget) WCHAR * const pwchTarget, + const UINT cchTarget) noexcept +{ + DBGCHARS(("ConvertInputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); + + return MultiByteToWideChar(uiCodePage, 0, pchSource, cchSource, pwchTarget, cchTarget); +} + +// Output data is always translated via the ansi codepage so glyph translation works. +int ConvertOutputToUnicode(_In_ UINT uiCodePage, + _In_reads_(cchSource) const CHAR * const pchSource, + _In_ UINT cchSource, + _Out_writes_(cchTarget) WCHAR *pwchTarget, + _In_ UINT cchTarget) noexcept +{ + FAIL_FAST_IF(!(cchTarget > 0)); + pwchTarget[0] = L'\0'; + + DBGCHARS(("ConvertOutputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); + + if (DoBuffersOverlap(reinterpret_cast(pchSource), + cchSource * sizeof(CHAR), + reinterpret_cast(pwchTarget), + cchTarget * sizeof(WCHAR))) + { + try + { + // buffers overlap so we need to copy one + std::string copyData(pchSource, cchSource); + return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, copyData.data(), cchSource, pwchTarget, cchTarget); + } + catch (...) + { + return 0; + } + } + else + { + return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); + } +} + +// Routine Description: +// - checks if two buffers overlap +// Arguments: +// - pBufferA - pointer to start of first buffer +// - cbBufferA - size of first buffer, in bytes +// - pBufferB - pointer to start of second buffer +// - cbBufferB - size of second buffer, in bytes +// Return Value: +// - true if buffers overlap, false otherwise +bool DoBuffersOverlap(const BYTE* const pBufferA, + const UINT cbBufferA, + const BYTE* const pBufferB, + const UINT cbBufferB) noexcept +{ + const BYTE* const pBufferAEnd = pBufferA + cbBufferA; + const BYTE* const pBufferBEnd = pBufferB + cbBufferB; + return (pBufferA <= pBufferB && pBufferAEnd >= pBufferB) || (pBufferB <= pBufferA && pBufferBEnd >= pBufferA); +} diff --git a/src/host/misc.h b/src/host/misc.h new file mode 100644 index 000000000..c4598d645 --- /dev/null +++ b/src/host/misc.h @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- misc.h + +Abstract: +- This file implements the NT console server font routines. + +Author: +- Therese Stowell (ThereseS) 22-Jan-1991 + +Revision History: + - Mike Griese, 30-oct-2017: Moved all functions that didn't require the host + to the contypes lib. The ones that are still here in one way or another + require code from the host to build. +--*/ + +#pragma once + +#include "screenInfo.hpp" +#include "../types/inc/IInputEvent.hpp" +#include +#include + +WCHAR CharToWchar(_In_reads_(cch) const char * const pch, const UINT cch); + +void SetConsoleCPInfo(const BOOL fOutput); + +BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR * pwchBuffer, + _In_ size_t cWords, + _In_ size_t cBytes) noexcept; +BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, + _In_reads_bytes_(cBytes) const WCHAR * pwchBuffer, + _In_ size_t cWords, + _In_ size_t cBytes, + _In_ SHORT sOriginalXPosition, + _In_ BOOL fEcho); + +int ConvertToOem(const UINT uiCodePage, + _In_reads_(cchSource) const WCHAR * const pwchSource, + const UINT cchSource, + _Out_writes_(cchTarget) CHAR * const pchTarget, + const UINT cchTarget) noexcept; + +void SplitToOem(std::deque>& events); + +int ConvertInputToUnicode(const UINT uiCodePage, + _In_reads_(cchSource) const CHAR * const pchSource, + const UINT cchSource, + _Out_writes_(cchTarget) WCHAR * const pwchTarget, + const UINT cchTarget) noexcept; + + +int ConvertOutputToUnicode(_In_ UINT uiCodePage, + _In_reads_(cchSource) const CHAR * const pchSource, + _In_ UINT cchSource, + _Out_writes_(cchTarget) WCHAR *pwchTarget, + _In_ UINT cchTarget) noexcept; + +bool DoBuffersOverlap(const BYTE* const pBufferA, + const UINT cbBufferA, + const BYTE* const pBufferB, + const UINT cbBufferB) noexcept; diff --git a/src/host/ntprivapi.cpp b/src/host/ntprivapi.cpp new file mode 100644 index 000000000..5a889e7f3 --- /dev/null +++ b/src/host/ntprivapi.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "NtPrivApi.hpp" + +[[nodiscard]] +NTSTATUS NtPrivApi::s_GetProcessParentId(_Inout_ PULONG ProcessId) +{ + // TODO: Get Parent current not really available without winternl + NtQueryInformationProcess. http://osgvsowi/8394495 + OBJECT_ATTRIBUTES oa; + InitializeObjectAttributes(&oa, nullptr, 0, nullptr, nullptr); + + CLIENT_ID ClientId; + ClientId.UniqueProcess = UlongToHandle(*ProcessId); + ClientId.UniqueThread = 0; + + HANDLE ProcessHandle; + NTSTATUS Status = s_NtOpenProcess(&ProcessHandle, PROCESS_QUERY_LIMITED_INFORMATION, &oa, &ClientId); + + PROCESS_BASIC_INFORMATION BasicInfo = { 0 }; + if (NT_SUCCESS(Status)) + { + Status = s_NtQueryInformationProcess(ProcessHandle, ProcessBasicInformation, &BasicInfo, sizeof(BasicInfo), nullptr); + LOG_IF_FAILED(s_NtClose(ProcessHandle)); + } + + if (!NT_SUCCESS(Status)) + { + *ProcessId = 0; + return Status; + } + + // This is the actual field name, but in the public SDK, it's named Reserved3. We need to pursue publishing the real name. + //*ProcessId = (ULONG)BasicInfo.InheritedFromUniqueProcessId; + *ProcessId = (ULONG)BasicInfo.Reserved3; + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS NtPrivApi::s_NtOpenProcess(_Out_ PHANDLE ProcessHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _In_opt_ PCLIENT_ID ClientId) +{ + HMODULE hNtDll = _Instance()._hNtDll; + + if (hNtDll != nullptr) + { + typedef NTSTATUS(*PfnNtOpenProcess)(HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId); + + static PfnNtOpenProcess pfn = (PfnNtOpenProcess)GetProcAddress(hNtDll, "NtOpenProcess"); + + if (pfn != nullptr) + { + return pfn(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); + } + } + + return STATUS_UNSUCCESSFUL; +} + +[[nodiscard]] +NTSTATUS NtPrivApi::s_NtQueryInformationProcess(_In_ HANDLE ProcessHandle, + _In_ PROCESSINFOCLASS ProcessInformationClass, + _Out_ PVOID ProcessInformation, + _In_ ULONG ProcessInformationLength, + _Out_opt_ PULONG ReturnLength) +{ + HMODULE hNtDll = _Instance()._hNtDll; + + if (hNtDll != nullptr) + { + typedef NTSTATUS(*PfnNtQueryInformationProcess)(HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength); + + static PfnNtQueryInformationProcess pfn = (PfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess"); + + if (pfn != nullptr) + { + return pfn(ProcessHandle, ProcessInformationClass, ProcessInformation, ProcessInformationLength, ReturnLength); + } + } + + return STATUS_UNSUCCESSFUL; +} + +[[nodiscard]] +NTSTATUS NtPrivApi::s_NtClose(_In_ HANDLE Handle) +{ + HMODULE hNtDll = _Instance()._hNtDll; + + if (hNtDll != nullptr) + { + typedef NTSTATUS(*PfnNtClose)(HANDLE Handle); + + static PfnNtClose pfn = (PfnNtClose)GetProcAddress(hNtDll, "NtClose"); + + if (pfn != nullptr) + { + return pfn(Handle); + } + } + + return STATUS_UNSUCCESSFUL; +} + +NtPrivApi::NtPrivApi() +{ + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _hNtDll = LoadLibraryExW(L"ntdll.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); +} + +NtPrivApi::~NtPrivApi() +{ + if (_hNtDll != nullptr) + { + FreeLibrary(_hNtDll); + _hNtDll = nullptr; + } +} + +NtPrivApi& NtPrivApi::_Instance() +{ + static NtPrivApi ntapi; + return ntapi; +} diff --git a/src/host/ntprivapi.hpp b/src/host/ntprivapi.hpp new file mode 100644 index 000000000..f9554dfd9 --- /dev/null +++ b/src/host/ntprivapi.hpp @@ -0,0 +1,68 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- userdpiapi.hpp + +Abstract: +- This module is used for abstracting calls to ntdll DLL APIs to break DDK dependencies. + +Author(s): +- Michael Niksa (MiNiksa) July-2016 +--*/ +#pragma once + +#include "conddkrefs.h" + +// From winternl.h + +typedef enum _PROCESSINFOCLASS { + ProcessBasicInformation = 0, + ProcessDebugPort = 7, + ProcessWow64Information = 26, + ProcessImageFileName = 27, + ProcessBreakOnTermination = 29 +} PROCESSINFOCLASS; + +typedef struct _PROCESS_BASIC_INFORMATION { + PVOID Reserved1; + PVOID PebBaseAddress; + PVOID Reserved2[2]; + ULONG_PTR UniqueProcessId; + ULONG_PTR Reserved3; +} PROCESS_BASIC_INFORMATION; +typedef PROCESS_BASIC_INFORMATION *PPROCESS_BASIC_INFORMATION; + +// end From winternl.h + +class NtPrivApi sealed +{ +public: + [[nodiscard]] + static NTSTATUS s_GetProcessParentId(_Inout_ PULONG ProcessId); + + ~NtPrivApi(); + +private: + [[nodiscard]] + static NTSTATUS s_NtOpenProcess(_Out_ PHANDLE ProcessHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _In_opt_ PCLIENT_ID ClientId); + + [[nodiscard]] + static NTSTATUS s_NtQueryInformationProcess(_In_ HANDLE ProcessHandle, + _In_ PROCESSINFOCLASS ProcessInformationClass, + _Out_ PVOID ProcessInformation, + _In_ ULONG ProcessInformationLength, + _Out_opt_ PULONG ReturnLength); + + [[nodiscard]] + static NTSTATUS s_NtClose(_In_ HANDLE Handle); + + static NtPrivApi& _Instance(); + HMODULE _hNtDll; + + NtPrivApi(); +}; diff --git a/src/host/output.cpp b/src/host/output.cpp new file mode 100644 index 000000000..ef0f83b83 --- /dev/null +++ b/src/host/output.cpp @@ -0,0 +1,526 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "_output.h" +#include "output.h" +#include "handle.h" + +#include "getset.h" +#include "misc.h" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/Viewport.hpp" +#include "../types/inc/convert.hpp" + +#pragma hdrstop +using namespace Microsoft::Console::Types; + +// This routine figures out what parameters to pass to CreateScreenBuffer based on the data from STARTUPINFO and the +// registry defaults, and then calls CreateScreenBuffer. +[[nodiscard]] +NTSTATUS DoCreateScreenBuffer() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + FontInfo fiFont(gci.GetFaceName(), + static_cast(gci.GetFontFamily()), + gci.GetFontWeight(), + gci.GetFontSize(), + gci.GetCodePage()); + + // For East Asian version, we want to get the code page from the registry or shell32, so we can specify console + // codepage by console.cpl or shell32. The default codepage is OEMCP. + gci.CP = gci.GetCodePage(); + gci.OutputCP = gci.GetCodePage(); + + gci.Flags |= CONSOLE_USE_PRIVATE_FLAGS; + + NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(gci.GetWindowSize(), + fiFont, + gci.GetScreenBufferSize(), + gci.GetDefaultAttributes(), + TextAttribute{ gci.GetPopupFillAttribute() }, + gci.GetCursorSize(), + &gci.ScreenBuffers); + + // TODO: MSFT 9355013: This needs to be resolved. We increment it once with no handle to ensure it's never cleaned up + // and one always exists for the renderer (and potentially other functions.) + // It's currently a load-bearing piece of code. http://osgvsowi/9355013 + if (NT_SUCCESS(Status)) + { + gci.ScreenBuffers[0].IncrementOriginalScreenBuffer(); + } + + return Status; +} + +// Routine Description: +// - This routine copies a rectangular region from the screen buffer to the screen buffer. +// Arguments: +// - screenInfo - reference to screen info +// - source - rectangle in source buffer to copy +// - targetOrigin - upper left coordinates of new location rectangle +static void _CopyRectangle(SCREEN_INFORMATION& screenInfo, + const Viewport& source, + const COORD targetOrigin) +{ + const auto sourceOrigin = source.Origin(); + + // 0. If the source and the target are the same... we have nothing to do. Leave. + if (sourceOrigin == targetOrigin) + { + return; + } + + // 1. If we're copying entire rows of the buffer and moving them directly up or down, + // then we can send a rotate command to the underlying buffer to just adjust the + // row locations instead of copying or moving anything. + { + const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); + const auto sourceFullRows = source.Width() == bufferSize.X; + const auto verticalCopyOnly = source.Left() == 0 && targetOrigin.X == 0; + if (sourceFullRows && verticalCopyOnly) + { + const auto delta = targetOrigin.Y - source.Top(); + + screenInfo.GetTextBuffer().ScrollRows(source.Top(), source.Height(), gsl::narrow(delta)); + + return; + } + } + + // 2. We can move any other scenario in-place without copying. We just have to carefully + // choose which direction we walk through filling up the target so it doesn't accidentally + // erase the source material before it can be copied/moved to the new location. + { + const auto target = Viewport::FromDimensions(targetOrigin, source.Dimensions()); + const auto walkDirection = Viewport::DetermineWalkDirection(source, target); + + auto sourcePos = source.GetWalkOrigin(walkDirection); + auto targetPos = target.GetWalkOrigin(walkDirection); + + do + { + const auto data = OutputCell(*screenInfo.GetCellDataAt(sourcePos)); + screenInfo.Write(OutputCellIterator({ &data, 1 }), targetPos); + + source.WalkInBounds(sourcePos, walkDirection); + } while (target.WalkInBounds(targetPos, walkDirection)); + } +} + +// Routine Description: +// - This routine reads a sequence of attributes from the screen buffer. +// Arguments: +// - screenInfo - reference to screen buffer information. +// - coordRead - Screen buffer coordinate to begin reading from. +// - amountToRead - the number of elements to read +// Return Value: +// - vector of attribute data +std::vector ReadOutputAttributes(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead) +{ + // Short circuit. If nothing to read, leave early. + if (amountToRead == 0) + { + return {}; + } + + // Short circuit, if reading out of bounds, leave early. + if (!screenInfo.GetBufferSize().IsInBounds(coordRead)) + { + return {}; + } + + // Get iterator to the position we should start reading at. + auto it = screenInfo.GetCellDataAt(coordRead); + // Count up the number of cells we've attempted to read. + ULONG amountRead = 0; + // Prepare the return value string. + std::vector retVal; + // Reserve the number of cells. If we have >U+FFFF, it will auto-grow later and that's OK. + retVal.reserve(amountToRead); + + // While we haven't read enough cells yet and the iterator is still valid (hasn't reached end of buffer) + while (amountRead < amountToRead && it) + { + // If the first thing we read is trailing, pad with a space. + // OR If the last thing we read is leading, pad with a space. + if ((amountRead == 0 && it->DbcsAttr().IsTrailing()) || + (amountRead == (amountToRead - 1) && it->DbcsAttr().IsLeading())) + { + retVal.push_back(it->TextAttr().GetLegacyAttributes()); + } + else + { + retVal.push_back(it->TextAttr().GetLegacyAttributes() | it->DbcsAttr().GeneratePublicApiAttributeFormat()); + } + + amountRead++; + it++; + } + + return retVal; +} + +// Routine Description: +// - This routine reads a sequence of unicode characters from the screen buffer +// Arguments: +// - screenInfo - reference to screen buffer information. +// - coordRead - Screen buffer coordinate to begin reading from. +// - amountToRead - the number of elements to read +// Return Value: +// - wstring +std::wstring ReadOutputStringW(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead) +{ + // Short circuit. If nothing to read, leave early. + if (amountToRead == 0) + { + return {}; + } + + // Short circuit, if reading out of bounds, leave early. + if (!screenInfo.GetBufferSize().IsInBounds(coordRead)) + { + return {}; + } + + // Get iterator to the position we should start reading at. + auto it = screenInfo.GetCellDataAt(coordRead); + + // Count up the number of cells we've attempted to read. + ULONG amountRead = 0; + + // Prepare the return value string. + std::wstring retVal; + retVal.reserve(amountToRead); // Reserve the number of cells. If we have >U+FFFF, it will auto-grow later and that's OK. + + // While we haven't read enough cells yet and the iterator is still valid (hasn't reached end of buffer) + while (amountRead < amountToRead && it) + { + // If the first thing we read is trailing, pad with a space. + // OR If the last thing we read is leading, pad with a space. + if ((amountRead == 0 && it->DbcsAttr().IsTrailing()) || + (amountRead == (amountToRead - 1) && it->DbcsAttr().IsLeading())) + { + retVal += UNICODE_SPACE; + } + else + { + // Otherwise, add anything that isn't a trailing cell. (Trailings are duplicate copies of the leading.) + if (!it->DbcsAttr().IsTrailing()) + { + retVal += it->Chars(); + } + } + + amountRead++; + it++; + } + + return retVal; +} + +// Routine Description: +// - This routine reads a sequence of ascii characters from the screen buffer +// Arguments: +// - screenInfo - reference to screen buffer information. +// - coordRead - Screen buffer coordinate to begin reading from. +// - amountToRead - the number of elements to read +// Return Value: +// - string of char data +std::string ReadOutputStringA(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead) +{ + const auto wstr = ReadOutputStringW(screenInfo, coordRead, amountToRead); + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return ConvertToA(gci.OutputCP, wstr); +} + +void ScreenBufferSizeChange(const COORD coordNewSize) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + gci.pInputBuffer->Write(std::make_unique(coordNewSize)); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +// Routine Description: +// - This is simply a notifier method to let accessibility and renderers know that a region of the buffer +// has been copied/moved to another location in a block fashion. +// Arguments: +// - screenInfo - The relevant screen buffer where data was moved +// - source - The viewport describing the region where data was copied from +// - fill - The viewport describing the area that was filled in with the fill character (uncovered area) +// - target - The viewport describing the region where data was copied to +static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source, const Viewport& fill, const Viewport& target) +{ + if (screenInfo.IsActiveScreenBuffer()) + { + IAccessibilityNotifier *pNotifier = ServiceLocator::LocateAccessibilityNotifier(); + if (pNotifier != nullptr) + { + pNotifier->NotifyConsoleUpdateScrollEvent(target.Origin().X - source.Left(), target.Origin().Y - source.RightInclusive()); + } + } + + // Get the render target and send it commands. + // It will figure out whether or not we're active and where the messages need to go. + auto& render = screenInfo.GetRenderTarget(); + // Redraw anything in the target area + render.TriggerRedraw(target); + // Also redraw anything that was filled. + render.TriggerRedraw(fill); +} + +// Routine Description: +// - This routine is a special-purpose scroll for use by AdjustCursorPosition. +// Arguments: +// - 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) +{ + // Rotate the circular buffer around and wipe out the previous final line. + bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(); + if (fSuccess) + { + // Trigger a graphical update if we're active. + if (screenInfo.IsActiveScreenBuffer()) + { + COORD coordDelta = { 0 }; + coordDelta.Y = -1; + + IAccessibilityNotifier *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); + } + } + } + return fSuccess; +} + +// Routine Description: +// - This routine copies ScrollRectangle to DestinationOrigin then fills in ScrollRectangle with Fill. +// - The scroll region is copied to a third buffer, the scroll region is filled, then the original contents of the scroll region are copied to the destination. +// Arguments: +// - screenInfo - reference to screen buffer info. +// - scrollRectGiven - Region to copy/move (source and size) +// - clipRectGiven - Optional clip region to contain buffer change effects +// - destinationOriginGiven - Upper left corner of target region. +// - fillCharGiven - Character to fill source region with. +// - fillAttrsGiven - Attribute to fill source region with. +// NOTE: Throws exceptions +void ScrollRegion(SCREEN_INFORMATION& screenInfo, + const SMALL_RECT scrollRectGiven, + const std::optional clipRectGiven, + const COORD destinationOriginGiven, + const wchar_t fillCharGiven, + const TextAttribute fillAttrsGiven) +{ + // ------ 1. PREP SOURCE ------ + // Set up the source viewport. + auto source = Viewport::FromInclusive(scrollRectGiven); + const auto originalSourceOrigin = source.Origin(); + + // Alright, let's make sure that our source fits inside the buffer. + const auto buffer = screenInfo.GetBufferSize(); + source = Viewport::Intersect(source, buffer); + + // If the source is no longer valid, then there's nowhere we can copy from + // and also nowhere we can fill. We're done. Return early. + if (!source.IsValid()) + { + return; + } + + // ------ 2. PREP CLIP ------ + // Now figure out our clipping area. If we have clipping specified, it will limit + // the area that can be affected (targeted or filling) throughout this operation. + // If there was no clip rect, we'll clip to the entire buffer size. + auto clip = Viewport::FromInclusive(clipRectGiven.value_or(buffer.ToInclusive())); + + // Account for the scroll margins set by DECSTBM + // DECSTBM command can sometimes apply a clipping behavior as well. Check if we have any + // margins defined by DECSTBM and further restrict the clipping area here. + if (screenInfo.AreMarginsSet()) + { + const auto margin = screenInfo.GetScrollingRegion(); + + // Update the clip rectangle to only include the area that is also in the margin. + clip = Viewport::Intersect(clip, margin); + + // We'll also need to update the source rectangle, but we need to do that later. + } + + // OK, make sure that the clip rectangle also fits inside the buffer + clip = Viewport::Intersect(buffer, clip); + + // ------ 3. PREP FILL ------ + // Then think about fill. We will fill in any area of the source that we copied from + // with the fill character as long as it falls inside the clip region (the area + // that is allowed to be affected). + auto fill = Viewport::Intersect(clip, source); + + // If fill is no longer valid, then there is no area that we're allowed to write to + // within the buffer. So we can just exit early. + if (!fill.IsValid()) + { + return; + } + + // Determine the cell we will use to fill in any revealed/uncovered space. + // We generally use exactly what was given to us. + OutputCellIterator fillData(fillCharGiven, fillAttrsGiven); + + // However, if the character is null and we were given a null attribute (represented as legacy 0), + // then we'll just fill with spaces and whatever the buffer's default colors are. + if (fillCharGiven == UNICODE_NULL && fillAttrsGiven.IsLegacy() && fillAttrsGiven.GetLegacyAttributes() == 0) + { + fillData = OutputCellIterator(UNICODE_SPACE, screenInfo.GetAttributes()); + } + + // ------ 4. PREP TARGET ------ + // Now it's time to think about the target. We're only given the origin of the target + // because it is assumed that it will have the same relative dimensions as the original source. + auto targetOrigin = destinationOriginGiven; + + // However, if we got to this point, we may have clipped the source because some part of it + // fell outside of the buffer. + // Apply any delta between the original source rectangle's origin and its current position to + // the target origin. + { + auto currentSourceOrigin = source.Origin(); + targetOrigin.X += currentSourceOrigin.X - originalSourceOrigin.X; + targetOrigin.Y += currentSourceOrigin.Y - originalSourceOrigin.Y; + } + + // See MSFT:20204600 - Update the source rectangle to only include the region + // inside the scroll margins. We need to do this AFTER we calculate the + // delta between the currentSourceOrigin and the originalSourceOrigin. + // Don't combine this with the above block, because if there are margins set + // and the source rectangle was clipped by the buffer, we still want to + // adjust the target origin point based on the clipping of the buffer. + if (screenInfo.AreMarginsSet()) + { + const auto margin = screenInfo.GetScrollingRegion(); + source = Viewport::Intersect(source, margin); + } + + // And now the target viewport is the same size as the source viewport but at the different position. + auto target = Viewport::FromDimensions(targetOrigin, source.Dimensions()); + + // However, this might mean that the target is falling outside of the region we're allowed to edit + // (the clip area). So we need to reduce the target to only inside the clip. + // But backup the original target origin first, because we need to know how it has changed. + const auto originalTargetOrigin = target.Origin(); + target = Viewport::Intersect(clip, target); + + // OK, if the target became smaller than before, we need to also adjust the source accordingly + // so we don't waste time loading up/copying things that have no place to go within the target. + { + const auto currentTargetOrigin = target.Origin(); + auto sourceOrigin = source.Origin(); + sourceOrigin.X += currentTargetOrigin.X - originalTargetOrigin.X; + sourceOrigin.Y += currentTargetOrigin.Y - originalTargetOrigin.Y; + + source = Viewport::FromDimensions(sourceOrigin, target.Dimensions()); + } + + // ------ 5. COPY ------ + // If the target region is valid, let's do this. + if (target.IsValid()) + { + // Perform the copy from the source to the target. + _CopyRectangle(screenInfo, source, target.Origin()); + + // Notify the renderer and accessibility as to what moved and where. + _ScrollScreen(screenInfo, source, fill, target); + } + + // ------ 6. FILL ------ + // Now fill in anything that wasn't already touched by the copy above. + // Fill as a single viewport represents the entire region we were allowed to + // write into. But since we already copied, filling the whole thing might + // overwrite what we just placed at the target. + // So use the special subtraction function to get the viewports that fall + // within the fill area but outside of the target area. + const auto remaining = Viewport::Subtract(fill, target); + + // Apply the fill data to each of the viewports we're given here. + for (size_t i = 0; i < remaining.size(); i++) + { + const auto& view = remaining.at(i); + screenInfo.WriteRect(fillData, view); + } +} + +void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.pCurrentScreenBuffer = &screenInfo; + + // initialize cursor + screenInfo.GetTextBuffer().GetCursor().SetIsOn(false); + + // set font + screenInfo.RefreshFontWithRenderer(); + + // Empty input buffer. + gci.pInputBuffer->FlushAllButKeys(); + + // Set window size. + screenInfo.PostUpdateWindowSize(); + + gci.ConsoleIme.RefreshAreaAttributes(); + + // Write data to screen. + WriteToScreen(screenInfo, screenInfo.GetViewport()); +} + +// TODO: MSFT 9450717 This should join the ProcessList class when CtrlEvents become moved into the server. https://osgvsowi/9450717 +void CloseConsoleProcessState() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // If there are no connected processes, sending control events is pointless as there's no one do send them to. In + // this case we'll just exit conhost. + + // N.B. We can get into this state when a process has a reference to the console but hasn't connected. For example, + // when it's created suspended and never resumed. + if (gci.ProcessHandleList.IsEmpty()) + { + ServiceLocator::RundownAndExit(STATUS_SUCCESS); + } + + HandleCtrlEvent(CTRL_CLOSE_EVENT); + + // Jiggle the handle: (see MSFT:19419231) + // When we call this function, we'll only actually close the console once + // we're totally unlocked. If our caller has the console locked, great, + // we'll displatch the ctrl event once they unlock. However, if they're + // not running under lock (eg PtySignalInputThread::_GetData), then the + // ctrl event will never actually get dispatched. + // So, lock and unlock here, to make sure the ctrl event gets handled. + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); +} diff --git a/src/host/output.h b/src/host/output.h new file mode 100644 index 000000000..feea70685 --- /dev/null +++ b/src/host/output.h @@ -0,0 +1,54 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- output.h + +Abstract: +- This module contains the internal structures and definitions used + by the output (screen) component of the NT console subsystem. + +Author: +- Therese Stowell (ThereseS) 12-Nov-1990 + +Revision History: +--*/ + +#pragma once + +#include "screenInfo.hpp" +#include "server.h" +#include "../buffer/out/OutputCell.hpp" +#include "../buffer/out/OutputCellRect.hpp" + +void ScreenBufferSizeChange(const COORD coordNewSize); + +[[nodiscard]] +NTSTATUS DoCreateScreenBuffer(); + +std::vector ReadOutputAttributes(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead); + +std::wstring ReadOutputStringW(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead); + +std::string ReadOutputStringA(const SCREEN_INFORMATION& screenInfo, + const COORD coordRead, + const size_t amountToRead); + +void ScrollRegion(SCREEN_INFORMATION& screenInfo, + const SMALL_RECT scrollRect, + const std::optional clipRect, + const COORD destinationOrigin, + const wchar_t fillCharGiven, + const TextAttribute fillAttrsGiven); + +VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pProcessData); + +bool 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 new file mode 100644 index 000000000..1ddf0fc48 --- /dev/null +++ b/src/host/outputStream.cpp @@ -0,0 +1,763 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "outputStream.hpp" + +#include "_stream.h" +#include "getset.h" +#include "directio.h" + +#include "../interactivity/inc/ServiceLocator.hpp" + +#pragma hdrstop +using namespace Microsoft::Console; + +WriteBuffer::WriteBuffer(_In_ Microsoft::Console::IIoProvider& io) : + _io{ io }, + _ntstatus{ STATUS_INVALID_DEVICE_STATE } +{ +} + +// Routine Description: +// - Handles the print action from the state machine +// Arguments: +// - wch - The character to be printed. +// Return Value: +// - +void WriteBuffer::Print(const wchar_t wch) +{ + _DefaultCase(wch); +} + +// Routine Description: +// - Handles the print action from the state machine +// Arguments: +// - wch - The character to be printed. +// Return Value: +// - +void WriteBuffer::PrintString(const wchar_t* const rgwch, const size_t cch) +{ + _DefaultStringCase(rgwch, cch); +} + +// Routine Description: +// - Handles the execute action from the state machine +// Arguments: +// - wch - The C0 control character to be executed. +// Return Value: +// - +void WriteBuffer::Execute(const wchar_t wch) +{ + _DefaultCase(wch); +} + +// Routine Description: +// - Default text editing/printing handler for all characters that were not routed elsewhere by other state machine intercepts. +// Arguments: +// - wch - The character to be processed by our default text editing/printing mechanisms. +// Return Value: +// - +void WriteBuffer::_DefaultCase(const wchar_t wch) +{ + _DefaultStringCase(const_cast(&wch), 1); // WriteCharsLegacy wants mutable chars, so we'll givve it mutable chars. +} + +// Routine Description: +// - Default text editing/printing handler for all characters that were not routed elsewhere by other state machine intercepts. +// Arguments: +// - wch - The character to be processed by our default text editing/printing mechanisms. +// Return Value: +// - +void WriteBuffer::_DefaultStringCase(_In_reads_(cch) const wchar_t* const rgwch, const size_t cch) +{ + size_t dwNumBytes = cch * sizeof(wchar_t); + + _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsOn(true); + + _ntstatus = WriteCharsLegacy(_io.GetActiveOutputBuffer(), + rgwch, + rgwch, + rgwch, + &dwNumBytes, + nullptr, + _io.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition().X, + WC_LIMIT_BACKSPACE | WC_NONDESTRUCTIVE_TAB | WC_DELAY_EOL_WRAP, + nullptr); +} + +ConhostInternalGetSet::ConhostInternalGetSet(_In_ IIoProvider& io) : + _io{ io } +{ +} + + +// Routine Description: +// - Connects the GetConsoleScreenBufferInfoEx API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - pConsoleScreenBufferInfoEx - Pointer to structure to hold screen buffer information like the public API call. +// Return Value: +// - TRUE if successful (see DoSrvGetConsoleScreenBufferInfo). FALSE otherwise. +BOOL ConhostInternalGetSet::GetConsoleScreenBufferInfoEx(_Out_ CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) const +{ + ServiceLocator::LocateGlobals().api.GetConsoleScreenBufferInfoExImpl(_io.GetActiveOutputBuffer(), *pConsoleScreenBufferInfoEx); + return TRUE; +} + +// Routine Description: +// - Connects the SetConsoleScreenBufferInfoEx API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - pConsoleScreenBufferInfoEx - Pointer to structure containing screen buffer information like the public API call. +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleScreenBufferInfo). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleScreenBufferInfoExImpl(_io.GetActiveOutputBuffer(), *pConsoleScreenBufferInfoEx)); +} + +// Routine Description: +// - Connects the SetConsoleCursorPosition API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - coordCursorPosition - new cursor position to set like the public API call. +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleCursorPosition). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleCursorPosition(const COORD coordCursorPosition) +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorPositionImpl(_io.GetActiveOutputBuffer(), coordCursorPosition)); +} + +// Routine Description: +// - Connects the GetConsoleCursorInfo API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - pConsoleCursorInfo - Pointer to structure to receive console cursor rendering info +// Return Value: +// - TRUE if successful (see DoSrvGetConsoleCursorInfo). FALSE otherwise. +BOOL ConhostInternalGetSet::GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const +{ + bool bVisible; + DWORD dwSize; + + ServiceLocator::LocateGlobals().api.GetConsoleCursorInfoImpl(_io.GetActiveOutputBuffer(), dwSize, bVisible); + pConsoleCursorInfo->bVisible = bVisible; + pConsoleCursorInfo->dwSize = dwSize; + return TRUE; +} + +// Routine Description: +// - Connects the SetConsoleCursorInfo API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - pConsoleCursorInfo - Updated size/visibility information to modify the cursor rendering behavior. +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleCursorInfo). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) +{ + const bool visible = !!pConsoleCursorInfo->bVisible; + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorInfoImpl(_io.GetActiveOutputBuffer(), pConsoleCursorInfo->dwSize, visible)); +} + +// Routine Description: +// - Connects the FillConsoleOutputCharacter API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - wch - Character to use for filling the buffer +// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length) +// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there) +// - numberOfCharsWritten - Pointer to memory location to hold the total number of characters written into the buffer +// Return Value: +// - TRUE if successful (see FillConsoleOutputCharacterWImpl). FALSE otherwise. +BOOL ConhostInternalGetSet::FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputCharacterWImpl(_io.GetActiveOutputBuffer(), + wch, + nLength, + dwWriteCoord, + numberOfCharsWritten)); +} + +// Routine Description: +// - Connects the FillConsoleOutputAttribute API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - wAttribute - Text attribute (colors/font style) for filling the buffer +// - nLength - The length of the fill run in characters (depending on mode, will wrap at the window edge so multiple lines are the sum of the total length) +// - dwWriteCoord - The first fill character's coordinate position in the buffer (writes continue rightward and possibly down from there) +// - numberOfCharsWritten - Pointer to memory location to hold the total number of text attributes written into the buffer +// Return Value: +// - TRUE if successful (see FillConsoleOutputAttributeImpl). FALSE otherwise. +BOOL ConhostInternalGetSet::FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.FillConsoleOutputAttributeImpl(_io.GetActiveOutputBuffer(), + wAttribute, + nLength, + dwWriteCoord, + numberOfAttrsWritten)); +} + +// Routine Description: +// - Connects the SetConsoleTextAttribute API call directly into our Driver Message servicing call inside Conhost.exe +// Sets BOTH the FG and the BG component of the attributes. +// Arguments: +// - wAttr - new color/graphical attributes to apply as default within the console text buffer +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleTextAttribute). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleTextAttribute(const WORD wAttr) +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleTextAttributeImpl(_io.GetActiveOutputBuffer(), wAttr)); +} + +// Routine Description: +// - Connects the PrivateSetDefaultAttributes API call directly into our Driver Message servicing call inside Conhost.exe +// Sets the FG and/or BG to the Default attributes values. +// Arguments: +// - fForeground - Set the foreground to the default attributes +// - fBackground - Set the background to the default attributes +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetDefaultAttributes). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetDefaultAttributes(const bool fForeground, + const bool fBackground) +{ + DoSrvPrivateSetDefaultAttributes(_io.GetActiveOutputBuffer(), fForeground, fBackground); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateSetLegacyAttributes API call directly into our Driver Message servicing call inside Conhost.exe +// Sets only the components of the attributes requested with the fForeground, fBackground, and fMeta flags. +// Arguments: +// - wAttr - new color/graphical attributes to apply as default within the console text buffer +// - fForeground - The new attributes contain an update to the foreground attributes +// - fBackground - The new attributes contain an update to the background attributes +// - fMeta - The new attributes contain an update to the meta attributes +// Return Value: +// - TRUE if successful (see DoSrvVtSetLegacyAttributes). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetLegacyAttributes(const WORD wAttr, + const bool fForeground, + const bool fBackground, + const bool fMeta) +{ + DoSrvPrivateSetLegacyAttributes(_io.GetActiveOutputBuffer(), wAttr, fForeground, fBackground, fMeta); + return TRUE; +} + +// Routine Description: +// - Sets the current attributes of the screen buffer to use the color table entry specified by +// the iXtermTableEntry. Sets either the FG or the BG component of the attributes. +// Arguments: +// - iXtermTableEntry - The entry of the xterm table to use. +// - fIsForeground - Whether or not the color applies to the foreground. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetConsoleXtermTextAttribute). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleXtermTextAttribute(const int iXtermTableEntry, const bool fIsForeground) +{ + DoSrvPrivateSetConsoleXtermTextAttribute(_io.GetActiveOutputBuffer(), iXtermTableEntry, fIsForeground); + return TRUE; +} + +// Routine Description: +// - Sets the current attributes of the screen buffer to use the given rgb color. +// Sets either the FG or the BG component of the attributes. +// Arguments: +// - rgbColor - The rgb color to use. +// - fIsForeground - Whether or not the color applies to the foreground. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetConsoleRGBTextAttribute). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleRGBTextAttribute(const COLORREF rgbColor, const bool fIsForeground) +{ + DoSrvPrivateSetConsoleRGBTextAttribute(_io.GetActiveOutputBuffer(), rgbColor, fIsForeground); + return TRUE; +} + +BOOL ConhostInternalGetSet::PrivateBoldText(const bool bolded) +{ + DoSrvPrivateBoldText(_io.GetActiveOutputBuffer(), bolded); + return TRUE; +} + +// Routine Description: +// - Connects the WriteConsoleInput API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - events - the input events to be copied into the head of the input +// buffer for the underlying attached process +// - eventsWritten - on output, the number of events written +// Return Value: +// - TRUE if successful (see DoSrvWriteConsoleInput). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateWriteConsoleInputW(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) +{ + eventsWritten = 0; + + return SUCCEEDED(DoSrvPrivateWriteConsoleInputW(_io.GetActiveInputBuffer(), + events, + eventsWritten, + true)); // append +} + +// Routine Description: +// - Connects the ScrollConsoleScreenBuffer API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - pScrollRectangle - The region to "cut" from the existing buffer text +// - pClipRectangle - The bounding rectangle within which all modifications should happen. Any modification outside this RECT should be clipped. +// - coordDestinationOrigin - The top left corner of the "paste" from pScrollREctangle +// - pFill - The text/attribute pair to fill all remaining space behind after the "cut" operation (bounded by clip, of course.) +// Return Value: +// - TRUE if successful (see DoSrvScrollConsoleScreenBuffer). FALSE otherwise. +BOOL ConhostInternalGetSet::ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, + _In_opt_ const SMALL_RECT* pClipRectangle, + _In_ COORD coordDestinationOrigin, + const CHAR_INFO* pFill) +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.ScrollConsoleScreenBufferWImpl(_io.GetActiveOutputBuffer(), + *pScrollRectangle, + coordDestinationOrigin, + pClipRectangle != nullptr ? std::optional(*pClipRectangle) : std::nullopt, + pFill->Char.UnicodeChar, + pFill->Attributes)); +} + +// Routine Description: +// - Connects the SetConsoleWindowInfo API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - bAbsolute - Should the window be moved to an absolute position? If false, the movement is relative to the current pos. +// - lpConsoleWindow - Info about how to move the viewport +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleWindowInfo). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleWindowInfo(const BOOL bAbsolute, const SMALL_RECT* const lpConsoleWindow) +{ + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleWindowInfoImpl(_io.GetActiveOutputBuffer(), !!bAbsolute, *lpConsoleWindow)); +} + +// Routine Description: +// - Connects the PrivateSetCursorKeysMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateSetCursorKeysMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fApplicationMode - set to true to enable Application Mode Input, false for Normal Mode. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetCursorKeysMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetCursorKeysMode(const bool fApplicationMode) +{ + return NT_SUCCESS(DoSrvPrivateSetCursorKeysMode(fApplicationMode)); +} + +// Routine Description: +// - Connects the PrivateSetKeypadMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateSetKeypadMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fApplicationMode - set to true to enable Application Mode Input, false for Numeric Mode. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetKeypadMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetKeypadMode(const bool fApplicationMode) +{ + return NT_SUCCESS(DoSrvPrivateSetKeypadMode(fApplicationMode)); +} + +// Routine Description: +// - Connects the PrivateShowCursor call directly into our Driver Message servicing call inside Conhost.exe +// PrivateShowCursor is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - show - set to true to make the cursor visible, false to hide. +// Return Value: +// - TRUE if successful (see DoSrvPrivateShowCursor). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateShowCursor(const bool show) noexcept +{ + DoSrvPrivateShowCursor(_io.GetActiveOutputBuffer(), show); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateAllowCursorBlinking call directly into our Driver Message servicing call inside Conhost.exe +// PrivateAllowCursorBlinking is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnable - set to true to enable blinking, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateAllowCursorBlinking). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateAllowCursorBlinking(const bool fEnable) +{ + DoSrvPrivateAllowCursorBlinking(_io.GetActiveOutputBuffer(), fEnable); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateSetScrollingRegion call directly into our Driver Message servicing call inside Conhost.exe +// PrivateSetScrollingRegion is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - psrScrollMargins - The bounds of the region to be the scrolling region of the viewport. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetScrollingRegion). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetScrollingRegion(const SMALL_RECT* const psrScrollMargins) +{ + return NT_SUCCESS(DoSrvPrivateSetScrollingRegion(_io.GetActiveOutputBuffer(), psrScrollMargins)); +} + +// Routine Description: +// - Connects the PrivateReverseLineFeed call directly into our Driver Message servicing call inside Conhost.exe +// PrivateReverseLineFeed is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Return Value: +// - TRUE if successful (see DoSrvPrivateReverseLineFeed). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateReverseLineFeed() +{ + return NT_SUCCESS(DoSrvPrivateReverseLineFeed(_io.GetActiveOutputBuffer())); +} + +// Routine Description: +// - Connects the MoveCursorVertically call directly into our Driver Message servicing call inside Conhost.exe +// MoveCursorVertically is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Return Value: +// - TRUE if successful (see DoSrvMoveCursorVertically). FALSE otherwise. +BOOL ConhostInternalGetSet::MoveCursorVertically(const short lines) +{ + return SUCCEEDED(DoSrvMoveCursorVertically(_io.GetActiveOutputBuffer(), lines)); +} + +// Routine Description: +// - Connects the SetConsoleTitleW API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - title - The null-terminated string to set as the window title +// Return Value: +// - TRUE if successful (see DoSrvSetConsoleTitle). FALSE otherwise. +BOOL ConhostInternalGetSet::SetConsoleTitleW(std::wstring_view title) +{ + return SUCCEEDED(DoSrvSetConsoleTitleW(title)); +} + +// Routine Description: +// - Connects the PrivateUseAlternateScreenBuffer call directly into our Driver Message servicing call inside Conhost.exe +// PrivateUseAlternateScreenBuffer is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Return Value: +// - TRUE if successful (see DoSrvPrivateUseAlternateScreenBuffer). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateUseAlternateScreenBuffer() +{ + return NT_SUCCESS(DoSrvPrivateUseAlternateScreenBuffer(_io.GetActiveOutputBuffer())); +} + +// Routine Description: +// - Connects the PrivateUseMainScreenBuffer call directly into our Driver Message servicing call inside Conhost.exe +// PrivateUseMainScreenBuffer is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Return Value: +// - TRUE if successful (see DoSrvPrivateUseMainScreenBuffer). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateUseMainScreenBuffer() +{ + DoSrvPrivateUseMainScreenBuffer(_io.GetActiveOutputBuffer()); + return TRUE; +} + +// - Connects the PrivateHorizontalTabSet call directly into our Driver Message servicing call inside Conhost.exe +// PrivateHorizontalTabSet is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// +// Return Value: +// - TRUE if successful (see PrivateHorizontalTabSet). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateHorizontalTabSet() +{ + return NT_SUCCESS(DoSrvPrivateHorizontalTabSet()); +} + +// Routine Description: +// - Connects the PrivateForwardTab call directly into our Driver Message servicing call inside Conhost.exe +// PrivateForwardTab is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - sNumTabs - the number of tabs to execute +// Return Value: +// - TRUE if successful (see PrivateForwardTab). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateForwardTab(const SHORT sNumTabs) +{ + return NT_SUCCESS(DoSrvPrivateForwardTab(sNumTabs)); +} + +// Routine Description: +// - Connects the PrivateBackwardsTab call directly into our Driver Message servicing call inside Conhost.exe +// PrivateBackwardsTab is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - sNumTabs - the number of tabs to execute +// Return Value: +// - TRUE if successful (see PrivateBackwardsTab). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateBackwardsTab(const SHORT sNumTabs) +{ + return NT_SUCCESS(DoSrvPrivateBackwardsTab(sNumTabs)); +} + +// Routine Description: +// - Connects the PrivateTabClear call directly into our Driver Message servicing call inside Conhost.exe +// PrivateTabClear is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fClearAll - set to true to enable blinking, false to disable +// Return Value: +// - TRUE if successful (see PrivateTabClear). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateTabClear(const bool fClearAll) +{ + DoSrvPrivateTabClear(fClearAll); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateSetDefaultTabStops call directly into the private api point +// Return Value: +// - TRUE +BOOL ConhostInternalGetSet::PrivateSetDefaultTabStops() +{ + DoSrvPrivateSetDefaultTabStops(); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableVT200MouseMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableVT200MouseMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable vt200 mouse mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableVT200MouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableVT200MouseMode(const bool fEnabled) +{ + DoSrvPrivateEnableVT200MouseMode(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableUTF8ExtendedMouseMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableUTF8ExtendedMouseMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable utf8 extended mouse mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableUTF8ExtendedMouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableUTF8ExtendedMouseMode(const bool fEnabled) +{ + DoSrvPrivateEnableUTF8ExtendedMouseMode(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableSGRExtendedMouseMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableSGRExtendedMouseMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable SGR extended mouse mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableSGRExtendedMouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableSGRExtendedMouseMode(const bool fEnabled) +{ + DoSrvPrivateEnableSGRExtendedMouseMode(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableButtonEventMouseMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableButtonEventMouseMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable button-event mouse mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableButtonEventMouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableButtonEventMouseMode(const bool fEnabled) +{ + DoSrvPrivateEnableButtonEventMouseMode(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableAnyEventMouseMode call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableAnyEventMouseMode is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable any-event mouse mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableAnyEventMouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableAnyEventMouseMode(const bool fEnabled) +{ + DoSrvPrivateEnableAnyEventMouseMode(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEnableAlternateScroll call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEnableAlternateScroll is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on out public API surface. +// Arguments: +// - fEnabled - set to true to enable alternate scroll mode, false to disable +// Return Value: +// - TRUE if successful (see DoSrvPrivateEnableAnyEventMouseMode). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEnableAlternateScroll(const bool fEnabled) +{ + DoSrvPrivateEnableAlternateScroll(fEnabled); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateEraseAll call directly into our Driver Message servicing call inside Conhost.exe +// PrivateEraseAll is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on our public API surface. +// Arguments: +// +// Return Value: +// - TRUE if successful (see DoSrvPrivateEraseAll). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateEraseAll() +{ + return NT_SUCCESS(DoSrvPrivateEraseAll(_io.GetActiveOutputBuffer())); +} + +// Routine Description: +// - Connects the SetCursorStyle call directly into our Driver Message servicing call inside Conhost.exe +// SetCursorStyle is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on our public API surface. +// Arguments: +// - cursorType: The style of cursor to change the cursor to. +// Return Value: +// - TRUE if successful (see DoSrvSetCursorStyle). FALSE otherwise. +BOOL ConhostInternalGetSet::SetCursorStyle(const CursorType cursorType) +{ + DoSrvSetCursorStyle(_io.GetActiveOutputBuffer(), cursorType); + return TRUE; +} + +// Routine Description: +// - Retrieves the default color attributes information for the active screen buffer. +// - This function is used to optimize SGR calls in lieu of calling GetConsoleScreenBufferInfoEx. +// Arguments: +// - pwAttributes - Pointer to space to receive color attributes data +// Return Value: +// - TRUE if successful. FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateGetConsoleScreenBufferAttributes(_Out_ WORD* const pwAttributes) +{ + return NT_SUCCESS(DoSrvPrivateGetConsoleScreenBufferAttributes(_io.GetActiveOutputBuffer(), pwAttributes)); +} + +// Routine Description: +// - Connects the PrivatePrependConsoleInput API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - events - the input events to be copied into the head of the input +// buffer for the underlying attached process +// - eventsWritten - on output, the number of events written +// Return Value: +// - TRUE if successful (see DoSrvPrivatePrependConsoleInput). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivatePrependConsoleInput(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) +{ + return SUCCEEDED(DoSrvPrivatePrependConsoleInput(_io.GetActiveInputBuffer(), + events, + eventsWritten)); +} + +// Routine Description: +// - Connects the PrivatePrependConsoleInput API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - +// Return Value: +// - TRUE if successful (see DoSrvPrivateRefreshWindow). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateRefreshWindow() +{ + DoSrvPrivateRefreshWindow(_io.GetActiveOutputBuffer()); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateWriteConsoleControlInput API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - key - a KeyEvent representing a special type of keypress, typically Ctrl-C +// Return Value: +// - TRUE if successful (see DoSrvPrivateWriteConsoleControlInput). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateWriteConsoleControlInput(_In_ KeyEvent key) +{ + return SUCCEEDED(DoSrvPrivateWriteConsoleControlInput(_io.GetActiveInputBuffer(), + key)); +} + +// Routine Description: +// - Connects the GetConsoleOutputCP API call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - puiOutputCP - recieves the outputCP of the console. +// Return Value: +// - TRUE if successful (see DoSrvPrivateWriteConsoleControlInput). FALSE otherwise. +BOOL ConhostInternalGetSet::GetConsoleOutputCP(_Out_ unsigned int* const puiOutputCP) +{ + DoSrvGetConsoleOutputCodePage(puiOutputCP); + return TRUE; +} + +// Routine Description: +// - Connects the PrivateSuppressResizeRepaint API call directly into our Driver +// Message servicing call inside Conhost.exe +// Arguments: +// - +// Return Value: +// - TRUE if successful (see DoSrvPrivateSuppressResizeRepaint). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSuppressResizeRepaint() +{ + return SUCCEEDED(DoSrvPrivateSuppressResizeRepaint()); +} + +// Routine Description: +// - Connects the SetCursorStyle call directly into our Driver Message servicing call inside Conhost.exe +// SetCursorStyle is an internal-only "API" call that the vt commands can execute, +// but it is not represented as a function call on our public API surface. +// Arguments: +// - cursorColor: The color to change the cursor to. INVALID_COLOR will revert +// it to the legacy inverting behavior. +// Return Value: +// - TRUE if successful (see DoSrvSetCursorStyle). FALSE otherwise. +BOOL ConhostInternalGetSet::SetCursorColor(const COLORREF cursorColor) +{ + DoSrvSetCursorColor(_io.GetActiveOutputBuffer(), cursorColor); + return TRUE; +} + +// Routine Description: +// - Connects the IsConsolePty call directly into our Driver Message servicing call inside Conhost.exe +// Arguments: +// - isPty: recieves the bool indicating whether or not we're in pty mode. +// Return Value: +// - TRUE if successful (see DoSrvIsConsolePty). FALSE otherwise. +BOOL ConhostInternalGetSet::IsConsolePty(_Out_ bool* const pIsPty) const +{ + DoSrvIsConsolePty(pIsPty); + return TRUE; +} + +BOOL ConhostInternalGetSet::DeleteLines(const unsigned int count) +{ + DoSrvPrivateDeleteLines(count); + return TRUE; +} + +BOOL ConhostInternalGetSet::InsertLines(const unsigned int count) +{ + DoSrvPrivateInsertLines(count); + return TRUE; +} + +// Method Description: +// - Connects the MoveToBottom call directly into our Driver Message servicing +// call inside Conhost.exe +// Arguments: +// +// Return Value: +// - TRUE if successful (see DoSrvPrivateMoveToBottom). FALSE otherwise. +BOOL ConhostInternalGetSet::MoveToBottom() const +{ + DoSrvPrivateMoveToBottom(_io.GetActiveOutputBuffer()); + return TRUE; +} + +// Method Description: +// - Connects the PrivateSetColorTableEntry call directly into our Driver Message servicing +// call inside Conhost.exe +// Arguments: +// - index: the index in the table to change. +// - value: the new RGB value to use for that index in the color table. +// Return Value: +// - TRUE if successful (see DoSrvPrivateSetColorTableEntry). FALSE otherwise. +BOOL ConhostInternalGetSet::PrivateSetColorTableEntry(const short index, const COLORREF value) const noexcept +{ + return SUCCEEDED(DoSrvPrivateSetColorTableEntry(index, value)); +} diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp new file mode 100644 index 000000000..b450edced --- /dev/null +++ b/src/host/outputStream.hpp @@ -0,0 +1,162 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- outputStream.hpp + +Abstract: +- Classes to process text written into the console on the attached application's output stream (usually STDOUT). + +Author: +- Michael Niksa July 27 2015 +--*/ + +#pragma once + +#include "..\terminal\adapter\adaptDefaults.hpp" +#include "..\types\inc\IInputEvent.hpp" +#include "..\inc\conattrs.hpp" +#include "IIoProvider.hpp" + +class SCREEN_INFORMATION; + +// The WriteBuffer class provides helpers for writing text into the TextBuffer that is backing a particular console screen buffer. +class WriteBuffer : public Microsoft::Console::VirtualTerminal::AdaptDefaults +{ +public: + WriteBuffer(_In_ Microsoft::Console::IIoProvider& io); + + // Implement Adapter callbacks for default cases (non-escape sequences) + void Print(const wchar_t wch) override; + void PrintString(const wchar_t* const rgwch, const size_t cch) override; + void Execute(const wchar_t wch) override; + + [[nodiscard]] + NTSTATUS GetResult() { return _ntstatus; }; + +private: + void _DefaultCase(const wchar_t wch); + void _DefaultStringCase(_In_reads_(cch) const wchar_t* const rgwch, const size_t cch); + + Microsoft::Console::IIoProvider& _io; + NTSTATUS _ntstatus; +}; + +#include "..\terminal\adapter\conGetSet.hpp" + + +// The ConhostInternalGetSet is for the Conhost process to call the entrypoints for its own Get/Set APIs. +// Normally, these APIs are accessible from the outside of the conhost process (like by the process being "hosted") through +// the kernelbase/32 exposed public APIs and routed by the console driver (condrv) to this console host. +// But since we're trying to call them from *inside* the console host itself, we need to get in the way and route them straight to the +// v-table inside this process instance. +class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::ConGetSet +{ +public: + ConhostInternalGetSet(_In_ Microsoft::Console::IIoProvider& io); + + BOOL GetConsoleScreenBufferInfoEx(_Out_ CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) const override; + BOOL SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) override; + + BOOL SetConsoleCursorPosition(const COORD coordCursorPosition) override; + + BOOL GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const override; + BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) override; + + BOOL FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, + const COORD dwWriteCoord, + size_t& numberOfCharsWritten) noexcept override; + BOOL FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, + const COORD dwWriteCoord, + size_t& numberOfAttrsWritten) noexcept override; + + BOOL SetConsoleTextAttribute(const WORD wAttr) override; + + BOOL PrivateSetLegacyAttributes(const WORD wAttr, + const bool fForeground, + const bool fBackground, + const bool fMeta) override; + + BOOL PrivateSetDefaultAttributes(const bool fForeground, + const bool fBackground) override; + + BOOL SetConsoleXtermTextAttribute(const int iXtermTableEntry, + const bool fIsForeground) override; + + BOOL SetConsoleRGBTextAttribute(const COLORREF rgbColor, + const bool fIsForeground) override; + + BOOL PrivateBoldText(const bool bolded) override; + + BOOL PrivateWriteConsoleInputW(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) override; + + BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, + _In_opt_ const SMALL_RECT* pClipRectangle, + _In_ COORD coordDestinationOrigin, + const CHAR_INFO* pFill) override; + + BOOL SetConsoleWindowInfo(BOOL const bAbsolute, + const SMALL_RECT* const lpConsoleWindow) override; + + BOOL PrivateSetCursorKeysMode(const bool fApplicationMode) override; + BOOL PrivateSetKeypadMode(const bool fApplicationMode) override; + + BOOL PrivateShowCursor(const bool show) noexcept override; + BOOL PrivateAllowCursorBlinking(const bool fEnable) override; + + BOOL PrivateSetScrollingRegion(const SMALL_RECT* const srScrollMargins) override; + + BOOL PrivateReverseLineFeed() override; + + BOOL MoveCursorVertically(const short lines) override; + + BOOL SetConsoleTitleW(const std::wstring_view title) override; + + BOOL PrivateUseAlternateScreenBuffer() override; + + BOOL PrivateUseMainScreenBuffer() override; + + BOOL PrivateHorizontalTabSet(); + BOOL PrivateForwardTab(const SHORT sNumTabs) override; + BOOL PrivateBackwardsTab(const SHORT sNumTabs) override; + BOOL PrivateTabClear(const bool fClearAll) override; + BOOL PrivateSetDefaultTabStops() override; + + BOOL PrivateEnableVT200MouseMode(const bool fEnabled) override; + BOOL PrivateEnableUTF8ExtendedMouseMode(const bool fEnabled) override; + BOOL PrivateEnableSGRExtendedMouseMode(const bool fEnabled) override; + BOOL PrivateEnableButtonEventMouseMode(const bool fEnabled) override; + BOOL PrivateEnableAnyEventMouseMode(const bool fEnabled) override; + BOOL PrivateEnableAlternateScroll(const bool fEnabled) override; + BOOL PrivateEraseAll() override; + + BOOL PrivateGetConsoleScreenBufferAttributes(_Out_ WORD* const pwAttributes) override; + + BOOL PrivatePrependConsoleInput(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) override; + + BOOL SetCursorStyle(CursorType const cursorType) override; + BOOL SetCursorColor(COLORREF const cursorColor) override; + + BOOL PrivateRefreshWindow() override; + + BOOL PrivateSuppressResizeRepaint() override; + + BOOL PrivateWriteConsoleControlInput(_In_ KeyEvent key) override; + + BOOL GetConsoleOutputCP(_Out_ unsigned int* const puiOutputCP) override; + + BOOL IsConsolePty(_Out_ bool* const pIsPty) const override; + + BOOL DeleteLines(const unsigned int count) override; + BOOL InsertLines(const unsigned int count) override; + + BOOL MoveToBottom() const override; + + BOOL PrivateSetColorTableEntry(const short index, const COLORREF value) const noexcept override; + +private: + Microsoft::Console::IIoProvider& _io; +}; diff --git a/src/host/popup.cpp b/src/host/popup.cpp new file mode 100644 index 000000000..c4aad7d73 --- /dev/null +++ b/src/host/popup.cpp @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "popup.h" + +#include "_output.h" +#include "output.h" + +#include "dbcs.h" +#include "srvinit.h" +#include "stream.h" + +#include "resource.h" + +#include "utils.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Creates an object representing an interactive popup overlay during cooked mode command line editing. +// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) +// Arguments: +// - screenInfo - Reference to screen on which the popup should be drawn/overlayed. +// - proposedSize - Suggested size of the popup. May be adjusted based on screen size. +Popup::Popup(SCREEN_INFORMATION& screenInfo, const COORD proposedSize) : + _screenInfo(screenInfo), + _userInputFunction(&Popup::_getUserInputInternal) +{ + _attributes = screenInfo.GetPopupAttributes()->GetLegacyAttributes(); + + const COORD size = _CalculateSize(screenInfo, proposedSize); + const COORD origin = _CalculateOrigin(screenInfo, size); + + _region.Left = origin.X; + _region.Top = origin.Y; + _region.Right = gsl::narrow(origin.X + size.X - 1i16); + _region.Bottom = gsl::narrow(origin.Y + size.Y - 1i16); + + _oldScreenSize = screenInfo.GetBufferSize().Dimensions(); + + SMALL_RECT TargetRect; + TargetRect.Left = 0; + TargetRect.Top = _region.Top; + TargetRect.Right = _oldScreenSize.X - 1; + TargetRect.Bottom = _region.Bottom; + + // copy the data into the backup buffer + _oldContents = std::move(screenInfo.ReadRect(Viewport::FromInclusive(TargetRect))); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto countWas = gci.PopupCount.fetch_add(1ui16); + if (0 == countWas) + { + // If this is the first popup to be shown, stop the cursor from appearing/blinking + screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(true); + } +} + +// Routine Description: +// - Cleans up a popup object +// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) +Popup::~Popup() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto countWas = gci.PopupCount.fetch_sub(1i16); + if (1 == countWas) + { + // Notify we're done showing popups. + gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsPopupShown(false); + } +} + +void Popup::Draw() +{ + _DrawBorder(); + _DrawContent(); +} + +// Routine Description: +// - Draws the outlines of the popup area in the screen buffer +void Popup::_DrawBorder() +{ + // fill attributes of top line + COORD WriteCoord; + WriteCoord.X = _region.Left; + WriteCoord.Y = _region.Top; + _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); + + // draw upper left corner + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[UPPER_LEFT_CORNER], 1), WriteCoord); + + // draw upper bar + WriteCoord.X += 1; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[HORIZONTAL_LINE], Width()), WriteCoord); + + // draw upper right corner + WriteCoord.X = _region.Right; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[UPPER_RIGHT_CORNER], 1), WriteCoord); + + for (SHORT i = 0; i < Height(); i++) + { + WriteCoord.Y += 1; + WriteCoord.X = _region.Left; + + // fill attributes + _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); + + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[VERTICAL_LINE], 1), WriteCoord); + + WriteCoord.X = _region.Right; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[VERTICAL_LINE], 1), WriteCoord); + } + + // Draw bottom line. + // Fill attributes of top line. + WriteCoord.X = _region.Left; + WriteCoord.Y = _region.Bottom; + _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); + + // Draw bottom left corner. + WriteCoord.X = _region.Left; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[BOTTOM_LEFT_CORNER], 1), WriteCoord); + + // Draw lower bar. + WriteCoord.X += 1; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[HORIZONTAL_LINE], Width()), WriteCoord); + + // draw lower right corner + WriteCoord.X = _region.Right; + _screenInfo.Write(OutputCellIterator(_screenInfo.LineChar[BOTTOM_RIGHT_CORNER], 1), WriteCoord); +} + +// Routine Description: +// - Draws prompt information in the popup area to tell the user what to enter. +// Arguments: +// - id - Resource ID for string to display to user +void Popup::_DrawPrompt(const UINT id) +{ + std::wstring text = _LoadString(id); + + // Draw empty popup. + COORD WriteCoord; + WriteCoord.X = _region.Left + 1i16; + WriteCoord.Y = _region.Top + 1i16; + size_t lStringLength = Width(); + for (SHORT i = 0; i < Height(); i++) + { + const OutputCellIterator it(UNICODE_SPACE, _attributes, lStringLength); + const auto done = _screenInfo.Write(it, WriteCoord); + lStringLength = done.GetCellDistance(it); + + WriteCoord.Y += 1; + } + + WriteCoord.X = _region.Left + 1i16; + WriteCoord.Y = _region.Top + 1i16; + + // write prompt to screen + lStringLength = text.size(); + if (lStringLength > (ULONG)Width()) + { + text = text.substr(0, Width()); + } + + size_t used; + LOG_IF_FAILED(ServiceLocator::LocateGlobals().api.WriteConsoleOutputCharacterWImpl(_screenInfo, + text, + WriteCoord, + used)); +} + +// Routine Description: +// - Updates the colors of the backed up information inside this popup. +// - This is useful if user preferences change while a popup is displayed. +// Arguments: +// - newAttr - The new default color for text in the buffer +// - newPopupAttr - The new color for text in popups +// - oldAttr - The previous default color for text in the buffer +// - oldPopupAttr - The previous color for text in popups +void Popup::UpdateStoredColors(const TextAttribute& newAttr, + const TextAttribute& newPopupAttr, + const TextAttribute& oldAttr, + const TextAttribute& oldPopupAttr) +{ + // We also want to find and replace the inversion of the popup colors in case there are highlights + const WORD wOldPopupLegacy = oldPopupAttr.GetLegacyAttributes(); + const WORD wNewPopupLegacy = newPopupAttr.GetLegacyAttributes(); + + const WORD wOldPopupAttrInv = (WORD)(((wOldPopupLegacy << 4) & 0xf0) | ((wOldPopupLegacy >> 4) & 0x0f)); + const WORD wNewPopupAttrInv = (WORD)(((wNewPopupLegacy << 4) & 0xf0) | ((wNewPopupLegacy >> 4) & 0x0f)); + + const TextAttribute oldPopupInv{ wOldPopupAttrInv }; + const TextAttribute newPopupInv{ wNewPopupAttrInv }; + + // Walk through every row in the rectangle + for (size_t i = 0; i < _oldContents.Height(); i++) + { + auto row = _oldContents.GetRow(i); + + // Walk through every cell + for (auto& cell : row) + { + auto& attr = cell.TextAttr(); + + if (attr == oldAttr) + { + attr = newAttr; + } + else if (attr == oldPopupAttr) + { + attr = newPopupAttr; + } + else if (attr == oldPopupInv) + { + attr = newPopupInv; + } + } + } +} + +// Routine Description: +// - Cleans up a popup by restoring the stored buffer information to the region of +// the screen that the popup was covering and frees resources. +void Popup::End() +{ + // restore previous contents to screen + + SMALL_RECT SourceRect; + SourceRect.Left = 0i16; + SourceRect.Top = _region.Top; + SourceRect.Right = _oldScreenSize.X - 1i16; + SourceRect.Bottom = _region.Bottom; + + const auto sourceViewport = Viewport::FromInclusive(SourceRect); + + _screenInfo.WriteRect(_oldContents, sourceViewport.Origin()); +} + +// Routine Description: +// - Helper to calculate the size of the popup. +// Arguments: +// - screenInfo - Screen buffer we will be drawing into +// - proposedSize - The suggested size of the popup that may need to be adjusted to fit +// Return Value: +// - Coordinate size that the popup should consume in the screen buffer +COORD Popup::_CalculateSize(const SCREEN_INFORMATION& screenInfo, const COORD proposedSize) +{ + // determine popup dimensions + COORD size = proposedSize; + size.X += 2; // add borders + size.Y += 2; // add borders + + const COORD viewportSize = screenInfo.GetViewport().Dimensions(); + + size.X = std::min(size.X, viewportSize.X); + size.Y = std::min(size.Y, viewportSize.Y); + + // make sure there's enough room for the popup borders + THROW_HR_IF(E_NOT_SUFFICIENT_BUFFER, size.X < 2 || size.Y < 2); + + return size; +} + +// Routine Description: +// - Helper to calculate the origin point (within the screen buffer) for the popup +// Arguments: +// - screenInfo - Screen buffer we will be drawing into +// - size - The size that the popup will consume +// Return Value: +// - Coordinate position of the origin point of the popup +COORD Popup::_CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const COORD size) +{ + const auto viewport = screenInfo.GetViewport(); + + // determine origin. center popup on window + COORD origin; + origin.X = gsl::narrow((viewport.Width() - size.X) / 2 + viewport.Left()); + origin.Y = gsl::narrow((viewport.Height() - size.Y) / 2 + viewport.Top()); + return origin; +} + +// Routine Description: +// - Helper to return the width of the popup in columns +// Return Value: +// - Width of popup inside attached screen buffer. +SHORT Popup::Width() const noexcept +{ + return _region.Right - _region.Left - 1i16; +} + +// Routine Description: +// - Helper to return the height of the popup in columns +// Return Value: +// - Height of popup inside attached screen buffer. +SHORT Popup::Height() const noexcept +{ + return _region.Bottom - _region.Top - 1i16; +} + +// Routine Description: +// - Helper to get the position on top of some types of popup dialogs where +// we should overlay the cursor for user input. +// Return Value: +// - Coordinate location on the popup where the cursor should be placed. +COORD Popup::GetCursorPosition() const noexcept +{ + COORD CursorPosition; + CursorPosition.X = _region.Right - static_cast(MINIMUM_COMMAND_PROMPT_SIZE); + CursorPosition.Y = _region.Top + 1i16; + return CursorPosition; +} + +// Routine Description: +// - changes the function used to gather user input. for allowing custom input during unit tests only +// Arguments: +// - function - function to use to fetch input +void Popup::SetUserInputFunction(UserInputFunction function) noexcept +{ + _userInputFunction = function; +} + +// Routine Description: +// - gets a single char input from the user +// Arguments: +// - cookedReadData - cookedReadData for this popup operation +// - popupKey - on completion, will be true if key was a popup key +// - wch - on completion, the char read from the user +// Return Value: +// - relevant NTSTATUS +NTSTATUS Popup::_getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept +{ + return _userInputFunction(cookedReadData, popupKey, modifiers, wch); +} + +// Routine Description: +// - gets a single char input from the user using the InputBuffer +// Arguments: +// - cookedReadData - cookedReadData for this popup operation +// - popupKey - on completion, will be true if key was a popup key +// - wch - on completion, the char read from the user +// Return Value: +// - relevant NTSTATUS +NTSTATUS Popup::_getUserInputInternal(COOKED_READ_DATA& cookedReadData, + bool& popupKey, + DWORD& modifiers, + wchar_t& wch) noexcept +{ + InputBuffer* const pInputBuffer = cookedReadData.GetInputBuffer(); + NTSTATUS Status = GetChar(pInputBuffer, + &wch, + true, + nullptr, + &popupKey, + &modifiers); + if (!NT_SUCCESS(Status) && Status != CONSOLE_STATUS_WAIT) + { + cookedReadData.BytesRead() = 0; + } + return Status; +} diff --git a/src/host/popup.h b/src/host/popup.h new file mode 100644 index 000000000..0429f9e73 --- /dev/null +++ b/src/host/popup.h @@ -0,0 +1,92 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- popup.h + +Abstract: +- This file contains the internal structures and definitions used by command line input and editing. + +Author: +- Therese Stowell (ThereseS) 15-Nov-1991 + +Revision History: +- Mike Griese (migrie) Jan 2018: + Refactored the history and alias functionality into their own files. +- Michael Niksa (miniksa) May 2018: + Separated out popups from the rest of command line functionality. +--*/ + +#pragma once + +#include "readDataCooked.hpp" +#include "screenInfo.hpp" +#include "readDataCooked.hpp" + + +#define MINIMUM_COMMAND_PROMPT_SIZE 5 + + +class CommandHistory; + +class Popup +{ +public: + + using UserInputFunction = std::function; + + Popup(SCREEN_INFORMATION& screenInfo, const COORD proposedSize); + virtual ~Popup(); + [[nodiscard]] + virtual NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept = 0; + + void Draw(); + + void UpdateStoredColors(const TextAttribute& newAttr, + const TextAttribute& newPopupAttr, + const TextAttribute& oldAttr, + const TextAttribute& oldPopupAttr); + + void End(); + + SHORT Width() const noexcept; + SHORT Height() const noexcept; + + COORD GetCursorPosition() const noexcept; + +protected: + // used in test code to alter how the popup fetches use input + void SetUserInputFunction(UserInputFunction function) noexcept; + +#ifdef UNIT_TESTING + friend class CopyFromCharPopupTests; + friend class CopyToCharPopupTests; + friend class CommandNumberPopupTests; + friend class CommandListPopupTests; +#endif + + NTSTATUS _getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept; + void _DrawPrompt(const UINT id); + virtual void _DrawContent() = 0; + + + SMALL_RECT _region; // region popup occupies + SCREEN_INFORMATION& _screenInfo; + TextAttribute _attributes; // text attributes + +private: + COORD _CalculateSize(const SCREEN_INFORMATION& screenInfo, const COORD proposedSize); + COORD _CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const COORD size); + + void _DrawBorder(); + + static NTSTATUS _getUserInputInternal(COOKED_READ_DATA& cookedReadData, + bool& popupKey, + DWORD& modifiers, + wchar_t& wch) noexcept; + + OutputCellRect _oldContents; // contains data under popup + COORD _oldScreenSize; + UserInputFunction _userInputFunction; +}; diff --git a/src/host/precomp.cpp b/src/host/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/host/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/host/precomp.h b/src/host/precomp.h new file mode 100644 index 000000000..9c5d40fe4 --- /dev/null +++ b/src/host/precomp.h @@ -0,0 +1,96 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +#define DEFINE_CONSOLEV2_PROPERTIES + +// Ignore checked iterators warning from VC compiler. +#define _SCL_SECURE_NO_WARNINGS + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +// This includes a lot of common headers needed by both the host and the propsheet +// including: windows.h, winuser, ntstatus, assert, and the DDK +#include "HostAndPropsheetIncludes.h" + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#define SCREEN_BUFFER_POINTER(X,Y,XSIZE,CELLSIZE) (((XSIZE * (Y)) + (X)) * (ULONG)CELLSIZE) +#include + +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include "conserv.h" + +#include "conv.h" + +#pragma prefast(push) +#pragma prefast(disable:26071, "Range violation in Intsafe. Not ours.") +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def +#include +#pragma prefast(pop) +#include +#include +#include +#include "utils.hpp" + +// Including TraceLogging essentials for the binary +#include +#include +TRACELOGGING_DECLARE_PROVIDER(g_hConhostV2EventTraceProvider); +#include +#include +#include "telemetry.hpp" +#include "tracing.hpp" + +#ifdef BUILDING_INSIDE_WINIDE +#define DbgRaiseAssertionFailure() __int2c() +#endif + +#include +#include "..\propslib\conpropsp.hpp" + +// Comment to build against the private SDK. +#define CON_BUILD_PUBLIC + +#ifdef CON_BUILD_PUBLIC + #define CON_USERPRIVAPI_INDIRECT + #define CON_DPIAPI_INDIRECT +#endif + +#include "..\inc\contsf.h" +#include "..\inc\operators.hpp" +#include "..\inc\conattrs.hpp" + +// TODO: MSFT 9355094 Find a better way of doing this. http://osgvsowi/9355094 +[[nodiscard]] +inline NTSTATUS NTSTATUS_FROM_HRESULT(HRESULT hr) +{ + return NTSTATUS_FROM_WIN32(HRESULT_CODE(hr)); +} diff --git a/src/host/readData.cpp b/src/host/readData.cpp new file mode 100644 index 000000000..710b5fd58 --- /dev/null +++ b/src/host/readData.cpp @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "readData.hpp" + +// Routine Description: +// - Constructs read data base class to hold input buffer and cross-call handle information +// - Increments count of readers waiting on the given handle. +// Arguments: +// - pInputBuffer - Buffer that data will be read from. +// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately. +// Return Value: +// - THROW: Throws E_INVALIDARG for invalid pointers. +ReadData::ReadData(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData) : + IWaitRoutine(ReplyDataType::Read), + _pInputBuffer(THROW_HR_IF_NULL(E_INVALIDARG, pInputBuffer)), + _pInputReadHandleData(THROW_HR_IF_NULL(E_INVALIDARG, pInputReadHandleData)) +{ + _pInputReadHandleData->IncrementReadCount(); +} + +// Routine Description: +// - Destructs a read data class. +// - Decrements count of readers waiting on the given handle. +ReadData::~ReadData() +{ + // If the contents were moved, this might be null. + if (_pInputReadHandleData != nullptr) + { + _pInputReadHandleData->DecrementReadCount(); + } +} + +// Routine Description: +// - Moves another ReadData instance into this one. +// - Effectively steals ownership of the other instance's members, setting them to nullptr so +// destroying other won't trigger resource cleanup actions. +// Arguments: +// - other - Another ReadData class instance. +ReadData::ReadData(ReadData&& other) : + IWaitRoutine(other.GetReplyType()), + _pInputBuffer(other._pInputBuffer), + _pInputReadHandleData(other._pInputReadHandleData) +{ + other._pInputBuffer = nullptr; + other._pInputReadHandleData = nullptr; +} + +// Routine Description: +// - Retrieves the input buffer pointer associated with this read data context +// Arguments: +// - +// Return Value: +// - Input buffer pointer. +InputBuffer* ReadData::GetInputBuffer() const +{ + return _pInputBuffer; +} + +// Routine Description: +// - Retrieves the persistent handle data structure used to store read information across calls +// Arguments: +// - +// Return Value: +// - Input read handle data pointer. +INPUT_READ_HANDLE_DATA* ReadData::GetInputReadHandleData() const +{ + return _pInputReadHandleData; +} diff --git a/src/host/readData.hpp b/src/host/readData.hpp new file mode 100644 index 000000000..c9cc3acad --- /dev/null +++ b/src/host/readData.hpp @@ -0,0 +1,61 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- readData.hpp + +Abstract: +- This file defines the interface for read data structures. +- Read data structures are used to pass context between various layers of the read + as well as to persist state across a read call that must wait until additional + data is added to the buffer at a later time. + +Author: +- Austin Diviness (AustDi) 1-Mar-2017 +- Michael Niksa (MiNiksa) 1-Mar-2017 + +Revision History: +--*/ + +#pragma once + +#include "../server/IWaitRoutine.h" +#include "../server/WaitTerminationReason.h" + +class ReadData : public IWaitRoutine +{ +public: + ReadData(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData); + + virtual ~ReadData(); + + ReadData(const ReadData&) = delete; + ReadData(ReadData&&); + ReadData& operator=(const ReadData&) & = delete; + ReadData& operator=(ReadData &&) & = delete; + + 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; + +// TODO MSFT:11285829 this is a temporary kludge until the constructors are ironed +// out, so that we can still run the tests in the meantime. +#if UNIT_TESTING + ReadData() : + IWaitRoutine(ReplyDataType::Read), + _pInputBuffer{ nullptr }, + _pInputReadHandleData{ nullptr} + {} +#endif +protected: + InputBuffer* _pInputBuffer; + INPUT_READ_HANDLE_DATA* _pInputReadHandleData; +}; diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp new file mode 100644 index 000000000..8bdc023e8 --- /dev/null +++ b/src/host/readDataCooked.cpp @@ -0,0 +1,1198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "readDataCooked.hpp" +#include "dbcs.h" +#include "stream.h" +#include "misc.h" +#include "_stream.h" +#include "inputBuffer.hpp" +#include "cmdline.h" +#include "../types/inc/GlyphWidth.hpp" +#include "../types/inc/convert.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#define LINE_INPUT_BUFFER_SIZE (256 * sizeof(WCHAR)) + +// Routine Description: +// - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'. +// Arguments: +// - pInputBuffer - Buffer that data will be read from. +// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately. +// - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it +// - BufferSize - +// - BytesRead - +// - CurrentPosition - +// - BufPtr - +// - BackupLimit - +// - UserBufferSize - The byte count of the buffer presented by the client +// - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host. +// - OriginalCursorPosition - +// - NumberOfVisibleChars +// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application +// - CommandHistory - +// - Echo - +// - InsertMode - +// - Processed - +// - Line - +// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text. +// - initialData - any text data that should be prepopulated into the buffer +// Return Value: +// - THROW: Throws E_INVALIDARG for invalid pointers. +COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + SCREEN_INFORMATION& screenInfo, + _In_ size_t UserBufferSize, + _In_ PWCHAR UserBuffer, + _In_ ULONG CtrlWakeupMask, + _In_ CommandHistory* CommandHistory, + const std::wstring_view exeName, + const std::string_view initialData +) : + ReadData(pInputBuffer, pInputReadHandleData), + _screenInfo{ screenInfo }, + _bytesRead{ 0 }, + _currentPosition{ 0 }, + _userBufferSize{ UserBufferSize }, + _userBuffer{ UserBuffer }, + _tempHandle{ nullptr }, + _exeName{ exeName }, + _pdwNumBytes{ nullptr }, + + _commandHistory{ CommandHistory }, + _controlKeyState{ 0 }, + _ctrlWakeupMask{ CtrlWakeupMask }, + _visibleCharCount{ 0 }, + _originalCursorPosition{ -1, -1 }, + _beforeDialogCursorPosition{ 0, 0 }, + + _echoInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_ECHO_INPUT) }, + _lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) }, + _processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) }, + _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() }, + _unicode{ false } +{ +#ifndef UNIT_TESTING + THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + _tempHandle)); +#endif + + // to emulate OS/2 KbdStringIn, we read into our own big buffer + // (256 bytes) until the user types enter. then return as many + // chars as will fit in the user's buffer. + _bufferSize = std::max(UserBufferSize, LINE_INPUT_BUFFER_SIZE); + _buffer = std::make_unique(_bufferSize); + _backupLimit = reinterpret_cast(_buffer.get()); + _bufPtr = reinterpret_cast(_buffer.get()); + + // Initialize the user's buffer to spaces. This is done so that + // moving in the buffer via cursor doesn't do strange things. + std::fill_n(_bufPtr, _bufferSize / sizeof(wchar_t), UNICODE_SPACE); + + if (!initialData.empty()) + { + memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size()); + + _bytesRead += initialData.size(); + + const size_t cchInitialData = initialData.size() / sizeof(wchar_t); + VisibleCharCount() = cchInitialData; + _bufPtr += cchInitialData; + _currentPosition = cchInitialData; + + OriginalCursorPosition() = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + OriginalCursorPosition().X -= (SHORT)_currentPosition; + + const SHORT sScreenBufferSizeX = screenInfo.GetBufferSize().Width(); + while (OriginalCursorPosition().X < 0) + { + OriginalCursorPosition().X += sScreenBufferSizeX; + OriginalCursorPosition().Y -= 1; + } + } + + // TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci +} + +// Routine Description: +// - Destructs a read data class. +// - Decrements count of readers waiting on the given handle. +COOKED_READ_DATA::~COOKED_READ_DATA() +{ + CommandLine::Instance().EndAllPopups(); +} + +gsl::span COOKED_READ_DATA::SpanWholeBuffer() +{ + return gsl::make_span(_backupLimit, (_bufferSize / sizeof(wchar_t))); +} + +gsl::span COOKED_READ_DATA::SpanAtPointer() +{ + auto wholeSpan = SpanWholeBuffer(); + return wholeSpan.subspan(_bufPtr - _backupLimit); +} + +bool COOKED_READ_DATA::HasHistory() const noexcept +{ + return _commandHistory != nullptr; +} + +CommandHistory& COOKED_READ_DATA::History() noexcept +{ + return *_commandHistory; +} + +const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept +{ + return _visibleCharCount; +} + +size_t& COOKED_READ_DATA::VisibleCharCount() noexcept +{ + return _visibleCharCount; +} + +SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept +{ + return _screenInfo; +} + +const COORD& COOKED_READ_DATA::OriginalCursorPosition() const noexcept +{ + return _originalCursorPosition; +} + +COORD& COOKED_READ_DATA::OriginalCursorPosition() noexcept +{ + return _originalCursorPosition; +} + +COORD& COOKED_READ_DATA::BeforeDialogCursorPosition() noexcept +{ + return _beforeDialogCursorPosition; +} + +bool COOKED_READ_DATA::IsEchoInput() const noexcept +{ + return _echoInput; +} + +bool COOKED_READ_DATA::IsInsertMode() const noexcept +{ + return _insertMode; +} + +void COOKED_READ_DATA::SetInsertMode(const bool mode) noexcept +{ + _insertMode = mode; +} + +bool COOKED_READ_DATA::IsUnicode() const noexcept +{ + return _unicode; +} + +// Routine Description: +// - gets the size of the user buffer +// Return Value: +// - the size of the user buffer in bytes +size_t COOKED_READ_DATA::UserBufferSize() const noexcept +{ + return _userBufferSize; +} + +// Routine Description: +// - gets a pointer to the beginning of the prompt storage +// Return Value: +// - pointer to the first char in the internal prompt storage array +wchar_t* COOKED_READ_DATA::BufferStartPtr() noexcept +{ + return _backupLimit; +} + +// Routine Description: +// - gets a pointer to where the next char will be inserted into the prompt storage +// Return Value: +// - pointer to the current insertion point of the prompt storage array +wchar_t* COOKED_READ_DATA::BufferCurrentPtr() noexcept +{ + return _bufPtr; +} + +// Routine Description: +// - Set the location of the next char insert into the prompt storage to be at +// ptr. ptr must point into a valid portion of the internal prompt storage array +// Arguments: +// - ptr - the new char insertion location +void COOKED_READ_DATA::SetBufferCurrentPtr(wchar_t* ptr) noexcept +{ + _bufPtr = ptr; +} + +// Routine Description: +// - gets the number of bytes read so far into the prompt buffer +// Return Value: +// - the number of bytes read +const size_t& COOKED_READ_DATA::BytesRead() const noexcept +{ + return _bytesRead; +} + +// Routine Description: +// - gets the number of bytes read so far into the prompt buffer +// Return Value: +// - the number of bytes read +size_t& COOKED_READ_DATA::BytesRead() noexcept +{ + return _bytesRead; +} + +// Routine Description: +// - gets the index for the current insertion point of the prompt +// Return Value: +// - the index of the current insertion point +const size_t& COOKED_READ_DATA::InsertionPoint() const noexcept +{ + return _currentPosition; +} + +// Routine Description: +// - gets the index for the current insertion point of the prompt +// Return Value: +// - the index of the current insertion point +size_t& COOKED_READ_DATA::InsertionPoint() noexcept +{ + return _currentPosition; +} + +// Routine Description: +// - sets the number of bytes that will be reported when this read block completes its read +// Arguments: +// - count - the number of bytes to report +void COOKED_READ_DATA::SetReportedByteCount(const size_t count) noexcept +{ + FAIL_FAST_IF_NULL(_pdwNumBytes); + *_pdwNumBytes = count; +} + +// Routine Description: +// - resets the prompt to be as if it was erased +void COOKED_READ_DATA::Erase() noexcept +{ + _bufPtr = _backupLimit; + _bytesRead = 0; + _currentPosition = 0; + _visibleCharCount = 0; + +} + +// Routine Description: +// - This routine is called to complete a cooked read that blocked in ReadInputBuffer. +// - The context of the read was saved in the CookedReadData structure. +// - This routine is called when events have been written to the input buffer. +// - It is called in the context of the writing thread. +// - It may be called more than once. +// Arguments: +// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument +// contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0. +// - fIsUnicode - Whether to convert the final data to A (using Console Input CP) at the end or treat everything as Unicode (UCS-2) +// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait) +// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process +// - pControlKeyState - For certain types of reads, this specifies which modifier keys were held. +// - pOutputData - not used +// Return Value: +// - true if the wait is done and result buffer/status code can be sent back to the client. +// - false if we need to continue to wait until more data is available. +bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, + const bool fIsUnicode, + _Out_ NTSTATUS* const pReplyStatus, + _Out_ size_t* const pNumBytes, + _Out_ DWORD* const pControlKeyState, + _Out_ void* const /*pOutputData*/) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // this routine should be called by a thread owning the same + // lock on the same console as we're reading from. + FAIL_FAST_IF(!gci.IsConsoleLocked()); + + *pNumBytes = 0; + *pControlKeyState = 0; + + *pReplyStatus = STATUS_SUCCESS; + + FAIL_FAST_IF(_pInputReadHandleData->IsInputPending()); + + // this routine should be called by a thread owning the same lock on the same console as we're reading from. + FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); + + // if ctrl-c or ctrl-break was seen, terminate read. + if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) + { + *pReplyStatus = STATUS_ALERTED; + gci.SetCookedReadData(nullptr); + return true; + } + + // See if we were called because the thread that owns this wait block is exiting. + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) + { + *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + gci.SetCookedReadData(nullptr); + return true; + } + + // We must see if we were woken up because the handle is being closed. If + // so, we decrement the read count. If it goes to zero, we wake up the + // close thread. Otherwise, we wake up any other thread waiting for data. + + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) + { + *pReplyStatus = STATUS_ALERTED; + gci.SetCookedReadData(nullptr); + return true; + } + + // If we get to here, this routine was called either by the input thread + // or a write routine. Both of these callers grab the current console + // lock. + + // MSFT:13994975 This is REALLY weird. + // When we're doing cooked reading for popups, we come through this method + // twice. Once when we press F7 to bring up the popup, then again when we + // press enter to input the selected command. + // The first time, there is no popup, and we go to CookedRead. We pass into + // CookedRead `pNumBytes`, which is passed to us as the address of the + // stack variable dwNumBytes, in ConsoleWaitBlock::Notify. + // CookedRead sets this->_pdwNumBytes to that value, and starts the popup, + // which returns all the way up, and pops the ConsoleWaitBlock::Notify + // stack frame containing the address we're pointing at. + // Then on the second time through this function, we hit this if block, + // because there is a popup to get input from. + // However, pNumBytes is now the address of a different stack frame, and not + // necessarily the same as before (presumably not at all). The + // Callback would try and write the number of bytes read to the + // value in _pdwNumBytes, and then we'd return up to ConsoleWaitBlock::Notify, + // who's dwNumBytes had nothing in it. + // To fix this, when we hit this with a popup, we're going to make sure to + // refresh the value of _pdwNumBytes to the current address we want to put + // the out value into. + // It's still really weird, but limits the potential fallout of changing a + // piece of old spaghetti code. + if (_commandHistory) + { + if (CommandLine::Instance().HasPopup()) + { + // (see above comment, MSFT:13994975) + // Make sure that the popup writes the dwNumBytes to the right place + if (pNumBytes) + { + _pdwNumBytes = pNumBytes; + } + + auto& popup = CommandLine::Instance().GetPopup(); + *pReplyStatus = popup.Process(*this); + if (*pReplyStatus == CONSOLE_STATUS_READ_COMPLETE || + (*pReplyStatus != CONSOLE_STATUS_WAIT && *pReplyStatus != CONSOLE_STATUS_WAIT_NO_BLOCK)) + { + *pReplyStatus = S_OK; + gci.SetCookedReadData(nullptr); + return true; + } + return false; + } + } + + *pReplyStatus = Read(fIsUnicode, *pNumBytes, *pControlKeyState); + if (*pReplyStatus != CONSOLE_STATUS_WAIT) + { + gci.SetCookedReadData(nullptr); + return true; + } + else + { + return false; + } +} + +bool COOKED_READ_DATA::AtEol() const noexcept +{ + return _bytesRead == (_currentPosition * 2); +} + +// Routine Description: +// - Method that actually retrieves a character/input record from the buffer (key press form) +// and determines the next action based on the various possible cooked read modes. +// - Mode options include the F-keys popup menus, keyboard manipulation of the edit line, etc. +// - This method also does the actual copying of the final manipulated data into the return buffer. +// Arguments: +// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. +// - numBytes - On in, the number of bytes available in the client +// buffer. On out, the number of bytes consumed in the client buffer. +// - controlKeyState - For some types of reads, this is the modifier key state with the last button press. +[[nodiscard]] +HRESULT COOKED_READ_DATA::Read(const bool isUnicode, + size_t& numBytes, + ULONG& controlKeyState) noexcept +{ + controlKeyState = 0; + + NTSTATUS Status = _readCharInputLoop(isUnicode, numBytes); + + // if the read was completed (status != wait), free the cooked read + // data. also, close the temporary output handle that was opened to + // echo the characters read. + if (Status != CONSOLE_STATUS_WAIT) + { + Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + } + + return Status; +} + +void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount) +{ + Alias::s_MatchAndCopyAliasLegacy(_backupLimit, + _bytesRead, + _backupLimit, + _bufferSize, + _bytesRead, + _exeName, + lineCount); +} + +// Routine Description: +// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and +// the final ENTER key press that will end the wait and finally return the data. +// Arguments: +// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.) +// - wch - The most recently pressed/retrieved character from the input buffer (keystroke) +// - keyState - Modifier keys/state information with the pressed key/character +// - status - The return code to pass to the client +// Return Value: +// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke. +bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig, + const DWORD keyState, + NTSTATUS& status) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + size_t NumSpaces = 0; + SHORT ScrollY = 0; + size_t NumToWrite; + WCHAR wch = wchOrig; + bool fStartFromDelim; + + status = STATUS_SUCCESS; + if (_bytesRead >= (_bufferSize - (2 * sizeof(WCHAR))) && wch != UNICODE_CARRIAGERETURN && wch != UNICODE_BACKSPACE) + { + return false; + } + + if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) + { + *_bufPtr = wch; + _bytesRead += sizeof(WCHAR); + _bufPtr += 1; + _currentPosition += 1; + _controlKeyState = keyState; + return true; + } + + if (wch == EXTKEY_ERASE_PREV_WORD) + { + wch = UNICODE_BACKSPACE; + } + + if (AtEol()) + { + // If at end of line, processing is relatively simple. Just store the character and write it to the screen. + if (wch == UNICODE_BACKSPACE2) + { + wch = UNICODE_BACKSPACE; + } + + if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit) + { + fStartFromDelim = IsWordDelim(_bufPtr[-1]); + + bool loop = true; + while (loop) + { + loop = false; + if (_echoInput) + { + NumToWrite = sizeof(WCHAR); + status = WriteCharsLegacy(_screenInfo, + _backupLimit, + _bufPtr, + &wch, + &NumToWrite, + &NumSpaces, + _originalCursorPosition.X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY); + if (NT_SUCCESS(status)) + { + _originalCursorPosition.Y += ScrollY; + } + else + { + RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); + } + } + + _visibleCharCount += NumSpaces; + if (wch == UNICODE_BACKSPACE && _processedInput) + { + _bytesRead -= sizeof(WCHAR); + #pragma prefast(suppress:__WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine") + *_bufPtr = (WCHAR)' '; + _bufPtr -= 1; + _currentPosition -= 1; + + // Repeat until it hits the word boundary + if (wchOrig == EXTKEY_ERASE_PREV_WORD && + _bufPtr != _backupLimit && + fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) + { + loop = true; + } + } + else + { + *_bufPtr = wch; + _bytesRead += sizeof(WCHAR); + _bufPtr += 1; + _currentPosition += 1; + } + } + } + } + else + { + bool CallWrite = true; + const SHORT sScreenBufferSizeX = _screenInfo.GetBufferSize().Width(); + + // processing in the middle of the line is more complex: + + // calculate new cursor position + // store new char + // clear the current command line from the screen + // write the new command line to the screen + // update the cursor position + + if (wch == UNICODE_BACKSPACE && _processedInput) + { + // for backspace, use writechars to calculate the new cursor position. + // this call also sets the cursor to the right position for the + // second call to writechars. + + if (_bufPtr != _backupLimit) + { + + fStartFromDelim = IsWordDelim(_bufPtr[-1]); + + bool loop = true; + while (loop) + { + loop = false; + // we call writechar here so that cursor position gets updated + // correctly. we also call it later if we're not at eol so + // that the remainder of the string can be updated correctly. + + if (_echoInput) + { + NumToWrite = sizeof(WCHAR); + status = WriteCharsLegacy(_screenInfo, + _backupLimit, + _bufPtr, + &wch, + &NumToWrite, + nullptr, + _originalCursorPosition.X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr); + if (!NT_SUCCESS(status)) + { + RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); + } + } + _bytesRead -= sizeof(WCHAR); + _bufPtr -= 1; + _currentPosition -= 1; + memmove(_bufPtr, + _bufPtr + 1, + _bytesRead - (_currentPosition * sizeof(WCHAR))); + { + PWCHAR buf = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); + *buf = (WCHAR)' '; + } + NumSpaces = 0; + + // Repeat until it hits the word boundary + if (wchOrig == EXTKEY_ERASE_PREV_WORD && + _bufPtr != _backupLimit && + fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) + { + loop = true; + } + } + } + else + { + CallWrite = false; + } + } + else + { + // store the char + if (wch == UNICODE_CARRIAGERETURN) + { + _bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); + *_bufPtr = wch; + _bufPtr += 1; + _bytesRead += sizeof(WCHAR); + _currentPosition += 1; + } + else + { + bool fBisect = false; + + if (_echoInput) + { + if (CheckBisectProcessW(_screenInfo, + _backupLimit, + _currentPosition + 1, + sScreenBufferSizeX - _originalCursorPosition.X, + _originalCursorPosition.X, + TRUE)) + { + fBisect = true; + } + } + + if (_insertMode) + { + memmove(_bufPtr + 1, + _bufPtr, + _bytesRead - (_currentPosition * sizeof(WCHAR))); + _bytesRead += sizeof(WCHAR); + } + *_bufPtr = wch; + _bufPtr += 1; + _currentPosition += 1; + + // calculate new cursor position + if (_echoInput) + { + NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.X, + _backupLimit, + _currentPosition - 1); + if (NumSpaces > 0 && fBisect) + NumSpaces--; + } + } + } + + if (_echoInput && CallWrite) + { + COORD CursorPosition; + + // save cursor position + CursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); + CursorPosition.X = (SHORT)(CursorPosition.X + NumSpaces); + + // clear the current command line from the screen +#pragma prefast(suppress:__WARNING_BUFFER_OVERFLOW, "Not sure why prefast doesn't like this call.") + DeleteCommandLine(*this, FALSE); + + // write the new command line to the screen + NumToWrite = _bytesRead; + + DWORD dwFlags = WC_DESTRUCTIVE_BACKSPACE | WC_ECHO; + if (wch == UNICODE_CARRIAGERETURN) + { + dwFlags |= WC_KEEP_CURSOR_VISIBLE; + } + status = WriteCharsLegacy(_screenInfo, + _backupLimit, + _backupLimit, + _backupLimit, + &NumToWrite, + &_visibleCharCount, + _originalCursorPosition.X, + dwFlags, + &ScrollY); + if (!NT_SUCCESS(status)) + { + RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); + _bytesRead = 0; + return true; + } + + // update cursor position + if (wch != UNICODE_CARRIAGERETURN) + { + if (CheckBisectProcessW(_screenInfo, + _backupLimit, + _currentPosition + 1, + sScreenBufferSizeX - _originalCursorPosition.X, + _originalCursorPosition.X, TRUE)) + { + if (CursorPosition.X == (sScreenBufferSizeX - 1)) + { + CursorPosition.X++; + } + } + + // adjust cursor position for WriteChars + _originalCursorPosition.Y += ScrollY; + CursorPosition.Y += ScrollY; + status = AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr); + if (!NT_SUCCESS(status)) + { + _bytesRead = 0; + return true; + } + } + } + } + + // in cooked mode, enter (carriage return) is converted to + // carriage return linefeed (0xda). carriage return is always + // stored at the end of the buffer. + if (wch == UNICODE_CARRIAGERETURN) + { + if (_processedInput) + { + if (_bytesRead < _bufferSize) + { + *_bufPtr = UNICODE_LINEFEED; + if (_echoInput) + { + NumToWrite = sizeof(WCHAR); + status = WriteCharsLegacy(_screenInfo, + _backupLimit, + _bufPtr, + _bufPtr, + &NumToWrite, + nullptr, + _originalCursorPosition.X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + nullptr); + if (!NT_SUCCESS(status)) + { + RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); + } + } + _bytesRead += sizeof(WCHAR); + _bufPtr++; + _currentPosition += 1; + } + } + // reset the cursor back to 25% if necessary + if (_lineInput) + { + if (_insertMode != gci.GetInsertMode()) + { + // Make cursor small. + LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0)); + } + + status = STATUS_SUCCESS; + return true; + } + } + + return false; +} + +// Routine Description: +// - Writes string to current position in prompt line. can overwrite text to the right of the cursor. +// Arguments: +// - wstr - the string to write +// Return Value: +// - The number of chars written +size_t COOKED_READ_DATA::Write(const std::wstring_view wstr) +{ + auto end = wstr.end(); + const size_t charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit); + if (wstr.size() > charsRemaining) + { + end = std::next(wstr.begin(), charsRemaining); + } + + std::copy(wstr.begin(), end, _bufPtr); + const size_t charsInserted = end - wstr.begin(); + size_t bytesInserted = charsInserted * sizeof(wchar_t); + _currentPosition += charsInserted; + _bytesRead += bytesInserted; + + + if (IsEchoInput()) + { + size_t NumSpaces = 0; + SHORT ScrollY = 0; + + FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(), + _backupLimit, + _bufPtr, + _bufPtr, + &bytesInserted, + &NumSpaces, + OriginalCursorPosition().X, + WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_ECHO, + &ScrollY)); + OriginalCursorPosition().Y += ScrollY; + VisibleCharCount() += NumSpaces; + } + _bufPtr += charsInserted; + + return charsInserted; +} + +// Routine Description: +// - saves data in the prompt buffer to the outgoing user buffer +// Arguments: +// - cch - the number of chars to write to the user buffer +// Return Value: +// - the number of bytes written to the user buffer +size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch) +{ + size_t bytesToWrite = 0; + const HRESULT hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite); + if (FAILED(hr)) + { + return 0; + } + + memmove(_userBuffer, _backupLimit, bytesToWrite); + + if (!IsUnicode()) + { + try + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const std::wstring wstr = ConvertToW(gci.CP, { reinterpret_cast(_userBuffer), cch }); + const size_t copyAmount = std::min(wstr.size(), _userBufferSize / sizeof(wchar_t)); + std::copy_n(wstr.begin(), copyAmount, _userBuffer); + return copyAmount * sizeof(wchar_t); + } + CATCH_LOG(); + } + return bytesToWrite; +} + +// Routine Description: +// - saves data in the prompt buffer as pending input +// Arguments: +// - index - the index of what wchar to start the saving +// - multiline - whether the pending input should be saved as multiline or not +void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline) +{ + INPUT_READ_HANDLE_DATA& inputReadHandleData = *GetInputReadHandleData(); + const std::wstring_view pending{ _backupLimit + index, + BytesRead() / sizeof(wchar_t) - index}; + if (multiline) + { + inputReadHandleData.SaveMultilinePendingInput(pending); + } + else + { + inputReadHandleData.SavePendingInput(pending); + } +} + +// Routine Description: +// - saves data in the prompt buffer as pending input +// Arguments: +// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. +// - numBytes - On in, the number of bytes available in the client +// buffer. On out, the number of bytes consumed in the client buffer. +// Return Value: +// - Status code that indicates success, wait, etc. +[[nodiscard]] +NTSTATUS COOKED_READ_DATA::_readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept +{ + NTSTATUS Status = STATUS_SUCCESS; + + while (_bytesRead < _bufferSize) + { + wchar_t wch = UNICODE_NULL; + bool commandLineEditingKeys = false; + DWORD keyState = 0; + + // This call to GetChar may block. + Status = GetChar(_pInputBuffer, + &wch, + true, + &commandLineEditingKeys, + nullptr, + &keyState); + if (!NT_SUCCESS(Status)) + { + if (Status != CONSOLE_STATUS_WAIT) + { + _bytesRead = 0; + } + break; + } + + // we should probably set these up in GetChars, but we set them + // up here because the debugger is multi-threaded and calls + // read before outputting the prompt. + + if (_originalCursorPosition.X == -1) + { + _originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); + } + + if (commandLineEditingKeys) + { + // TODO: this is super weird for command line popups only + _unicode = isUnicode; + + _pdwNumBytes = &numBytes; + + Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState); + if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT) + { + break; + } + if (!NT_SUCCESS(Status)) + { + if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK) + { + Status = CONSOLE_STATUS_WAIT; + } + else + { + _bytesRead = 0; + } + break; + } + } + else + { + if (ProcessInput(wch, keyState, Status)) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; + break; + } + } + } + return Status; +} + +// Routine Description: +// - handles any tasks that need to be completed after the read input loop finishes +// Arguments: +// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. +// - numBytes - On in, the number of bytes available in the client +// buffer. On out, the number of bytes consumed in the client buffer. +// - controlKeyState - For some types of reads, this is the modifier key state with the last button press. +// Return Value: +// - Status code that indicates success, out of memory, etc. +[[nodiscard]] +NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept +{ + DWORD LineCount = 1; + + if (_echoInput) + { + // Figure out where real string ends (at carriage return or end of buffer). + PWCHAR StringPtr = _backupLimit; + size_t StringLength = _bytesRead; + bool FoundCR = false; + for (size_t i = 0; i < (_bytesRead / sizeof(WCHAR)); i++) + { + if (*StringPtr++ == UNICODE_CARRIAGERETURN) + { + StringLength = i * sizeof(WCHAR); + FoundCR = true; + break; + } + } + + if (FoundCR) + { + if (_commandHistory) + { + // add to command line recall list if we have a history list. + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength / sizeof(wchar_t) }, + WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); + } + + // check for alias + ProcessAliases(LineCount); + } + } + + bool fAddDbcsLead = false; + size_t NumBytes = 0; + // at this point, a->NumBytes contains the number of bytes in + // the UNICODE string read. UserBufferSize contains the converted + // size of the app's buffer. + if (_bytesRead > _userBufferSize || LineCount > 1) + { + if (LineCount > 1) + { + PWSTR Tmp; + if (!isUnicode) + { + if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) + { + fAddDbcsLead = true; + std::unique_ptr event = GetInputBuffer()->FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *_userBuffer = static_cast(pKeyEvent->GetCharData()); + _userBuffer++; + _userBufferSize -= sizeof(wchar_t); + } + + NumBytes = 0; + for (Tmp = _backupLimit; + *Tmp != UNICODE_LINEFEED && _userBufferSize / sizeof(WCHAR) > NumBytes; + Tmp++) + { + NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; + } + } + +#pragma prefast(suppress:__WARNING_BUFFER_OVERFLOW, "LineCount > 1 means there's a UNICODE_LINEFEED") + for (Tmp = _backupLimit; *Tmp != UNICODE_LINEFEED; Tmp++) + { + FAIL_FAST_IF(!(Tmp < (_backupLimit + _bytesRead))); + } + + numBytes = (ULONG)(Tmp - _backupLimit + 1) * sizeof(*Tmp); + } + else + { + if (!isUnicode) + { + PWSTR Tmp; + + if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) + { + fAddDbcsLead = true; + std::unique_ptr event = GetInputBuffer()->FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *_userBuffer = static_cast(pKeyEvent->GetCharData()); + _userBuffer++; + _userBufferSize -= sizeof(wchar_t); + } + NumBytes = 0; + size_t NumToWrite = _bytesRead; + for (Tmp = _backupLimit; + NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes; + Tmp++, NumToWrite -= sizeof(WCHAR)) + { + NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; + } + } + numBytes = _userBufferSize; + } + + __analysis_assume(numBytes <= _userBufferSize); + memmove(_userBuffer, _backupLimit, numBytes); + + INPUT_READ_HANDLE_DATA* const pInputReadHandleData = GetInputReadHandleData(); + const std::wstring_view pending{ _backupLimit + (numBytes / sizeof(wchar_t)), (_bytesRead - numBytes) / sizeof(wchar_t) }; + if (LineCount > 1) + { + pInputReadHandleData->SaveMultilinePendingInput(pending); + } + else + { + pInputReadHandleData->SavePendingInput(pending); + } + } + else + { + if (!isUnicode) + { + PWSTR Tmp; + + if (_pInputBuffer->IsReadPartialByteSequenceAvailable()) + { + fAddDbcsLead = true; + std::unique_ptr event = GetInputBuffer()->FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *_userBuffer = static_cast(pKeyEvent->GetCharData()); + _userBuffer++; + _userBufferSize -= sizeof(wchar_t); + + if (_userBufferSize == 0) + { + numBytes = 1; + return STATUS_SUCCESS; + } + } + NumBytes = 0; + size_t NumToWrite = _bytesRead; + for (Tmp = _backupLimit; + NumToWrite && _userBufferSize / sizeof(WCHAR) > NumBytes; + Tmp++, NumToWrite -= sizeof(WCHAR)) + { + NumBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; + } + } + + numBytes = _bytesRead; + + if (numBytes > _userBufferSize) + { + return STATUS_BUFFER_OVERFLOW; + } + + memmove(_userBuffer, _backupLimit, numBytes); + } + controlKeyState = _controlKeyState; + + if (!isUnicode) + { + // if ansi, translate string. + std::unique_ptr tempBuffer; + try + { + tempBuffer = std::make_unique(NumBytes); + } + catch (...) + { + return STATUS_NO_MEMORY; + } + + std::unique_ptr partialEvent; + numBytes = TranslateUnicodeToOem(_userBuffer, + gsl::narrow(numBytes / sizeof(wchar_t)), + tempBuffer.get(), + gsl::narrow(NumBytes), + partialEvent); + + if (partialEvent.get()) + { + GetInputBuffer()->StoreReadPartialByteSequence(std::move(partialEvent)); + } + + + if (numBytes > _userBufferSize) + { + return STATUS_BUFFER_OVERFLOW; + } + + memmove(_userBuffer, tempBuffer.get(), numBytes); + if (fAddDbcsLead) + { + numBytes++; + } + } + return STATUS_SUCCESS; +} diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp new file mode 100644 index 000000000..d261c1124 --- /dev/null +++ b/src/host/readDataCooked.hpp @@ -0,0 +1,164 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- readDataCooked.hpp + +Abstract: +- This file defines the read data structure for reading the command line. +- Cooked reads specifically refer to when the console host acts as a command line on behalf + of another console application (e.g. aliases, command history, completion, line manipulation, etc.) +- The data struct will help store context across multiple calls or in the case of a wait condition. +- Wait conditions happen frequently for cooked reads because they're virtually always waiting for + the user to finish "manipulating" the edit line before hitting enter and submitting the final + result to the client application. +- A cooked read is also limited specifically to string/textual information. Only keyboard-type input applies. +- This can be triggered via ReadConsole A/W and ReadFile A/W calls. + +Author: +- Austin Diviness (AustDi) 1-Mar-2017 +- Michael Niksa (MiNiksa) 1-Mar-2017 + +Revision History: +- Pulled from original authoring by Therese Stowell (ThereseS, 1990) +- Separated from cmdline.h/cmdline.cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "readData.hpp" +#include "history.h" + +class COOKED_READ_DATA final : public ReadData +{ +public: + COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + SCREEN_INFORMATION& screenInfo, + _In_ size_t UserBufferSize, + _In_ PWCHAR UserBuffer, + _In_ ULONG CtrlWakeupMask, + _In_ CommandHistory* CommandHistory, + const std::wstring_view exeName, + const std::string_view initialData); + + ~COOKED_READ_DATA() override; + COOKED_READ_DATA(COOKED_READ_DATA&&) = default; + + bool AtEol() const noexcept; + + 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) override; + + gsl::span SpanAtPointer(); + gsl::span SpanWholeBuffer(); + + size_t Write(const std::wstring_view wstr); + + void ProcessAliases(DWORD& lineCount); + + [[nodiscard]] + HRESULT Read(const bool isUnicode, + size_t& numBytes, + ULONG& controlKeyState) noexcept; + + bool ProcessInput(const wchar_t wch, + const DWORD keyState, + NTSTATUS& status); + + CommandHistory& History() noexcept; + bool HasHistory() const noexcept; + + const size_t& VisibleCharCount() const noexcept; + size_t& VisibleCharCount() noexcept; + + SCREEN_INFORMATION& ScreenInfo() noexcept; + + const COORD& OriginalCursorPosition() const noexcept; + COORD& OriginalCursorPosition() noexcept; + + COORD& BeforeDialogCursorPosition() noexcept; + + bool IsEchoInput() const noexcept; + bool IsInsertMode() const noexcept; + void SetInsertMode(const bool mode) noexcept; + bool IsUnicode() const noexcept; + + size_t UserBufferSize() const noexcept; + + wchar_t* BufferStartPtr() noexcept; + wchar_t* BufferCurrentPtr() noexcept; + void SetBufferCurrentPtr(wchar_t* ptr) noexcept; + + const size_t& BytesRead() const noexcept; + size_t& BytesRead() noexcept; + + const size_t& InsertionPoint() const noexcept; + size_t& InsertionPoint() noexcept; + + void SetReportedByteCount(const size_t count) noexcept; + + void Erase() noexcept; + size_t SavePromptToUserBuffer(const size_t cch); + void SavePendingInput(const size_t cch, const bool multiline); + + +#if UNIT_TESTING + friend class CommandLineTests; + friend class CopyToCharPopupTests; + friend class CommandNumberPopupTests; + friend class CommandListPopupTests; + friend class PopupTestHelper; +#endif + +private: + size_t _bufferSize; // size in bytes + size_t _bytesRead; + + // insertion position into the buffer (where the conceptual prompt cursor is) + size_t _currentPosition; // char position, not byte position + + wchar_t* _bufPtr; // current position to insert chars at + + // should be const. the first char of the buffer + wchar_t* _backupLimit; + + size_t _userBufferSize; // doubled size in ansi case + wchar_t* _userBuffer; + + size_t* _pdwNumBytes; + + std::unique_ptr _buffer; + std::wstring _exeName; + std::unique_ptr _tempHandle; + + // TODO MSFT:11285829 make this something other than a deletable pointer + // non-ownership pointer + CommandHistory* _commandHistory; + + ULONG _controlKeyState; + ULONG _ctrlWakeupMask; + size_t _visibleCharCount; // TODO MSFT:11285829 is this cells or glyphs? ie. is a wide char counted as 1 or 2? + SCREEN_INFORMATION& _screenInfo; + + // Note that cookedReadData's _originalCursorPosition is the position before ANY text was entered on the edit line. + COORD _originalCursorPosition; + COORD _beforeDialogCursorPosition; // Currently only used for F9 (ProcessCommandNumberInput) since it's the only pop-up to move the cursor when it starts. + + const bool _echoInput; + const bool _lineInput; + const bool _processedInput; + bool _insertMode; + bool _unicode; + + [[nodiscard]] + NTSTATUS _readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept; + + [[nodiscard]] + NTSTATUS _handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept; +}; diff --git a/src/host/readDataDirect.cpp b/src/host/readDataDirect.cpp new file mode 100644 index 000000000..f23d07501 --- /dev/null +++ b/src/host/readDataDirect.cpp @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "readDataDirect.hpp" +#include "dbcs.h" +#include "misc.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Routine Description: +// - Constructs direct read data class to hold context across sessions +// generally when there's not enough data to return. Also used a bit +// internally to just pass some information along the stack +// (regardless of wait necessity). +// Arguments: +// - pInputBuffer - Buffer that data will be read from. +// - pInputReadHandleData - Context stored across calls from the same +// input handle to return partial data appropriately. +// the user's buffer (pOutRecords) +// - eventReadCount - the number of events to read +// - partialEvents - any partial events already read +// Return Value: +// - 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) : + ReadData(pInputBuffer, pInputReadHandleData), + _eventReadCount{ eventReadCount }, + _partialEvents{ std::move(partialEvents) }, + _outEvents{ } +{ +} + +// Routine Description: +// - Destructs a read data class. +// - Decrements count of readers waiting on the given handle. +DirectReadData::~DirectReadData() +{ +} + +// Routine Description: +// - This routine is called to complete a direct read that blocked in +// ReadInputBuffer. The context of the read was saved in the DirectReadData +// structure. This routine is called when events have been written to +// the input buffer. It is called in the context of the writing thread. +// Arguments: +// - TerminationReason - if this routine is called because a ctrl-c or +// ctrl-break was seen, this argument contains CtrlC or CtrlBreak. If +// the owning thread is exiting, it will have ThreadDying. Otherwise 0. +// - fIsUnicode - Should we return UCS-2 unicode data, or should we +// run the final data through the current Input Codepage before +// returning? +// - pReplyStatus - The status code to return to the client +// application that originally called the API (before it was queued to +// wait) +// - pNumBytes - not used +// - pControlKeyState - For certain types of reads, this specifies +// which modifier keys were held. +// - pOutputData - a pointer to a +// std::deque> that is used to the read +// input events back to the server +// Return Value: +// - true if the wait is done and result buffer/status code can be sent back to the client. +// - false if we need to continue to wait until more data is available. +bool DirectReadData::Notify(const WaitTerminationReason TerminationReason, + const bool fIsUnicode, + _Out_ NTSTATUS* const pReplyStatus, + _Out_ size_t* const pNumBytes, + _Out_ DWORD* const pControlKeyState, + _Out_ void* const pOutputData) +{ + FAIL_FAST_IF_NULL(pOutputData); + + FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); + + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + FAIL_FAST_IF(!gci.IsConsoleLocked()); + + *pReplyStatus = STATUS_SUCCESS; + *pControlKeyState = 0; + *pNumBytes = 0; + bool retVal = true; + std::deque> readEvents; + + // If ctrl-c or ctrl-break was seen, ignore it. + if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) + { + return false; + } + + // check if a partial byte is already stored that we should read + if (!fIsUnicode && + _pInputBuffer->IsReadPartialByteSequenceAvailable() && + _eventReadCount == 1) + { + _partialEvents.push_back(_pInputBuffer->FetchReadPartialByteSequence(false)); + } + + // See if called by CsrDestroyProcess or CsrDestroyThread + // via ConsoleNotifyWaitBlock. If so, just decrement the ReadCount and return. + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) + { + *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + } + // We must see if we were woken up because the handle is being + // closed. If so, we decrement the read count. If it goes to + // zero, we wake up the close thread. Otherwise, we wake up any + // other thread waiting for data. + else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) + { + *pReplyStatus = STATUS_ALERTED; + } + else + { + // if we get to here, this routine was called either by the input + // thread or a write routine. both of these callers grab the + // current console lock. + + // calculate how many events we need to read + size_t amountAlreadyRead; + if (FAILED(SizeTAdd(_partialEvents.size(), _outEvents.size(), &amountAlreadyRead))) + { + *pReplyStatus = STATUS_INTEGER_OVERFLOW; + return retVal; + } + size_t amountToRead; + if (FAILED(SizeTSub(_eventReadCount, amountAlreadyRead, &amountToRead))) + { + *pReplyStatus = STATUS_INTEGER_OVERFLOW; + return retVal; + } + + *pReplyStatus = _pInputBuffer->Read(readEvents, + amountToRead, + false, + false, + fIsUnicode, + false); + + if (*pReplyStatus == CONSOLE_STATUS_WAIT) + { + retVal = false; + } + } + + if (*pReplyStatus != CONSOLE_STATUS_WAIT) + { + // split key events to oem chars if necessary + if (*pReplyStatus == STATUS_SUCCESS && !fIsUnicode) + { + try + { + SplitToOem(readEvents); + } + CATCH_LOG(); + } + + // combine partial and whole events + while (!_partialEvents.empty()) + { + readEvents.push_front(std::move(_partialEvents.back())); + _partialEvents.pop_back(); + } + + // move read events to out storage + for (size_t i = 0; i < _eventReadCount; ++i) + { + if (readEvents.empty()) + { + break; + } + _outEvents.push_back(std::move(readEvents.front())); + readEvents.pop_front(); + } + + // store partial event if necessary + if (!readEvents.empty()) + { + _pInputBuffer->StoreReadPartialByteSequence(std::move(readEvents.front())); + readEvents.pop_front(); + FAIL_FAST_IF(!(readEvents.empty())); + } + + // move events to pOutputData + std::deque>* const pOutputDeque = reinterpret_cast>* const>(pOutputData); + *pNumBytes = _outEvents.size() * sizeof(INPUT_RECORD); + pOutputDeque->swap(_outEvents); + } + return retVal; +} diff --git a/src/host/readDataDirect.hpp b/src/host/readDataDirect.hpp new file mode 100644 index 000000000..925d53e19 --- /dev/null +++ b/src/host/readDataDirect.hpp @@ -0,0 +1,55 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- readDataDirect.hpp + +Abstract: +- This file defines the read data structure for IInputEvent-reading console APIs. +- A direct read specifically means that we are returning multiplexed input data stored + in the internal console buffers that could have originated from any type of input device. + This is not strictly string/text information but could also be mouse moves, screen changes, etc. +- Specifically this refers to ReadConsoleInput A/W and PeekConsoleInput A/W + +Author: +- Austin Diviness (AustDi) 1-Mar-2017 +- Michael Niksa (MiNiksa) 1-Mar-2017 + +Revision History: +- Pulled from original code by - KazuM Apr.19.1996 +- Separated from directio.h/directio.cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "readData.hpp" +#include "../types/inc/IInputEvent.hpp" +#include +#include + + +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); + + DirectReadData(DirectReadData&&) = default; + + ~DirectReadData() override; + + 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) override; + +private: + const size_t _eventReadCount; + std::deque> _partialEvents; + std::deque> _outEvents; +}; diff --git a/src/host/readDataRaw.cpp b/src/host/readDataRaw.cpp new file mode 100644 index 000000000..b5d930f8c --- /dev/null +++ b/src/host/readDataRaw.cpp @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "readDataRaw.hpp" +#include "dbcs.h" +#include "stream.h" +#include "../types/inc/GlyphWidth.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Routine Description: +// - Constructs raw read data class to hold context across sessions +// generally when there's not enough data to return. +// Arguments: +// - pInputBuffer - Buffer that data will be read from. +// - pInputReadHandleData - Context stored across calls from the same +// input handle to return partial data appropriately. +// - BufferSize - The amount of client byte space available for +// returning information. +// - BufPtr - Pointer to the client space available for returning +// information (BufferSize is *2 of this count because it's wchar_t) +// Return Value: +// - THROW: Throws E_INVALIDARG for invalid pointers, if BufferSize is zero or if +// it's not divisible by the size of a wchar +RAW_READ_DATA::RAW_READ_DATA(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + const size_t BufferSize, + _In_ WCHAR* const BufPtr) : + ReadData(pInputBuffer, pInputReadHandleData), + _BufferSize{ BufferSize }, + _BufPtr{ THROW_HR_IF_NULL(E_INVALIDARG, BufPtr) } +{ + THROW_HR_IF(E_INVALIDARG, _BufferSize % sizeof(wchar_t) != 0); + THROW_HR_IF(E_INVALIDARG, _BufferSize == 0); +} + +// Routine Description: +// - Destructs a read data class. +// - Decrements count of readers waiting on the given handle. +RAW_READ_DATA::~RAW_READ_DATA() +{ + +} + +// Routine Description: +// - This routine is called to complete a raw read that blocked in ReadInputBuffer. +// - The context of the read was saved in the RawReadData structure. +// - This routine is called when events have been written to the input buffer. +// - It is called in the context of the writing thread. +// - It will be called at most once per read. +// Arguments: +// - TerminationReason - if this routine is called because a ctrl-c or +// ctrl-break was seen, this argument contains CtrlC or CtrlBreak. If +// the owning thread is exiting, it will have ThreadDying. Otherwise 0. +// - fIsUnicode - Whether to convert the final data to A (using +// Console Input CP) at the end or treat everything as Unicode (UCS-2) +// - pReplyStatus - The status code to return to the client +// application that originally called the API (before it was queued to +// wait) +// - pNumBytes - The number of bytes of data that the server/driver +// will need to transmit back to the client process +// - pControlKeyState - For certain types of reads, this specifies +// which modifier keys were held. +// - pOutputData - not used +// Return Value: +// - true if the wait is done and result buffer/status code can be +// sent back to the client. +// - false if we need to continue to wait until more data is +// available. +bool RAW_READ_DATA::Notify(const WaitTerminationReason TerminationReason, + const bool fIsUnicode, + _Out_ NTSTATUS* const pReplyStatus, + _Out_ size_t* const pNumBytes, + _Out_ DWORD* const pControlKeyState, + _Out_ void* const /*pOutputData*/) +{ + // This routine should be called by a thread owning the same lock + // on the same console as we're reading from. + FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); + + FAIL_FAST_IF(!ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + + *pReplyStatus = STATUS_SUCCESS; + *pControlKeyState = 0; + + + *pNumBytes = 0; + size_t NumBytes = 0; + + PWCHAR lpBuffer; + bool RetVal = true; + bool fAddDbcsLead = false; + bool fSkipFinally = false; + + // If a ctrl-c is seen, don't terminate read. If ctrl-break is seen, terminate read. + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlC)) + { + return false; + } + else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::CtrlBreak)) + { + *pReplyStatus = STATUS_ALERTED; + } + // See if we were called because the thread that owns this wait block is exiting. + else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) + { + *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + } + // We must see if we were woken up because the handle is being + // closed. If so, we decrement the read count. If it goes to zero, + // we wake up the close thread. Otherwise, we wake up any other + // thread waiting for data. + else if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) + { + *pReplyStatus = STATUS_ALERTED; + } + else + { + // If we get to here, this routine was called either by the input + // thread or a write routine. Both of these callers grab the current + // console lock. + + lpBuffer = _BufPtr; + + if (!fIsUnicode && _pInputBuffer->IsReadPartialByteSequenceAvailable()) + { + std::unique_ptr event = _pInputBuffer->FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *lpBuffer = static_cast(pKeyEvent->GetCharData()); + _BufferSize -= sizeof(wchar_t); + *pReplyStatus = STATUS_SUCCESS; + fAddDbcsLead = true; + + if (_BufferSize == 0) + { + *pNumBytes = 1; + RetVal = false; + fSkipFinally = true; + } + } + else + { + // This call to GetChar may block. + *pReplyStatus = GetChar(_pInputBuffer, + lpBuffer, + true, + nullptr, + nullptr, + nullptr); + } + + if (!NT_SUCCESS(*pReplyStatus) || fSkipFinally) + { + if (*pReplyStatus == CONSOLE_STATUS_WAIT) + { + RetVal = false; + } + } + else + { + NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1; + lpBuffer++; + *pNumBytes += sizeof(WCHAR); + while (*pNumBytes < _BufferSize) + { + // This call to GetChar won't block. + *pReplyStatus = GetChar(_pInputBuffer, + lpBuffer, + false, + nullptr, + nullptr, + nullptr); + if (!NT_SUCCESS(*pReplyStatus)) + { + *pReplyStatus = STATUS_SUCCESS; + break; + } + NumBytes += IsGlyphFullWidth(*lpBuffer) ? 2 : 1; + lpBuffer++; + *pNumBytes += sizeof(WCHAR); + } + } + } + + // If the read was completed (status != wait), free the raw read data. + if (*pReplyStatus != CONSOLE_STATUS_WAIT && + !fSkipFinally && + !fIsUnicode) + { + // It's ansi, so translate the string. + std::unique_ptr tempBuffer; + try + { + tempBuffer = std::make_unique(NumBytes); + } + catch (...) + { + return true; + } + + lpBuffer = _BufPtr; + std::unique_ptr partialEvent; + + *pNumBytes = TranslateUnicodeToOem(lpBuffer, + gsl::narrow(*pNumBytes / sizeof(wchar_t)), + tempBuffer.get(), + gsl::narrow(NumBytes), + partialEvent); + if (partialEvent.get()) + { + _pInputBuffer->StoreReadPartialByteSequence(std::move(partialEvent)); + } + + memmove(lpBuffer, tempBuffer.get(), *pNumBytes); + if (fAddDbcsLead) + { + (*pNumBytes)++; + } + } + return RetVal; +} diff --git a/src/host/readDataRaw.hpp b/src/host/readDataRaw.hpp new file mode 100644 index 000000000..a62dcce2d --- /dev/null +++ b/src/host/readDataRaw.hpp @@ -0,0 +1,50 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- readDataRaw.hpp + +Abstract: +- This file defines the read data structure for char-reading console APIs. +- Raw reads specifically refer to when the client application just wants text data with no edit line. +- The data struct will help store context across multiple calls or in the case of a wait condition. +- Wait conditions happen pretty much only when we don't have enough text (keyboard) data to return to the client. +- This can be triggered via ReadConsole A/W and ReadFile A/W calls. + +Author: +- Austin Diviness (AustDi) 1-Mar-2017 +- Michael Niksa (MiNiksa) 1-Mar-2017 + +Revision History: +- Pulled from original authoring by Therese Stowell (ThereseS) 6-Nov-1990 +- Separated from stream.h/stream.cpp (AustDi, 2017) +--*/ + +#pragma once + +#include "readData.hpp" + +class RAW_READ_DATA final : public ReadData +{ +public: + RAW_READ_DATA(_In_ InputBuffer* const pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + const size_t BufferSize, + _In_ WCHAR* const BufPtr); + + ~RAW_READ_DATA() override; + + RAW_READ_DATA(RAW_READ_DATA&&) = default; + + 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) override; + +private: + size_t _BufferSize; + PWCHAR _BufPtr; +}; diff --git a/src/host/registry.cpp b/src/host/registry.cpp new file mode 100644 index 000000000..6b7ff5b08 --- /dev/null +++ b/src/host/registry.cpp @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "registry.hpp" + +#include "dbcs.h" +#include "srvinit.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +#define SET_FIELD_AND_SIZE(x) FIELD_OFFSET(Settings, (x)), RTL_FIELD_SIZE(Settings, (x)) + +Registry::Registry(_In_ Settings* const pSettings) : + _pSettings(pSettings) +{ + +} + +Registry::~Registry() +{ + +} + +// Routine Description: +// - Reads extended edit keys and related registry information into the global state. +// Arguments: +// - hConsoleKey - The console subkey to use for querying. +// Return Value: +// - +void Registry::GetEditKeys(_In_opt_ HKEY hConsoleKey) const +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + NTSTATUS Status; + HKEY hCurrentUserKey = nullptr; + if (hConsoleKey == nullptr) + { + Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); + if (!NT_SUCCESS(Status)) + { + return; + } + } + + // determine whether the user wants to allow alt-f4 to close the console (global setting) + DWORD dwValue; + Status = RegistrySerialization::s_QueryValue(hConsoleKey, + CONSOLE_REGISTRY_ALLOW_ALTF4_CLOSE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)& dwValue, + nullptr); + if (NT_SUCCESS(Status) && dwValue <= 1) + { + gci.SetAltF4CloseAllowed(!!dwValue); + } + + // --- START LOAD BEARING CODE --- + // NOTE: Because of some accident of history (win2k time or before) the key type of + // CONSOLE_REGISTRY_WORD_DELIM was set to REG_DWORD when it should have been REG_SZ. Registry key reads + // didn't use to be type checked so the key was "read" and the buffer it was read into was used instead of + // the default word delimiters. Meaning that it really just turned off the word delimiter feature entirely + // because the buffer was still zero'd out (except for the space character which is handled differently + // and not modifiable by this registry setting). + // + // In order to maintain compatibility with this long-standing behavior we need set the word delimiters + // based on three scenarios: + // 1. key type is REG_DWORD: have no word delimiters + // 2. key type is REG_SZ: someone has specified custom word delimiters, use them + // 3. key doesn't exist: use the original default word delimiters + // + // The space character is always considered a word delimiter, no matter the scenario. + // + // Read word delimiters from registry + auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; + delimiters.clear(); + Status = RegistrySerialization::s_QueryValue(hConsoleKey, + CONSOLE_REGISTRY_WORD_DELIM, + sizeof(dwValue), + REG_DWORD, + reinterpret_cast(&dwValue), + nullptr); + if (!NT_SUCCESS(Status)) + { + // the key isn't a REG_DWORD, try to read it as a REG_SZ + const size_t bufferSize = 64; + WCHAR awchBuffer[bufferSize]; + DWORD cbWritten = 0; + Status = RegistrySerialization::s_QueryValue(hConsoleKey, + CONSOLE_REGISTRY_WORD_DELIM, + bufferSize * sizeof(WCHAR), + REG_SZ, + reinterpret_cast(awchBuffer), + &cbWritten); + if (NT_SUCCESS(Status)) + { + // we read something, set it as the word delimiters + const std::wstring regWordDelimiters{ awchBuffer, cbWritten / sizeof(wchar_t) }; + for (const wchar_t wch : regWordDelimiters) + { + if (wch == '\0') + { + break; + } + delimiters.push_back(wch); + } + } + else + { + // the key isn't a REG_DWORD or a REG_SZ, fall back to our default word delimiters + delimiters = { '\\', '+', '!', ':', '=', '/', '.', '<', '>', ';', '|', '&' }; + } + } + // --- END LOAD BEARING CODE --- + + if (hCurrentUserKey) + { + RegCloseKey((HKEY)hConsoleKey); + RegCloseKey((HKEY)hCurrentUserKey); + } +} + +void Registry::_LoadMappedProperties(_In_reads_(cPropertyMappings) const RegistrySerialization::RegPropertyMap* const rgPropertyMappings, + const size_t cPropertyMappings, + const HKEY hKey) +{ + // Iterate through properties table and load each setting for common property types + for (UINT iMapping = 0; iMapping < cPropertyMappings; iMapping++) + { + const RegistrySerialization::RegPropertyMap* const pPropMap = &(rgPropertyMappings[iMapping]); + + NTSTATUS Status = STATUS_SUCCESS; + + switch (pPropMap->propertyType) + { + case RegistrySerialization::_RegPropertyType::Boolean: + case RegistrySerialization::_RegPropertyType::Dword: + case RegistrySerialization::_RegPropertyType::Word: + case RegistrySerialization::_RegPropertyType::Byte: + case RegistrySerialization::_RegPropertyType::Coordinate: + { + + Status = RegistrySerialization::s_LoadRegDword(hKey, pPropMap, _pSettings); + break; + } + case RegistrySerialization::_RegPropertyType::String: + { + Status = RegistrySerialization::s_LoadRegString(hKey, pPropMap, _pSettings); + break; + } + } + + // Don't log "file not found" messages. It's fine to not find a registry key. Log other types. + if (!NT_SUCCESS(Status) && NTSTATUS_FROM_WIN32(ERROR_FILE_NOT_FOUND) != Status) + { + LOG_NTSTATUS(Status); + } + } +} + +// Routine Description: +// - Read settings that apply to all console instances from the registry. +// Arguments: +// - +// Return Value: +// - +void Registry::LoadGlobalsFromRegistry() +{ + HKEY hCurrentUserKey; + HKEY hConsoleKey; + NTSTATUS status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); + + if (NT_SUCCESS(status)) + { + _LoadMappedProperties(RegistrySerialization::s_GlobalPropMappings, RegistrySerialization::s_GlobalPropMappingsSize, hConsoleKey); + + RegCloseKey((HKEY)hConsoleKey); + RegCloseKey((HKEY)hCurrentUserKey); + } +} + +// Routine Description: +// - Reads default settings from the registry into the current console state. +// Arguments: +// - +// Return Value: +// - +void Registry::LoadDefaultFromRegistry() +{ + LoadFromRegistry(L""); +} + +// Routine Description: +// - Reads settings from the registry into the current console state. +// Arguments: +// - pwszConsoleTitle - Name of the console subkey to open. Empty string for the default console settings. +// Return Value: +// - +void Registry::LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle) +{ + HKEY hCurrentUserKey; + HKEY hConsoleKey; + NTSTATUS Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); + if (!NT_SUCCESS(Status)) + { + return; + } + + // Open the console title subkey. + LPWSTR TranslatedConsoleTitle = TranslateConsoleTitle(pwszConsoleTitle, TRUE, TRUE); + if (TranslatedConsoleTitle == nullptr) + { + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + return; + } + + HKEY hTitleKey; + Status = RegistrySerialization::s_OpenKey(hConsoleKey, TranslatedConsoleTitle, &hTitleKey); + delete[] TranslatedConsoleTitle; + TranslatedConsoleTitle = nullptr; + + if (!NT_SUCCESS(Status)) + { + TranslatedConsoleTitle = TranslateConsoleTitle(pwszConsoleTitle, TRUE, FALSE); + + if (TranslatedConsoleTitle == nullptr) + { + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + return; + } + + Status = RegistrySerialization::s_OpenKey(hConsoleKey, TranslatedConsoleTitle, &hTitleKey); + delete[] TranslatedConsoleTitle; + TranslatedConsoleTitle = nullptr; + } + + if (!NT_SUCCESS(Status)) + { + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + return; + } + + // Iterate through properties table and load each setting for common property types + _LoadMappedProperties(RegistrySerialization::s_PropertyMappings, RegistrySerialization::s_PropertyMappingsSize, hTitleKey); + + // Now load complex properties + // Some properties shouldn't be filled by the registry if a copy already exists from the process start information. + DWORD dwValue; + + // Window Origin Autopositioning Setting + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_WINDOWPOS, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + nullptr); + + if (NT_SUCCESS(Status)) + { + // The presence of a position key means autopositioning is false. + _pSettings->SetAutoPosition(FALSE); + } + // The absence of the window position key means that autopositioning is true, + // HOWEVER, the defaults might not have been auto-pos, so don't assume that they are. + + // Code Page + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CODEPAGE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)& dwValue, + nullptr); + if (NT_SUCCESS(Status)) + { + _pSettings->SetCodePage(dwValue); + + // If this routine specified default settings for console property, + // then make sure code page value when East Asian environment. + // If code page value does not the same to OEMCP and any EA's code page then + // we are override code page value to OEMCP on default console property. + // Because, East Asian environment has limitation that doesn not switch to + // another EA's code page by the SetConsoleCP/SetConsoleOutputCP. + // + // Compare of pwszConsoleTitle and L"" has limit to default property of console. + // It means, this code doesn't care user defined property. + // Content of user defined property has responsibility to themselves. + if (wcscmp(pwszConsoleTitle, L"") == 0 && + IsAvailableEastAsianCodePage(_pSettings->GetCodePage()) && + ServiceLocator::LocateGlobals().uiOEMCP != _pSettings->GetCodePage()) + { + _pSettings->SetCodePage(ServiceLocator::LocateGlobals().uiOEMCP); + } + } + + // Color table + for (DWORD i = 0; i < COLOR_TABLE_SIZE; i++) + { + WCHAR awchBuffer[64]; + StringCchPrintfW(awchBuffer, ARRAYSIZE(awchBuffer), CONSOLE_REGISTRY_COLORTABLE, i); + Status = RegistrySerialization::s_QueryValue(hTitleKey, + awchBuffer, + sizeof(dwValue), + REG_DWORD, + (PBYTE)& dwValue, + nullptr); + if (NT_SUCCESS(Status)) + { + _pSettings->SetColorTableEntry(i, dwValue); + } + } + + GetEditKeys(hConsoleKey); + + // Close the registry keys + RegCloseKey(hTitleKey); + + // These could be equal if there was no title. Don't try to double close. + if (hTitleKey != hConsoleKey) + { + RegCloseKey(hConsoleKey); + } + + RegCloseKey(hCurrentUserKey); +} diff --git a/src/host/registry.hpp b/src/host/registry.hpp new file mode 100644 index 000000000..6b89f70ae --- /dev/null +++ b/src/host/registry.hpp @@ -0,0 +1,42 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- registry.hpp + +Abstract: +- This module is used for reading/writing registry operations + +Author(s): +- Michael Niksa (MiNiksa) 23-Jul-2014 +- Paul Campbell (PaulCam) 23-Jul-2014 + +Revision History: +- From components of srvinit.c +--*/ + +#pragma once + +#include "precomp.h" + +class Registry +{ +public: + Registry(_In_ Settings* const pSettings); + ~Registry(); + + void LoadGlobalsFromRegistry(); + void LoadDefaultFromRegistry(); + void LoadFromRegistry(_In_ PCWSTR const pwszConsoleTitle); + + + void GetEditKeys(_In_opt_ HKEY hConsoleKey) const; +private: + void _LoadMappedProperties(_In_reads_(cPropertyMappings) const RegistrySerialization::RegPropertyMap* const rgPropertyMappings, + const size_t cPropertyMappings, + const HKEY hKey); + + + Settings* const _pSettings; +}; diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp new file mode 100644 index 000000000..5c808579d --- /dev/null +++ b/src/host/renderData.cpp @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "renderData.hpp" + +#include "dbcs.h" +#include "handle.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Retrieves the viewport that applies over the data available in the GetTextBuffer() call +// Return Value: +// - Viewport describing rectangular region of TextBuffer that should be displayed. +Microsoft::Console::Types::Viewport RenderData::GetViewport() noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().GetViewport(); +} + +// Routine Description: +// - Provides access to the text data that can be presented. Check GetViewport() for +// the appropriate windowing. +// Return Value: +// - Text buffer with cell information for display +const TextBuffer& RenderData::GetTextBuffer() noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().GetTextBuffer(); +} + +// Routine Description: +// - Describes which font should be used for presenting text +// Return Value: +// - Font description structure +const FontInfo& RenderData::GetFontInfo() noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().GetCurrentFont(); +} + +// Routine Description: +// - Retrieves the brush colors that should be used in absence of any other color data from +// cells in the text buffer. +// Return Value: +// - TextAttribute containing the foreground and background brush color data. +const TextAttribute RenderData::GetDefaultBrushColors() noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().GetAttributes(); +} + +// Method Description: +// - Gets the cursor's position in the buffer, relative to the buffer origin. +// Arguments: +// - +// Return Value: +// - the cursor's position in the buffer relative to the buffer origin. +COORD RenderData::GetCursorPosition() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + return cursor.GetPosition(); +} + +// Method Description: +// - Returns whether the cursor is currently visible or not. If the cursor is +// visible and blinking, this is true, even if the cursor has currently +// blinked to the "off" state. +// Arguments: +// - +// Return Value: +// - true if the cursor is set to the visible state, regardless of blink state +bool RenderData::IsCursorVisible() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + return cursor.IsVisible() && !cursor.IsPopupShown(); +} + +// Method Description: +// - Returns whether the cursor is currently visually visible or not. If the +// cursor is visible, and blinking, this will alternate between true and +// false as the cursor blinks. +// Arguments: +// - +// Return Value: +// - true if the cursor is currently visually visible, depending upon blink state +bool RenderData::IsCursorOn() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + return cursor.IsVisible() && cursor.IsOn(); +} + +// Method Description: +// - The height of the cursor, out of 100, where 100 indicates the cursor should +// be the full height of the cell. +// Arguments: +// - +// Return Value: +// - height of the cursor, out of 100 +ULONG RenderData::GetCursorHeight() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + // Determine cursor height + ULONG ulHeight = cursor.GetSize(); + + // Now adjust the height for the overwrite/insert mode. If we're in overwrite mode, IsDouble will be set. + // When IsDouble is set, we either need to double the height of the cursor, or if it's already too big, + // then we need to shrink it by half. + if (cursor.IsDouble()) + { + if (ulHeight > 50) // 50 because 50 percent is half of 100 percent which is the max size. + { + ulHeight >>= 1; + } + else + { + ulHeight <<= 1; + } + } + + return ulHeight; +} + +// Method Description: +// - The CursorType of the cursor. The CursorType is used to determine what +// shape the cursor should be. +// Arguments: +// - +// Return Value: +// - the CursorType of the cursor. +CursorType RenderData::GetCursorStyle() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + return cursor.GetType(); +} + +// Method Description: +// - Retrieves the operating system preference from Ease of Access for the pixel +// width of the cursor. Useful for a bar-style cursor. +// Arguments: +// - +// Return Value: +// - The suggested width of the cursor in pixels. +ULONG RenderData::GetCursorPixelWidth() const noexcept +{ + return ServiceLocator::LocateGlobals().cursorPixelWidth; +} + +// Method Description: +// - Get the color of the cursor. If the color is INVALID_COLOR, the cursor +// should be drawn by inverting the color of the cursor. +// Arguments: +// - +// Return Value: +// - the color of the cursor. +COLORREF RenderData::GetCursorColor() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); + return cursor.GetColor(); +} + +// Routine Description: +// - Retrieves overlays to be drawn on top of the main screen buffer area. +// - Overlays are drawn from first to last +// (the highest overlay should be given last) +// Return Value: +// - Iterable set of overlays +const std::vector RenderData::GetOverlays() const noexcept +{ + std::vector overlays; + + try + { + // First retrieve the IME information and build overlays. + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& ime = gci.ConsoleIme; + + for (const auto& composition : ime.ConvAreaCompStr) + { + // Only send the overlay to the renderer on request if it's not supposed to be hidden at this moment. + if (!composition.IsHidden()) + { + // This is holding the data. + const auto& textBuffer = composition.GetTextBuffer(); + + // The origin of the text buffer above (top left corner) is supposed to sit at this + // point within the visible viewport of the current window. + const auto origin = composition.GetAreaBufferInfo().coordConView; + + // This is the area of the viewport that is actually in use relative to the text buffer itself. + // (e.g. 0,0 is the origin of the text buffer above, not the placement within the visible viewport) + const auto used = Viewport::FromInclusive(composition.GetAreaBufferInfo().rcViewCaWindow); + + overlays.emplace_back(RenderOverlay{ textBuffer, origin, used }); + } + } + } + CATCH_LOG(); + + return overlays; +} + +// Method Description: +// - Returns true if the cursor should be drawn twice as wide as usual because +// the cursor is currently over a cell with a double-wide character in it. +// Arguments: +// - +// Return Value: +// - true if the cursor should be drawn twice as wide as usual +bool RenderData::IsCursorDoubleWidth() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().CursorIsDoubleWidth(); +} + +// Method Description: +// - Retrieves one rectangle per line describing the area of the viewport +// that should be highlighted in some way to represent a user-interactive selection +// Return Value: +// - Vector of Viewports describing the area selected +std::vector RenderData::GetSelectionRects() noexcept +{ + std::vector result; + + try + { + for (const auto& select : Selection::Instance().GetSelectionRects()) + { + result.emplace_back(Viewport::FromInclusive(select)); + } + } + CATCH_LOG(); + + return result; +} + +// Routine Description: +// - Checks the user preference as to whether grid line drawing is allowed around the edges of each cell. +// - This is for backwards compatibility with old behaviors in the legacy console. +// Return Value: +// - If true, line drawing information retrieved from the text buffer can/should be displayed. +// - If false, it should be ignored and never drawn +const bool RenderData::IsGridLineDrawingAllowed() noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // If virtual terminal output is set, grid line drawing is a must. It is always allowed. + if (WI_IsFlagSet(gci.GetActiveOutputBuffer().OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + return true; + } + else + { + // If someone explicitly asked for worldwide line drawing, enable it. + if (gci.IsGridRenderingAllowedWorldwide()) + { + return true; + } + else + { + // Otherwise, for compatibility reasons with legacy applications that used the additional CHAR_INFO bits by accident or for their own purposes, + // we must enable grid line drawing only in a DBCS output codepage. (Line drawing historically only worked in DBCS codepages.) + // The only known instance of this is Image for Windows by TeraByte, Inc. (TeryByte Unlimited) which used the bits accidentally and for no purpose + // (according to the app developer) in conjunction with the Borland Turbo C cgscrn library. + return !!IsAvailableEastAsianCodePage(gci.OutputCP); + } + } +} + +// Routine Description: +// - Retrieves the title information to be displayed in the frame/edge of the window +// Return Value: +// - String with title information +const std::wstring RenderData::GetConsoleTitle() const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetTitleAndPrefix(); +} + +// Routine Description: +// - Converts a text attribute into the foreground RGB value that should be presented, applying +// relevant table translation information and preferences. +// Return Value: +// - ARGB color value +const COLORREF RenderData::GetForegroundColor(const TextAttribute& attr) const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.LookupForegroundColor(attr); +} + +// Routine Description: +// - Converts a text attribute into the background RGB value that should be presented, applying +// relevant table translation information and preferences. +// Return Value: +// - ARGB color value +const COLORREF RenderData::GetBackgroundColor(const TextAttribute& attr) const noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.LookupBackgroundColor(attr); +} + +// Method Description: +// - Lock the console for reading the contents of the buffer. Ensures that the +// contents of the console won't be changed in the middle of a paint +// operation. +// Callers should make sure to also call RenderData::UnlockConsole once +// they're done with any querying they need to do. +void RenderData::LockConsole() noexcept +{ + ::LockConsole(); +} + +// Method Description: +// - Unlocks the console after a call to RenderData::LockConsole. +void RenderData::UnlockConsole() noexcept +{ + ::UnlockConsole(); +} diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp new file mode 100644 index 000000000..588e6dafb --- /dev/null +++ b/src/host/renderData.hpp @@ -0,0 +1,52 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- renderData.hpp + +Abstract: +- This method provides an interface for rendering the final display based on the current console state + +Author(s): +- Michael Niksa (miniksa) Nov 2015 +--*/ + +#pragma once + +#include "..\renderer\inc\IRenderData.hpp" + +using namespace Microsoft::Console::Render; + +class RenderData final : public IRenderData +{ +public: + Microsoft::Console::Types::Viewport GetViewport() noexcept override; + const TextBuffer& GetTextBuffer() noexcept override; + const FontInfo& GetFontInfo() noexcept override; + const TextAttribute GetDefaultBrushColors() noexcept override; + + const COLORREF GetForegroundColor(const TextAttribute& attr) const noexcept override; + const COLORREF GetBackgroundColor(const TextAttribute& attr) const noexcept override; + + COORD GetCursorPosition() const noexcept override; + bool IsCursorVisible() const noexcept override; + bool IsCursorOn() const noexcept override; + ULONG GetCursorHeight() const noexcept override; + CursorType GetCursorStyle() const noexcept override; + ULONG GetCursorPixelWidth() const noexcept override; + COLORREF GetCursorColor() const noexcept override; + bool IsCursorDoubleWidth() const noexcept override; + + const std::vector GetOverlays() const noexcept override; + + const bool IsGridLineDrawingAllowed() noexcept override; + + std::vector GetSelectionRects() noexcept override; + + const std::wstring GetConsoleTitle() const noexcept override; + + void LockConsole() noexcept override; + void UnlockConsole() noexcept override; + +}; diff --git a/src/host/renderFontDefaults.cpp b/src/host/renderFontDefaults.cpp new file mode 100644 index 000000000..176c29f57 --- /dev/null +++ b/src/host/renderFontDefaults.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "renderFontDefaults.hpp" + +#pragma hdrstop + +RenderFontDefaults::RenderFontDefaults() +{ + LOG_IF_NTSTATUS_FAILED(TrueTypeFontList::s_Initialize()); +} + +RenderFontDefaults::~RenderFontDefaults() +{ + LOG_IF_FAILED(TrueTypeFontList::s_Destroy()); +} + +[[nodiscard]] +HRESULT RenderFontDefaults::RetrieveDefaultFontNameForCodepage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pwszFaceName, + const size_t cchFaceName) +{ + NTSTATUS status = TrueTypeFontList::s_SearchByCodePage(uiCodePage, pwszFaceName, cchFaceName); + return HRESULT_FROM_NT(status); +} diff --git a/src/host/renderFontDefaults.hpp b/src/host/renderFontDefaults.hpp new file mode 100644 index 000000000..dd9597c4d --- /dev/null +++ b/src/host/renderFontDefaults.hpp @@ -0,0 +1,31 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- renderFontDefaults.hpp + +Abstract: +- This provides the implementation of the interface that abstracts the lookup of default fonts from the actual rendering engine. + +Author(s): +- Michael Niksa (miniksa) Mar 2016 +--*/ + +#pragma once + +#include "..\renderer\inc\IFontDefaultList.hpp" + +using namespace Microsoft::Console::Render; + +class RenderFontDefaults sealed : public IFontDefaultList +{ +public: + RenderFontDefaults(); + ~RenderFontDefaults(); + + [[nodiscard]] + HRESULT RetrieveDefaultFontNameForCodepage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pwszFaceName, + const size_t cchFaceName); +}; diff --git a/src/host/res.rc b/src/host/res.rc new file mode 100644 index 000000000..b35e9c879 --- /dev/null +++ b/src/host/res.rc @@ -0,0 +1,96 @@ +/****************************** Module Header ******************************\ +* Module Name: res.rc +* +* Copyright (c) 1985-91, Microsoft Corporation +* +* Constants +* +* History: +* 08-21-91 Created. +\***************************************************************************/ + +#include +#include "resource.h" + +#ifndef EXTERNAL_BUILD +#include "conhost.rcv" +#include +#include +#endif + +IDI_APPICON ICON "..\\..\\..\\res\\console.ico" + +#ifndef EXTERNAL_BUILD +// Fusion default manifest (needed since we are created w/ RtlCreateUserProcess) +IDR_SYSTEM_MANIFEST RT_MANIFEST MOVEABLE PURE "SystemDefault.man" +#endif + +// +// Menus +// + +ID_CONSOLE_SYSTEMMENU MENU PRELOAD +BEGIN + MENUITEM "Mar&k\tCtrl-M", ID_CONSOLE_MARK + MENUITEM "Cop&y\tEnter", ID_CONSOLE_COPY + MENUITEM "&Paste\tCtrl-V", ID_CONSOLE_PASTE + MENUITEM "&Select All\tCtrl-A", ID_CONSOLE_SELECTALL + MENUITEM "Scro&ll", ID_CONSOLE_SCROLL + MENUITEM "&Find...\tCtrl-F", ID_CONSOLE_FIND +END + +// +// Strings +// + +STRINGTABLE PRELOAD +BEGIN +/* errors */ + + /* + * Command line editing messages + */ + + ID_CONSOLE_MSGCMDLINEF2, "Enter char to copy up to: " + ID_CONSOLE_MSGCMDLINEF4, "Enter char to delete up to: " + ID_CONSOLE_MSGCMDLINEF9, "Enter command number: " + + ID_CONSOLE_MSGMARKMODE, "Mark " + ID_CONSOLE_MSGSELECTMODE, "Select " + ID_CONSOLE_MSGSCROLLMODE, "Scroll " + + ID_CONSOLE_FMT_WINDOWTITLE, "%s%s" + +/* WIP Audit destination name */ + ID_CONSOLE_WIP_DESTINATIONNAME, "console application" + +/* Menu items that replace the standard ones. These don't have the accelerators */ + SC_CLOSE, "&Close" + +/* just menu items */ + ID_CONSOLE_CONTROL, "&Properties" + ID_CONSOLE_EDIT, "&Edit" + ID_CONSOLE_DEFAULTS, "&Defaults" +END + +// +// Dialogs +// + +ID_CONSOLE_FINDDLG DIALOG LOADONCALL MOVEABLE DISCARDABLE 30, 73, 236, 62 +STYLE WS_BORDER | WS_CAPTION | DS_MODALFRAME | WS_POPUP | DS_3DLOOK | WS_SYSMENU +CAPTION "Find" +FONT 8, "MS Shell Dlg" +BEGIN + LTEXT "Fi&nd what:", -1, 4, 8, 42, 8 + EDITTEXT ID_CONSOLE_FINDSTR, 47, 7, 128, 12, WS_GROUP | WS_TABSTOP | ES_AUTOHSCROLL + + AUTOCHECKBOX "Match &case", ID_CONSOLE_FINDCASE, 4, 42, 64, 12 + + GROUPBOX "Direction", -1, 107, 26, 68, 28, WS_GROUP + AUTORADIOBUTTON "&Up", ID_CONSOLE_FINDUP, 111, 38, 25, 12, WS_GROUP + AUTORADIOBUTTON "&Down", ID_CONSOLE_FINDDOWN, 138, 38, 35, 12 + + DEFPUSHBUTTON "&Find Next", IDOK, 182, 5, 50, 14, WS_GROUP + PUSHBUTTON "Cancel", IDCANCEL, 182, 23, 50, 14 +END diff --git a/src/host/resource.h b/src/host/resource.h new file mode 100644 index 000000000..bc6c5a660 --- /dev/null +++ b/src/host/resource.h @@ -0,0 +1,50 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- resource.h + +Abstract: +- This file contains resource identifiers for menu popups. + +Author(s): +- Michael Niksa (MiNiksa) 14-Oct-2014 +- Paul Campbell (PaulCam) 14-Oct-2014 +--*/ + +#pragma once + +// Fusion Application Manifest +#define IDR_SYSTEM_MANIFEST 100 + +// IDs of various STRINGTABLE entries +#define ID_CONSOLE_MSGCMDLINEF2 0x1008 +#define ID_CONSOLE_MSGCMDLINEF4 0x1009 +#define ID_CONSOLE_MSGCMDLINEF9 0x100A +#define ID_CONSOLE_MSGSELECTMODE 0x100B +#define ID_CONSOLE_MSGMARKMODE 0x100C +#define ID_CONSOLE_MSGSCROLLMODE 0x100D +#define ID_CONSOLE_FMT_WINDOWTITLE 0x100E +#define ID_CONSOLE_WIP_DESTINATIONNAME 0x100F + +// Menu Item strings +#define ID_CONSOLE_COPY 0xFFF0 +#define ID_CONSOLE_PASTE 0xFFF1 +#define ID_CONSOLE_MARK 0xFFF2 +#define ID_CONSOLE_SCROLL 0xFFF3 +#define ID_CONSOLE_FIND 0xFFF4 +#define ID_CONSOLE_SELECTALL 0xFFF5 +#define ID_CONSOLE_EDIT 0xFFF6 +#define ID_CONSOLE_CONTROL 0xFFF7 +#define ID_CONSOLE_DEFAULTS 0xFFF8 + +// MENU IDs +#define ID_CONSOLE_SYSTEMMENU 500 + +// DIALOG IDs +#define ID_CONSOLE_FINDDLG 600 +#define ID_CONSOLE_FINDSTR 601 +#define ID_CONSOLE_FINDCASE 602 +#define ID_CONSOLE_FINDUP 603 +#define ID_CONSOLE_FINDDOWN 604 diff --git a/src/host/runft.bat b/src/host/runft.bat new file mode 100644 index 000000000..1cec25dd3 --- /dev/null +++ b/src/host/runft.bat @@ -0,0 +1,2 @@ +te %_NTTREE%\unittests\conhost.api.tests.dll %1 %2 %3 %4 %5 %6 %7 %8 %9 +te %_NTTREE%\unittests\conhost.uia.tests.dll %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/host/runtests.bat b/src/host/runtests.bat new file mode 100644 index 000000000..57ebce485 --- /dev/null +++ b/src/host/runtests.bat @@ -0,0 +1,2 @@ +call runut.bat %1 %2 %3 %4 %5 %6 %7 %8 %9 +call runft.bat %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/host/runut.bat b/src/host/runut.bat new file mode 100644 index 000000000..7c728c7cc --- /dev/null +++ b/src/host/runut.bat @@ -0,0 +1 @@ +te %_NTTREE%\unittests\conhost.unit.tests.dll %* \ No newline at end of file diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp new file mode 100644 index 000000000..fa5337659 --- /dev/null +++ b/src/host/screenInfo.cpp @@ -0,0 +1,2932 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "screenInfo.hpp" +#include "dbcs.h" +#include "output.h" +#include "_output.h" +#include "misc.h" +#include "handle.h" +#include "../buffer/out/CharRow.hpp" + +#include +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/Viewport.hpp" +#include "../types/inc/GlyphWidth.hpp" +#include "../terminal/parser/OutputStateMachineEngine.hpp" + +#include "../types/inc/convert.hpp" + +#pragma hdrstop +using namespace Microsoft::Console; +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::Render; + +#pragma region Construct/Destruct + +SCREEN_INFORMATION::SCREEN_INFORMATION( + _In_ IWindowMetrics *pMetrics, + _In_ IAccessibilityNotifier *pNotifier, + const TextAttribute popupAttributes, + const FontInfo fontInfo) : + OutputMode{ ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT }, + ResizingWindow{ 0 }, + WheelDelta{ 0 }, + HWheelDelta{ 0 }, + _textBuffer{ nullptr }, + Next{ nullptr }, + WriteConsoleDbcsLeadByte{ 0, 0 }, + FillOutDbcsLeadChar{ 0 }, + // LineChar initialized below. + ConvScreenInfo{ nullptr }, + ScrollScale{ 1ul }, + _pConsoleWindowMetrics{ pMetrics }, + _pAccessibilityNotifier{ pNotifier }, + _stateMachine{ nullptr }, + _scrollMargins{ Viewport::FromCoord({0}) }, + _viewport(Viewport::Empty()), + _psiAlternateBuffer{ nullptr }, + _psiMainBuffer{ nullptr }, + _rcAltSavedClientNew{ 0 }, + _rcAltSavedClientOld{ 0 }, + _fAltWindowChanged{ false }, + _PopupAttributes{ popupAttributes }, + _tabStops{}, + _virtualBottom{ 0 }, + _renderTarget{ *this }, + _currentFont{ fontInfo }, + _desiredFont{ fontInfo } +{ + LineChar[0] = UNICODE_BOX_DRAW_LIGHT_DOWN_AND_RIGHT; + LineChar[1] = UNICODE_BOX_DRAW_LIGHT_DOWN_AND_LEFT; + LineChar[2] = UNICODE_BOX_DRAW_LIGHT_HORIZONTAL; + LineChar[3] = UNICODE_BOX_DRAW_LIGHT_VERTICAL; + LineChar[4] = UNICODE_BOX_DRAW_LIGHT_UP_AND_RIGHT; + LineChar[5] = UNICODE_BOX_DRAW_LIGHT_UP_AND_LEFT; + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.GetVirtTermLevel() != 0) + { + OutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } +} + +// Routine Description: +// - This routine frees the memory associated with a screen buffer. +// Arguments: +// - ScreenInfo - screen buffer data to free. +// Return Value: +// Note: +// - console handle table lock must be held when calling this routine +SCREEN_INFORMATION::~SCREEN_INFORMATION() +{ + _FreeOutputStateMachine(); +} + +// Routine Description: +// - This routine allocates and initializes the data associated with a screen buffer. +// Arguments: +// - ScreenInformation - the new screen buffer. +// - coordWindowSize - the initial size of screen buffer's window (in rows/columns) +// - nFont - the initial font to generate text with. +// - dwScreenBufferSize - the initial size of the screen buffer (in rows/columns). +// Return Value: +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::CreateInstance(_In_ COORD coordWindowSize, + const FontInfo fontInfo, + _In_ COORD coordScreenBufferSize, + const TextAttribute defaultAttributes, + const TextAttribute popupAttributes, + const UINT uiCursorSize, + _Outptr_ SCREEN_INFORMATION** const ppScreen) +{ + *ppScreen = nullptr; + + try + { + IWindowMetrics *pMetrics = ServiceLocator::LocateWindowMetrics(); + THROW_IF_NULL_ALLOC(pMetrics); + + IAccessibilityNotifier *pNotifier = ServiceLocator::LocateAccessibilityNotifier(); + THROW_IF_NULL_ALLOC(pNotifier); + + SCREEN_INFORMATION* const pScreen = new SCREEN_INFORMATION(pMetrics, pNotifier, popupAttributes, fontInfo); + + // Set up viewport + pScreen->_viewport = Viewport::FromDimensions({ 0, 0 }, + pScreen->_IsInPtyMode() ? coordScreenBufferSize : coordWindowSize); + pScreen->UpdateBottom(); + + // Set up text buffer + pScreen->_textBuffer = std::make_unique(coordScreenBufferSize, + defaultAttributes, + uiCursorSize, + pScreen->_renderTarget); + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + pScreen->_textBuffer->GetCursor().SetColor(gci.GetCursorColor()); + pScreen->_textBuffer->GetCursor().SetType(gci.GetCursorType()); + + const NTSTATUS status = pScreen->_InitializeOutputStateMachine(); + + if (NT_SUCCESS(status)) + { + *ppScreen = pScreen; + } + + LOG_IF_NTSTATUS_FAILED(status); + return status; + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } +} + +Viewport SCREEN_INFORMATION::GetBufferSize() const +{ + return _textBuffer->GetSize(); +} + +// Method Description: +// - Returns the "terminal" dimensions of this buffer. If we're in Terminal +// Scrolling mode, this will return our Y dimension as only extending up to +// the _virtualBottom. The height of the returned viewport would then be +// (number of lines in scrollback) + (number of lines in viewport). +// If we're not in teminal scrolling mode, this will return our normal buffer +// size. +// Arguments: +// - +// Return Value: +// - a viewport whos height is the height of the "terminal" portion of the +// buffer in terminal scrolling mode, and is the height of the full buffer +// in normal scrolling mode. +Viewport SCREEN_INFORMATION::GetTerminalBufferSize() const +{ + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + Viewport v = _textBuffer->GetSize(); + if (gci.IsTerminalScrolling() && v.Height() > _virtualBottom) + { + v = Viewport::FromDimensions({0, 0}, v.Width(), _virtualBottom+1); + } + return v; +} + +const StateMachine& SCREEN_INFORMATION::GetStateMachine() const +{ + return *_stateMachine; +} + +StateMachine& SCREEN_INFORMATION::GetStateMachine() +{ + return *_stateMachine; +} + +// Method Description: +// - returns true if this buffer is in Virtual Terminal Output mode. +// Arguments: +// +// Return Value: +// true iff this buffer is in Virtual Terminal Output mode. +bool SCREEN_INFORMATION::InVTMode() const +{ + return WI_IsFlagSet(OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); +} + +// Routine Description: +// - This routine inserts the screen buffer pointer into the console's list of screen buffers. +// Arguments: +// - ScreenInfo - Pointer to screen information structure. +// Return Value: +// Note: +// - The console lock must be held when calling this routine. +void SCREEN_INFORMATION::s_InsertScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + FAIL_FAST_IF(!(gci.IsConsoleLocked())); + + pScreenInfo->Next = gci.ScreenBuffers; + gci.ScreenBuffers = pScreenInfo; +} + +// Routine Description: +// - This routine removes the screen buffer pointer from the console's list of screen buffers. +// Arguments: +// - ScreenInfo - Pointer to screen information structure. +// Return Value: +// Note: +// - The console lock must be held when calling this routine. +void SCREEN_INFORMATION::s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (pScreenInfo == gci.ScreenBuffers) + { + gci.ScreenBuffers = pScreenInfo->Next; + } + else + { + auto* Cur = gci.ScreenBuffers; + auto* Prev = Cur; + while (Cur != nullptr) + { + if (pScreenInfo == Cur) + { + break; + } + + Prev = Cur; + Cur = Cur->Next; + } + + FAIL_FAST_IF_NULL(Cur); + Prev->Next = Cur->Next; + } + + if (pScreenInfo == gci.pCurrentScreenBuffer && + gci.ScreenBuffers != gci.pCurrentScreenBuffer) + { + if (gci.ScreenBuffers != nullptr) + { + SetActiveScreenBuffer(*gci.ScreenBuffers); + } + else + { + gci.pCurrentScreenBuffer = nullptr; + } + } + + delete pScreenInfo; +} + +#pragma endregion + +#pragma region Output State Machine + +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::_InitializeOutputStateMachine() +{ + try + { + auto adapter = std::make_unique(new ConhostInternalGetSet{ *this }, + new WriteBuffer{ *this }); + THROW_IF_NULL_ALLOC(adapter.get()); + + // Note that at this point in the setup, we haven't determined if we're + // in VtIo mode or not yet. We'll set the OutputStateMachine's + // TerminalConnection later, in VtIo::StartIfNeeded + _stateMachine = std::make_shared(new OutputStateMachineEngine(adapter.release())); + THROW_IF_NULL_ALLOC(_stateMachine.get()); + } + catch (...) + { + // if any part of initialization failed, free the allocated ones. + _FreeOutputStateMachine(); + + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + return STATUS_SUCCESS; +} + +// If we're an alternate buffer, we want to give the GetSet back to our main +void SCREEN_INFORMATION::_FreeOutputStateMachine() +{ + if (_psiMainBuffer == nullptr) // If this is a main buffer + { + if (_psiAlternateBuffer != nullptr) + { + s_RemoveScreenBuffer(_psiAlternateBuffer); + } + + _stateMachine.reset(); + } +} +#pragma endregion + +#pragma region IIoProvider + +// Method Description: +// - Return the active screen buffer of the console. +// Arguments: +// - +// Return Value: +// - the active screen buffer of the console. +SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveOutputBuffer() +{ + return GetActiveBuffer(); +} + +const SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveOutputBuffer() const +{ + return GetActiveBuffer(); +} + +// Method Description: +// - Return the active input buffer of the console. +// Arguments: +// - +// Return Value: +// - the active input buffer of the console. +InputBuffer* const SCREEN_INFORMATION::GetActiveInputBuffer() const +{ + return ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveInputBuffer(); +} + +#pragma endregion + +#pragma region Get Data + +bool SCREEN_INFORMATION::IsActiveScreenBuffer() const +{ + // the following macro returns TRUE if the given screen buffer is the active screen buffer. + + //#define ACTIVE_SCREEN_BUFFER(SCREEN_INFO) (gci.CurrentScreenBuffer == SCREEN_INFO) + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return (gci.pCurrentScreenBuffer == this); +} + +// Routine Description: +// - This routine returns data about the screen buffer. +// Arguments: +// - Size - Pointer to location in which to store screen buffer size. +// - CursorPosition - Pointer to location in which to store the cursor position. +// - ScrollPosition - Pointer to location in which to store the scroll position. +// - Attributes - Pointer to location in which to store the default attributes. +// - CurrentWindowSize - Pointer to location in which to store current window size. +// - MaximumWindowSize - Pointer to location in which to store maximum window size. +// Return Value: +// - None +void SCREEN_INFORMATION::GetScreenBufferInformation(_Out_ PCOORD pcoordSize, + _Out_ PCOORD pcoordCursorPosition, + _Out_ PSMALL_RECT psrWindow, + _Out_ PWORD pwAttributes, + _Out_ PCOORD pcoordMaximumWindowSize, + _Out_ PWORD pwPopupAttributes, + _Out_writes_(COLOR_TABLE_SIZE) LPCOLORREF lpColorTable) const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + *pcoordSize = GetBufferSize().Dimensions(); + + *pcoordCursorPosition = _textBuffer->GetCursor().GetPosition(); + + *psrWindow = _viewport.ToInclusive(); + + *pwAttributes = gci.GenerateLegacyAttributes(GetAttributes()); + *pwPopupAttributes = gci.GenerateLegacyAttributes(_PopupAttributes); + + // the copy length must be constant for now to keep OACR happy with buffer overruns. + memmove(lpColorTable, gci.GetColorTable(), COLOR_TABLE_SIZE * sizeof(COLORREF)); + + *pcoordMaximumWindowSize = GetMaxWindowSizeInCharacters(); +} + +// Routine Description: +// - Gets the smallest possible client area in characters. Takes the window client area and divides by the active font dimensions. +// Arguments: +// - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. +// Return Value: +// - COORD containing the width and height representing the minimum character grid that can be rendered in the window. +COORD SCREEN_INFORMATION::GetMinWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const +{ + FAIL_FAST_IF(coordFontSize.X == 0); + FAIL_FAST_IF(coordFontSize.Y == 0); + + // prepare rectangle + RECT const rcWindowInPixels = _pConsoleWindowMetrics->GetMinClientRectInPixels(); + + // assign the pixel widths and heights to the final output + COORD coordClientAreaSize; + coordClientAreaSize.X = (SHORT)RECT_WIDTH(&rcWindowInPixels); + coordClientAreaSize.Y = (SHORT)RECT_HEIGHT(&rcWindowInPixels); + + // now retrieve the font size and divide the pixel counts into character counts + COORD coordFont = coordFontSize; // by default, use the size we were given + + // If text info has been set up, instead retrieve its font size + if (_textBuffer != nullptr) + { + coordFont = GetScreenFontSize(); + } + + FAIL_FAST_IF(coordFont.X == 0); + FAIL_FAST_IF(coordFont.Y == 0); + + coordClientAreaSize.X /= coordFont.X; + coordClientAreaSize.Y /= coordFont.Y; + + return coordClientAreaSize; +} + +// Routine Description: +// - Gets the maximum client area in characters that would fit on the current monitor or given the current buffer size. +// Takes the monitor work area and divides by the active font dimensions then limits by buffer size. +// Arguments: +// - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. +// Return Value: +// - COORD containing the width and height representing the largest character +// grid that can be rendered on the current monitor and/or from the current buffer size. +COORD SCREEN_INFORMATION::GetMaxWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const +{ + FAIL_FAST_IF(coordFontSize.X == 0); + FAIL_FAST_IF(coordFontSize.Y == 0); + + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + COORD coordClientAreaSize = coordScreenBufferSize; + + // Important re: headless consoles on onecore (for telnetd, etc.) + // GetConsoleScreenBufferInfoEx hits this to get the max size of the display. + // Because we're headless, we don't really care about the max size of the display. + // In that case, we'll just return the buffer size as the "max" window size. + if (!ServiceLocator::LocateGlobals().IsHeadless()) + { + const COORD coordWindowRestrictedSize = GetLargestWindowSizeInCharacters(coordFontSize); + // If the buffer is smaller than what the max window would allow, then the max client area can only be as big as the + // buffer we have. + coordClientAreaSize.X = std::min(coordScreenBufferSize.X, coordWindowRestrictedSize.X); + coordClientAreaSize.Y = std::min(coordScreenBufferSize.Y, coordWindowRestrictedSize.Y); + } + + return coordClientAreaSize; +} + +// Routine Description: +// - Gets the largest possible client area in characters if the window were stretched as large as it could go. +// - Takes the window client area and divides by the active font dimensions. +// Arguments: +// - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. +// Return Value: +// - COORD containing the width and height representing the largest character +// grid that can be rendered on the current monitor with the maximum size window. +COORD SCREEN_INFORMATION::GetLargestWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const +{ + FAIL_FAST_IF(coordFontSize.X == 0); + FAIL_FAST_IF(coordFontSize.Y == 0); + + RECT const rcClientInPixels = _pConsoleWindowMetrics->GetMaxClientRectInPixels(); + + // first assign the pixel widths and heights to the final output + COORD coordClientAreaSize; + coordClientAreaSize.X = (SHORT)RECT_WIDTH(&rcClientInPixels); + coordClientAreaSize.Y = (SHORT)RECT_HEIGHT(&rcClientInPixels); + + // now retrieve the font size and divide the pixel counts into character counts + COORD coordFont = coordFontSize; // by default, use the size we were given + + // If renderer has been set up, instead retrieve its font size + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + coordFont = GetScreenFontSize(); + } + + FAIL_FAST_IF(coordFont.X == 0); + FAIL_FAST_IF(coordFont.Y == 0); + + coordClientAreaSize.X /= coordFont.X; + coordClientAreaSize.Y /= coordFont.Y; + + return coordClientAreaSize; +} + +COORD SCREEN_INFORMATION::GetScrollBarSizesInCharacters() const +{ + COORD coordFont = GetScreenFontSize(); + + SHORT vScrollSize = ServiceLocator::LocateGlobals().sVerticalScrollSize; + SHORT hScrollSize = ServiceLocator::LocateGlobals().sHorizontalScrollSize; + + COORD coordBarSizes; + coordBarSizes.X = (vScrollSize / coordFont.X) + ((vScrollSize % coordFont.X) != 0 ? 1 : 0); + coordBarSizes.Y = (hScrollSize / coordFont.Y) + ((hScrollSize % coordFont.Y) != 0 ? 1 : 0); + + return coordBarSizes; +} + +void SCREEN_INFORMATION::GetRequiredConsoleSizeInPixels(_Out_ PSIZE const pRequiredSize) const +{ + COORD const coordFontSize = GetCurrentFont().GetSize(); + + // TODO: Assert valid size boundaries + pRequiredSize->cx = GetViewport().Width() * coordFontSize.X; + pRequiredSize->cy = GetViewport().Height() * coordFontSize.Y; +} + +COORD SCREEN_INFORMATION::GetScreenFontSize() const +{ + // If we have no renderer, then we don't really need any sort of pixel math. so the "font size" for the scale factor + // (which is used almost everywhere around the code as * and / calls) should just be 1,1 so those operations will do + // effectively nothing. + COORD coordRet = { 1, 1 }; + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + coordRet = GetCurrentFont().GetSize(); + } + + // For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations. + coordRet.X = std::max(coordRet.X, 1i16); + coordRet.Y = std::max(coordRet.Y, 1i16); + + return coordRet; +} + +#pragma endregion + +#pragma region Set Data + +void SCREEN_INFORMATION::RefreshFontWithRenderer() +{ + if (IsActiveScreenBuffer()) + { + // Hand the handle to our internal structure to the font change trigger in case it updates it based on what's appropriate. + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender + ->TriggerFontChange(ServiceLocator::LocateGlobals().dpi, + GetDesiredFont(), + GetCurrentFont()); + } + } +} + +void SCREEN_INFORMATION::UpdateFont(const FontInfo* const pfiNewFont) +{ + FontInfoDesired fiDesiredFont(*pfiNewFont); + + GetDesiredFont() = fiDesiredFont; + + RefreshFontWithRenderer(); + + // If we're the active screen buffer... + if (IsActiveScreenBuffer()) + { + // If there is a window attached, let it know that it should try to update so the rows/columns are now accounting for the new font. + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (nullptr != pWindow) + { + COORD coordViewport = GetViewport().Dimensions(); + pWindow->UpdateWindowSize(coordViewport); + } + } + + // If we're an alt buffer, also update our main buffer. + if (_psiMainBuffer) + { + _psiMainBuffer->UpdateFont(pfiNewFont); + } +} + +// NOTE: This method was historically used to notify accessibility apps AND +// to aggregate drawing metadata to determine whether or not to use PolyTextOut. +// After the Nov 2015 graphics refactor, the metadata drawing flag calculation is no longer necessary. +// This now only notifies accessibility apps of a change. +void SCREEN_INFORMATION::NotifyAccessibilityEventing(const short sStartX, + const short sStartY, + const short sEndX, + const short sEndY) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // Fire off a winevent to let accessibility apps know what changed. + if (IsActiveScreenBuffer()) + { + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + FAIL_FAST_IF(!(sEndX < coordScreenBufferSize.X)); + + if (sStartX == sEndX && sStartY == sEndY) + { + try + { + const auto cellData = GetCellDataAt({ sStartX, sStartY }); + const LONG charAndAttr = MAKELONG(Utf16ToUcs2(cellData->Chars()), + gci.GenerateLegacyAttributes(cellData->TextAttr())); + _pAccessibilityNotifier->NotifyConsoleUpdateSimpleEvent(MAKELONG(sStartX, sStartY), + charAndAttr); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return; + } + } + else + { + _pAccessibilityNotifier->NotifyConsoleUpdateRegionEvent(MAKELONG(sStartX, sStartY), + MAKELONG(sEndX, sEndY)); + } + IConsoleWindow* pConsoleWindow = ServiceLocator::LocateConsoleWindow(); + if (pConsoleWindow) + { + LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); + // TODO MSFT 7960168 do we really need this event to not signal? + //pConsoleWindow->SignalUia(UIA_LayoutInvalidatedEventId); + } + } +} + +#pragma endregion + +#pragma region UI/Refresh + +VOID SCREEN_INFORMATION::UpdateScrollBars() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (!IsActiveScreenBuffer()) + { + return; + } + + if (gci.Flags & CONSOLE_UPDATING_SCROLL_BARS) + { + return; + } + + gci.Flags |= CONSOLE_UPDATING_SCROLL_BARS; + + if (ServiceLocator::LocateConsoleWindow() != nullptr) + { + ServiceLocator::LocateConsoleWindow()->PostUpdateScrollBars(); + } +} + +VOID SCREEN_INFORMATION::InternalUpdateScrollBars() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + + WI_ClearFlag(gci.Flags, CONSOLE_UPDATING_SCROLL_BARS); + + if (!IsActiveScreenBuffer()) + { + return; + } + + ResizingWindow++; + + if (pWindow != nullptr) + { + const auto buffer = GetBufferSize(); + + // If this is the main buffer, make sure we enable both of the scroll bars. + // The alt buffer likely disabled the scroll bars, this is the only + // way to re-enable it. + if (!_IsAltBuffer()) + { + pWindow->EnableBothScrollBars(); + } + + pWindow->UpdateScrollBar(true, + _IsAltBuffer() | gci.IsTerminalScrolling(), + _viewport.Height(), + gci.IsTerminalScrolling() ? _virtualBottom : buffer.BottomInclusive(), + _viewport.Top()); + pWindow->UpdateScrollBar(false, + _IsAltBuffer(), + _viewport.Width(), + buffer.RightInclusive(), + _viewport.Left()); + } + + // Fire off an event to let accessibility apps know the layout has changed. + _pAccessibilityNotifier->NotifyConsoleLayoutEvent(); + + ResizingWindow--; +} + +// Routine Description: +// - Modifies the size of the current viewport to match the width/height of the request given. +// - This will act like a resize operation from the bottom right corner of the window. +// Arguments: +// - pcoordSize - Requested viewport width/heights in characters +// Return Value: +// - +void SCREEN_INFORMATION::SetViewportSize(const COORD* const pcoordSize) +{ + // If this is the alt buffer or a VT I/O buffer: + // first resize ourselves to match the new viewport + // then also make sure that the main buffer gets the same call + // (if necessary) + if (_IsInPtyMode()) + { + LOG_IF_FAILED(ResizeScreenBuffer(*pcoordSize, TRUE)); + + if (_psiMainBuffer) + { + const auto bufferSize = GetBufferSize().Dimensions(); + + _psiMainBuffer->SetViewportSize(&bufferSize); + } + } + _InternalSetViewportSize(pcoordSize, false, false); +} + +// Method Description: +// - Update the origin of the buffer's viewport. You can either move the +// viewport with a delta relative to it's current location, or set it's +// absolute origin. Either way leaves the dimensions of the viewport +// unchanged. Also potentially updates our "virtual bottom", the last real +// location of the viewport in the buffer. +// Also notifies the window implementation to update it's scrollbars. +// Arguments: +// - fAbsolute: If true, coordWindowOrigin is the absolute location of the origin of the new viewport. +// If false, coordWindowOrigin is a delta to move the viewport relative to it's current position. +// - coordWindowOrigin: Either the new absolute position of the origin of the +// viewport, or a delta to add to the current viewport location. +// - updateBottom: If true, update our virtual bottom position. This should be +// false if we're moving the viewport in response to the users scrolling up +// and down in the buffer, but API calls should set this to true. +// Return Value: +// - STATUS_INVALID_PARAMETER if the new viewport would be outside the buffer, +// else STATUS_SUCCESS +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::SetViewportOrigin(const bool fAbsolute, + const COORD coordWindowOrigin, + const bool updateBottom) +{ + // calculate window size + COORD WindowSize = _viewport.Dimensions(); + + SMALL_RECT NewWindow; + // if relative coordinates, figure out absolute coords. + if (!fAbsolute) + { + if (coordWindowOrigin.X == 0 && coordWindowOrigin.Y == 0) + { + return STATUS_SUCCESS; + } + NewWindow.Left = _viewport.Left() + coordWindowOrigin.X; + NewWindow.Top = _viewport.Top() + coordWindowOrigin.Y; + } + else + { + if (coordWindowOrigin == _viewport.Origin()) + { + return STATUS_SUCCESS; + } + NewWindow.Left = coordWindowOrigin.X; + NewWindow.Top = coordWindowOrigin.Y; + } + NewWindow.Right = (SHORT)(NewWindow.Left + WindowSize.X - 1); + NewWindow.Bottom = (SHORT)(NewWindow.Top + WindowSize.Y - 1); + + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // If we're in terminal scrolling mode, and we're rying to set the viewport + // below the logical viewport, without updating our virtual bottom + // (the logical viewport's position), dont. + // Instead move us to the bottom of the logical viewport. + if (gci.IsTerminalScrolling() && !updateBottom && NewWindow.Bottom > _virtualBottom) + { + const short delta = _virtualBottom - NewWindow.Bottom; + NewWindow.Top += delta; + NewWindow.Bottom += delta; + } + + // see if new window origin would extend window beyond extent of screen buffer + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + if (NewWindow.Left < 0 || + NewWindow.Top < 0 || + NewWindow.Right < 0 || + NewWindow.Bottom < 0 || + NewWindow.Right >= coordScreenBufferSize.X || + NewWindow.Bottom >= coordScreenBufferSize.Y) + { + return STATUS_INVALID_PARAMETER; + } + + if (IsActiveScreenBuffer() && ServiceLocator::LocateConsoleWindow() != nullptr) + { + // Tell the window that it needs to set itself to the new origin if we're the active buffer. + ServiceLocator::LocateConsoleWindow()->ChangeViewport(NewWindow); + } + else + { + // Otherwise, just store the new position and go on. + _viewport = Viewport::FromInclusive(NewWindow); + Tracing::s_TraceWindowViewport(_viewport); + } + + // Update our internal virtual bottom tracker if requested. This helps keep + // the viewport's logical position consistent from the perspective of a + // VT client application, even if the user scrolls the viewport with the mouse. + if (updateBottom) + { + UpdateBottom(); + } + + return STATUS_SUCCESS; +} + +bool SCREEN_INFORMATION::SendNotifyBeep() const +{ + if (IsActiveScreenBuffer()) + { + if (ServiceLocator::LocateConsoleWindow() != nullptr) + { + return ServiceLocator::LocateConsoleWindow()->SendNotifyBeep(); + } + } + + return false; +} + +bool SCREEN_INFORMATION::PostUpdateWindowSize() const +{ + if (IsActiveScreenBuffer()) + { + if (ServiceLocator::LocateConsoleWindow() != nullptr) + { + return ServiceLocator::LocateConsoleWindow()->PostUpdateWindowSize(); + } + } + + return false; +} + +// Routine Description: +// - Modifies the screen buffer and viewport dimensions when the available client area rendering space changes. +// Arguments: +// - prcClientNew - Client rectangle in pixels after this update +// - prcClientOld - Client rectangle in pixels before this update +// Return Value: +// - +void SCREEN_INFORMATION::ProcessResizeWindow(const RECT* const prcClientNew, + const RECT* const prcClientOld) +{ + if (_IsAltBuffer()) + { + // Stash away the size of the window, we'll need to do this to the main when we pop back + // We set this on the main, so that main->alt(resize)->alt keeps the resize + _psiMainBuffer->_fAltWindowChanged = true; + _psiMainBuffer->_rcAltSavedClientNew = *prcClientNew; + _psiMainBuffer->_rcAltSavedClientOld = *prcClientOld; + } + + // 1.a In some modes, the screen buffer size needs to change on window size, + // so do that first. + // _AdjustScreenBuffer might hide the commandline. If it does so, it'll + // return S_OK instead of S_FALSE. In that case, we'll need to re-show + // the commandline ourselves once the viewport size is updated. + // (See 1.b below) + const HRESULT adjustBufferSizeResult = _AdjustScreenBuffer(prcClientNew); + LOG_IF_FAILED(adjustBufferSizeResult); + + // 2. Now calculate how large the new viewport should be + COORD coordViewportSize; + _CalculateViewportSize(prcClientNew, &coordViewportSize); + + // 3. And adjust the existing viewport to match the same dimensions. + // The old/new comparison is to figure out which side the window was resized from. + _AdjustViewportSize(prcClientNew, prcClientOld, &coordViewportSize); + + // 1.b If we did actually change the buffer size, then we need to show the + // commandline again. We hid it during _AdjustScreenBuffer, but we + // couldn't turn it back on until the Viewport was updated to the new + // size. See MSFT:19976291 + if (SUCCEEDED(adjustBufferSizeResult) && adjustBufferSizeResult != S_FALSE) + { + CommandLine& commandLine = CommandLine::Instance(); + commandLine.Show(); + } + + // 4. Finally, update the scroll bars. + UpdateScrollBars(); + + FAIL_FAST_IF(!(_viewport.Top() >= 0)); + // TODO MSFT: 17663344 - Audit call sites for this precondition. Extremely tiny offscreen windows. + //FAIL_FAST_IF(!(_viewport.IsValid())); +} + +#pragma endregion + +#pragma region Support/Calculation + +// Routine Description: +// - This helper converts client pixel areas into the number of characters that could fit into the client window. +// - It requires the buffer size to figure out whether it needs to reserve space for the scroll bars (or not). +// Arguments: +// - prcClientNew - Client region of window in pixels +// - coordBufferOld - Size of backing buffer in characters +// - pcoordClientNewCharacters - The maximum number of characters X by Y that can be displayed in the window with the given backing buffer. +// Return Value: +// - S_OK if math was successful. Check with SUCCEEDED/FAILED macro. +[[nodiscard]] +HRESULT SCREEN_INFORMATION::_AdjustScreenBufferHelper(const RECT* const prcClientNew, + const COORD coordBufferOld, + _Out_ COORD* const pcoordClientNewCharacters) +{ + // Get the font size ready. + COORD const coordFontSize = GetScreenFontSize(); + + // We cannot operate if the font size is 0. This shouldn't happen, but stop early if it does. + RETURN_HR_IF(E_NOT_VALID_STATE, 0 == coordFontSize.X || 0 == coordFontSize.Y); + + // Find out how much client space we have to work with in the new area. + SIZE sizeClientNewPixels = { 0 }; + sizeClientNewPixels.cx = RECT_WIDTH(prcClientNew); + sizeClientNewPixels.cy = RECT_HEIGHT(prcClientNew); + + // Subtract out scroll bar space if scroll bars will be necessary. + bool fIsHorizontalVisible = false; + bool fIsVerticalVisible = false; + s_CalculateScrollbarVisibility(prcClientNew, &coordBufferOld, &coordFontSize, &fIsHorizontalVisible, &fIsVerticalVisible); + + if (fIsHorizontalVisible) + { + sizeClientNewPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + } + + if (fIsVerticalVisible) + { + sizeClientNewPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + } + + // Now with the scroll bars removed, calculate how many characters could fit into the new window area. + pcoordClientNewCharacters->X = (SHORT)(sizeClientNewPixels.cx / coordFontSize.X); + pcoordClientNewCharacters->Y = (SHORT)(sizeClientNewPixels.cy / coordFontSize.Y); + + // If the new client is too tiny, our viewport will be 1x1. + pcoordClientNewCharacters->X = std::max(pcoordClientNewCharacters->X, 1i16); + pcoordClientNewCharacters->Y = std::max(pcoordClientNewCharacters->Y, 1i16); + return S_OK; +} + +// Routine Description: +// - Modifies the size of the backing text buffer when the window changes to support "intuitive" resizing modes by grabbing the window edges. +// - This function will compensate for scroll bars. +// - Buffer size changes will happen internally to this function. +// Arguments: +// - prcClientNew - Client rectangle in pixels after this update +// Return Value: +// - appropriate HRESULT +[[nodiscard]] +HRESULT SCREEN_INFORMATION::_AdjustScreenBuffer(const RECT* const prcClientNew) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // Prepare the buffer sizes. + // We need the main's size here to maintain the right scrollbar visibility. + COORD const coordBufferSizeOld = _IsAltBuffer() ? _psiMainBuffer->GetBufferSize().Dimensions() : GetBufferSize().Dimensions(); + COORD coordBufferSizeNew = coordBufferSizeOld; + + // First figure out how many characters we could fit into the new window given the old buffer size + COORD coordClientNewCharacters; + + RETURN_IF_FAILED(_AdjustScreenBufferHelper(prcClientNew, coordBufferSizeOld, &coordClientNewCharacters)); + + // If we're in wrap text mode, then we want to be fixed to the window size. So use the character calculation we just got + // to fix the buffer and window width together. + if (gci.GetWrapText()) + { + coordBufferSizeNew.X = coordClientNewCharacters.X; + } + + // Reanalyze scroll bars in case we fixed the edge together for word wrap. + // Use the new buffer client size. + RETURN_IF_FAILED(_AdjustScreenBufferHelper(prcClientNew, coordBufferSizeNew, &coordClientNewCharacters)); + + // Now reanalyze the buffer size and grow if we can fit more characters into the window no matter the console mode. + if (_IsInPtyMode()) + { + // The alt buffer always wants to be exactly the size of the screen, never more or less. + // This prevents scrollbars when you increase the alt buffer size, then decrease it. + // Can't have a buffer dimension of 0 - that'll cause divide by zeros in the future. + coordBufferSizeNew.X = std::max(coordClientNewCharacters.X, 1i16); + coordBufferSizeNew.Y = std::max(coordClientNewCharacters.Y, 1i16); + } + else + { + if (coordClientNewCharacters.X > coordBufferSizeNew.X) + { + coordBufferSizeNew.X = coordClientNewCharacters.X; + } + if (coordClientNewCharacters.Y > coordBufferSizeNew.Y) + { + coordBufferSizeNew.Y = coordClientNewCharacters.Y; + } + } + + HRESULT hr = S_FALSE; + + // Only attempt to modify the buffer if something changed. Expensive operation. + if (coordBufferSizeOld.X != coordBufferSizeNew.X || + coordBufferSizeOld.Y != coordBufferSizeNew.Y) + { + CommandLine& commandLine = CommandLine::Instance(); + + // TODO: Deleting and redrawing the command line during resizing can cause flickering. See: http://osgvsowi/658439 + // 1. Delete input string if necessary (see menu.c) + commandLine.Hide(FALSE); + _textBuffer->GetCursor().SetIsVisible(false); + + // 2. Call the resize screen buffer method (expensive) to redimension the backing buffer (and reflow) + LOG_IF_FAILED(ResizeScreenBuffer(coordBufferSizeNew, FALSE)); + + // MSFT:19976291 Don't re-show the commandline here. We need to wait for + // the viewport to also get resized before we can re-show the commandline. + // ProcessResizeWindow will call commandline.Show() for us. + _textBuffer->GetCursor().SetIsVisible(true); + + // Return S_OK, to indicate we succeeded and actually did something. + hr = S_OK; + } + + return hr; +} + +// Routine Description: +// - Calculates what width/height the viewport must have to consume all the available space in the given client area. +// - This compensates for scroll bars and will leave space in the client area for the bars if necessary. +// Arguments: +// - prcClientArea - The client rectangle in pixels of available rendering space. +// - pcoordSize - Filled with the width/height to which the viewport should be set. +// Return Value: +// - +void SCREEN_INFORMATION::_CalculateViewportSize(const RECT* const prcClientArea, _Out_ COORD* const pcoordSize) +{ + COORD const coordBufferSize = GetBufferSize().Dimensions(); + COORD const coordFontSize = GetScreenFontSize(); + + SIZE sizeClientPixels = { 0 }; + sizeClientPixels.cx = RECT_WIDTH(prcClientArea); + sizeClientPixels.cy = RECT_HEIGHT(prcClientArea); + + bool fIsHorizontalVisible; + bool fIsVerticalVisible; + s_CalculateScrollbarVisibility(prcClientArea, + &coordBufferSize, + &coordFontSize, + &fIsHorizontalVisible, + &fIsVerticalVisible); + + if (fIsHorizontalVisible) + { + sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + } + + if (fIsVerticalVisible) + { + sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + } + + pcoordSize->X = (SHORT)(sizeClientPixels.cx / coordFontSize.X); + pcoordSize->Y = (SHORT)(sizeClientPixels.cy / coordFontSize.Y); +} + +// Routine Description: +// - Modifies the size of the current viewport to match the width/height of the request given. +// - Must specify which corner to adjust from. Default to false/false to resize from the bottom right corner. +// Arguments: +// - pcoordSize - Requested viewport width/heights in characters +// - fResizeFromTop - If false, will trim/add to bottom of viewport first. If true, will trim/add to top. +// - fResizeFromBottom - If false, will trim/add to top of viewport first. If true, will trim/add to left. +// Return Value: +// - +void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize, + const bool fResizeFromTop, + const bool fResizeFromLeft) +{ + const short DeltaX = pcoordSize->X - _viewport.Width(); + const short DeltaY = pcoordSize->Y - _viewport.Height(); + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + + // do adjustments on a copy that's easily manipulated. + SMALL_RECT srNewViewport = _viewport.ToInclusive(); + + // Now we need to determine what our new Window size should + // be. Note that Window here refers to the character/row window. + if (fResizeFromLeft) + { + // we're being horizontally sized from the left border + const SHORT sLeftProposed = (srNewViewport.Left - DeltaX); + if (sLeftProposed >= 0) + { + // there's enough room in the backlog to just expand left + srNewViewport.Left -= DeltaX; + } + else + { + // if we're resizing horizontally, we want to show as much + // content above as we can, but we can't show more + // than the left of the window + srNewViewport.Left = 0; + srNewViewport.Right += (SHORT)abs(sLeftProposed); + } + } + else + { + // we're being horizontally sized from the right border + const SHORT sRightProposed = (srNewViewport.Right + DeltaX); + if (sRightProposed <= (coordScreenBufferSize.X - 1)) + { + srNewViewport.Right += DeltaX; + } + else + { + srNewViewport.Right = (coordScreenBufferSize.X - 1); + srNewViewport.Left -= (sRightProposed - (coordScreenBufferSize.X - 1)); + } + } + + if (fResizeFromTop) + { + const SHORT sTopProposed = (srNewViewport.Top - DeltaY); + // we're being vertically sized from the top border + if (sTopProposed >= 0) + { + // Special case: Only modify the top position if we're not + // on the 0th row of the buffer. + + // If we're on the 0th row, people expect it to stay stuck + // to the top of the window, not to start collapsing down + // and hiding the top rows. + if (srNewViewport.Top > 0) + { + // there's enough room in the backlog to just expand the top + srNewViewport.Top -= DeltaY; + } + else + { + // If we didn't adjust the top, we need to trim off + // the number of rows from the bottom instead. + // NOTE: It's += because DeltaY will be negative + // already for this circumstance. + FAIL_FAST_IF(!(DeltaY <= 0)); + srNewViewport.Bottom += DeltaY; + } + } + else + { + // if we're resizing vertically, we want to show as much + // content above as we can, but we can't show more + // than the top of the window + srNewViewport.Top = 0; + srNewViewport.Bottom += (SHORT)abs(sTopProposed); + } + } + else + { + // we're being vertically sized from the bottom border + const SHORT sBottomProposed = (srNewViewport.Bottom + DeltaY); + if (sBottomProposed <= (coordScreenBufferSize.Y - 1)) + { + // If the new bottom is supposed to be before the final line of the buffer + // Check to ensure that we don't hide the prompt by collapsing the window. + + // The final valid end position will be the coordinates of + // the last character displayed (including any characters + // in the input line) + COORD coordValidEnd; + Selection::Instance().GetValidAreaBoundaries(nullptr, &coordValidEnd); + + // If the bottom of the window when adjusted would be + // above the final line of valid text... + if (srNewViewport.Bottom + DeltaY < coordValidEnd.Y) + { + // Adjust the top of the window instead of the bottom + // (so the lines slide upward) + srNewViewport.Top -= DeltaY; + + // If we happened to move the top of the window past + // the 0th row (first row in the buffer) + if (srNewViewport.Top < 0) + { + // Find the amount we went past 0, correct the top + // of the window back to 0, and instead adjust the + // bottom even though it will cause us to lose the + // prompt line. + const short cRemainder = 0 - srNewViewport.Top; + srNewViewport.Top += cRemainder; + FAIL_FAST_IF(!(srNewViewport.Top == 0)); + srNewViewport.Bottom += cRemainder; + } + } + else + { + srNewViewport.Bottom += DeltaY; + } + } + else + { + srNewViewport.Bottom = (coordScreenBufferSize.Y - 1); + srNewViewport.Top -= (sBottomProposed - (coordScreenBufferSize.Y - 1)); + } + } + + // Ensure the viewport is valid. + // We can't have a negative left or top. + if (srNewViewport.Left < 0) + { + srNewViewport.Right -= srNewViewport.Left; + srNewViewport.Left = 0; + } + + if (srNewViewport.Top < 0) + { + srNewViewport.Bottom -= srNewViewport.Top; + srNewViewport.Top = 0; + } + + // Bottom and right cannot pass the final characters in the array. + srNewViewport.Right = std::min(srNewViewport.Right, gsl::narrow(coordScreenBufferSize.X - 1)); + srNewViewport.Bottom = std::min(srNewViewport.Bottom, gsl::narrow(coordScreenBufferSize.Y - 1)); + + // See MSFT:19917443 + // If we're in terminal scrolling mode, and we've changed the height of the + // viewport, the new viewport's bottom to the _virtualBottom + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto newViewport = Viewport::FromInclusive(srNewViewport); + if (gci.IsTerminalScrolling() && newViewport.Height() != _viewport.Height()) + { + const short newTop = static_cast(std::max(0, _virtualBottom - (newViewport.Height() - 1))); + + newViewport = Viewport::FromDimensions(COORD({newViewport.Left(), newTop}), newViewport.Dimensions()); + } + + _viewport = newViewport; + UpdateBottom(); + Tracing::s_TraceWindowViewport(_viewport); +} + +// Routine Description: +// - Modifies the size of the current viewport to match the width/height of the request given. +// - Uses the old and new client areas to determine which side the window was resized from. +// Arguments: +// - prcClientNew - Client rectangle in pixels after this update +// - prcClientOld - Client rectangle in pixels before this update +// - pcoordSize - Requested viewport width/heights in characters +// Return Value: +// - +void SCREEN_INFORMATION::_AdjustViewportSize(const RECT* const prcClientNew, + const RECT* const prcClientOld, + const COORD* const pcoordSize) +{ + // If the left is the only one that changed (and not the right + // also), then adjust from the left. Otherwise if the right + // changes or both changed, bias toward leaving the top-left + // corner in place and resize from the bottom right. + // -- + // Resizing from the bottom right is more expected by + // users. Normally only one dimension (or one corner) will change + // at a time if the user is moving it. However, if the window is + // being dragged and forced to resize at a monitor boundary, all 4 + // will change. In this case especially, users expect the top left + // to stay in place and the bottom right to adapt. + bool const fResizeFromLeft = prcClientNew->left != prcClientOld->left && + prcClientNew->right == prcClientOld->right; + bool const fResizeFromTop = prcClientNew->top != prcClientOld->top && + prcClientNew->bottom == prcClientOld->bottom; + + const Viewport oldViewport = Viewport(_viewport); + + _InternalSetViewportSize(pcoordSize, fResizeFromLeft, fResizeFromTop); + + // MSFT 13194969, related to 12092729. + // If we're in virtual terminal mode, and the viewport dimensions change, + // send a WindowBufferSizeEvent. If the client wants VT mode, then they + // probably want the viewport resizes, not just the screen buffer + // resizes. This does change the behavior of the API for v2 callers, + // but only callers who've requested VT mode. In 12092729, we enabled + // sending notifications from window resizes in cases where the buffer + // didn't resize, so this applies the same expansion to resizes using + // the window, not the API. + if (IsInVirtualTerminalInputMode()) + { + if ((_viewport.Width() != oldViewport.Width()) || + (_viewport.Height() != oldViewport.Height())) + { + ScreenBufferSizeChange(GetBufferSize().Dimensions()); + } + } +} + +// Routine Description: +// - From a window client area in pixels, a buffer size, and the font size, this will determine +// whether scroll bars will need to be shown (and consume a portion of the client area) for the +// given buffer to be rendered. +// Arguments: +// - prcClientArea - Client area in pixels of the available space for rendering +// - pcoordBufferSize - Buffer size in characters +// - pcoordFontSize - Font size in pixels per character +// - pfIsHorizontalVisible - Indicates whether the horizontal scroll +// bar (consuming vertical space) will need to be visible +// - pfIsVerticalVisible - Indicates whether the vertical scroll bar +// (consuming horizontal space) will need to be visible +// Return Value: +// - +void SCREEN_INFORMATION::s_CalculateScrollbarVisibility(const RECT* const prcClientArea, + const COORD* const pcoordBufferSize, + const COORD* const pcoordFontSize, + _Out_ bool* const pfIsHorizontalVisible, + _Out_ bool* const pfIsVerticalVisible) +{ + // Start with bars not visible as the initial state of the client area doesn't account for scroll bars. + *pfIsHorizontalVisible = false; + *pfIsVerticalVisible = false; + + // Set up the client area in pixels + SIZE sizeClientPixels = { 0 }; + sizeClientPixels.cx = RECT_WIDTH(prcClientArea); + sizeClientPixels.cy = RECT_HEIGHT(prcClientArea); + + // Set up the buffer area in pixels by multiplying the size by the font size scale factor + SIZE sizeBufferPixels = { 0 }; + sizeBufferPixels.cx = pcoordBufferSize->X * pcoordFontSize->X; + sizeBufferPixels.cy = pcoordBufferSize->Y * pcoordFontSize->Y; + + // Now figure out whether we need one or both scroll bars. + // Showing a scroll bar in one direction may necessitate showing + // the scroll bar in the other (as it will consume client area + // space). + + if (sizeBufferPixels.cx > sizeClientPixels.cx) + { + *pfIsHorizontalVisible = true; + + // If we have a horizontal bar, remove it from available + // vertical space and check that remaining client area is + // enough. + sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; + + if (sizeBufferPixels.cy > sizeClientPixels.cy) + { + *pfIsVerticalVisible = true; + } + } + else if (sizeBufferPixels.cy > sizeClientPixels.cy) + { + *pfIsVerticalVisible = true; + + // If we have a vertical bar, remove it from available + // horizontal space and check that remaining client area is + // enough. + sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; + + if (sizeBufferPixels.cx > sizeClientPixels.cx) + { + *pfIsHorizontalVisible = true; + } + } +} + +bool SCREEN_INFORMATION::IsMaximizedBoth() const +{ + return IsMaximizedX() && IsMaximizedY(); +} + +bool SCREEN_INFORMATION::IsMaximizedX() const +{ + // If the viewport is displaying the entire size of the allocated buffer, it's maximized. + return _viewport.Left() == 0 && (_viewport.Width() == GetBufferSize().Width()); +} + +bool SCREEN_INFORMATION::IsMaximizedY() const +{ + // If the viewport is displaying the entire size of the allocated buffer, it's maximized. + return _viewport.Top() == 0 && (_viewport.Height() == GetBufferSize().Height()); +} + +#pragma endregion + +// Routine Description: +// - This is a screen resize algorithm which will reflow the ends of lines based on the +// line wrap state used for clipboard line-based copy. +// Arguments: +// - Coordinates of the new screen size +// Return Value: +// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::ResizeWithReflow(const COORD coordNewScreenSize) +{ + if ((USHORT)coordNewScreenSize.X >= SHORT_MAX || (USHORT)coordNewScreenSize.Y >= SHORT_MAX) + { + RIPMSG2(RIP_WARNING, "Invalid screen buffer size (0x%x, 0x%x)", coordNewScreenSize.X, coordNewScreenSize.Y); + return STATUS_INVALID_PARAMETER; + } + + // First allocate a new text buffer to take the place of the current one. + std::unique_ptr newTextBuffer; + try + { + newTextBuffer = std::make_unique(coordNewScreenSize, + GetAttributes(), + 0, + _renderTarget); // temporarily set size to 0 so it won't render. + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + // Save cursor's relative height versus the viewport + SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top(); + + Cursor& oldCursor = _textBuffer->GetCursor(); + Cursor& newCursor = newTextBuffer->GetCursor(); + // skip any drawing updates that might occur as we manipulate the new buffer + oldCursor.StartDeferDrawing(); + newCursor.StartDeferDrawing(); + + // We need to save the old cursor position so that we can + // place the new cursor back on the equivalent character in + // the new buffer. + COORD cOldCursorPos = oldCursor.GetPosition(); + COORD cOldLastChar = _textBuffer->GetLastNonSpaceCharacter(); + + short const cOldRowsTotal = cOldLastChar.Y + 1; + short const cOldColsTotal = GetBufferSize().Width(); + + COORD cNewCursorPos = { 0 }; + bool fFoundCursorPos = false; + + NTSTATUS status = STATUS_SUCCESS; + // Loop through all the rows of the old buffer and reprint them into the new buffer + for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++) + { + // Fetch the row and its "right" which is the last printable character. + const ROW& Row = _textBuffer->GetRowByOffset(iOldRow); + const CharRow& charRow = Row.GetCharRow(); + short iRight = static_cast(charRow.MeasureRight()); + + // There is a special case here. If the row has a "wrap" + // flag on it, but the right isn't equal to the width (one + // index past the final valid index in the row) then there + // were a bunch trailing of spaces in the row. + // (But the measuring functions for each row Left/Right do + // not count spaces as "displayable" so they're not + // included.) + // As such, adjust the "right" to be the width of the row + // to capture all these spaces + if (charRow.WasWrapForced()) + { + iRight = cOldColsTotal; + + // And a combined special case. + // If we wrapped off the end of the row by adding a + // piece of padding because of a double byte LEADING + // character, then remove one from the "right" to + // leave this padding out of the copy process. + if (charRow.WasDoubleBytePadded()) + { + iRight--; + } + } + + // Loop through every character in the current row (up to + // the "right" boundary, which is one past the final valid + // character) + for (short iOldCol = 0; iOldCol < iRight; iOldCol++) + { + if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) + { + cNewCursorPos = newCursor.GetPosition(); + fFoundCursorPos = true; + } + + try + { + // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... + const auto glyph = Row.GetCharRow().GlyphAt(iOldCol); + const auto dbcsAttr = Row.GetCharRow().DbcsAttrAt(iOldCol); + const auto textAttr = Row.GetAttrRow().GetAttrByColumn(iOldCol); + + if (!newTextBuffer->InsertCharacter(glyph, dbcsAttr, textAttr)) + { + status = STATUS_NO_MEMORY; + break; + } + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + } + if (NT_SUCCESS(status)) + { + // 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 && !charRow.WasWrapForced()) + { + if (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) + { + status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY; + } + 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 COORD coordNewCursor = newCursor.GetPosition(); + if (coordNewCursor.X == 0 && coordNewCursor.Y > 0) + { + if (newTextBuffer->GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced()) + { + status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY; + } + } + } + } + } + } + if (NT_SUCCESS(status)) + { + // Finish copying remaining parameters from the old text buffer to the new one + newTextBuffer->CopyProperties(*_textBuffer); + + // 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 + { + // 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 + int iNewlines = cOldCursorPos.Y - cOldLastChar.Y; + int iIncrements = cOldCursorPos.X - cOldLastChar.X; + const COORD cNewLastChar = newTextBuffer->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 (newTextBuffer->GetRowByOffset(cNewLastChar.Y).GetCharRow().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 (_textBuffer->GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced()) + { + iNewlines = std::max(iNewlines - 1, 0); + } + } + + for (int r = 0; r < iNewlines; r++) + { + if (!newTextBuffer->NewlineCursor()) + { + status = STATUS_NO_MEMORY; + break; + } + } + if (NT_SUCCESS(status)) + { + for (int c = 0; c < iIncrements - 1; c++) + { + if (!newTextBuffer->IncrementCursor()) + { + status = STATUS_NO_MEMORY; + break; + } + } + } + } + } + + if (NT_SUCCESS(status)) + { + // Adjust the viewport so the cursor doesn't wildly fly off up or down. + SHORT const sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top(); + COORD coordCursorHeightDiff = { 0 }; + coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; + LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true)); + + // Save old cursor size before we delete it + ULONG const ulSize = oldCursor.GetSize(); + + _textBuffer.swap(newTextBuffer); + + // Set size back to real size as it will be taking over the rendering duties. + newCursor.SetSize(ulSize); + newCursor.EndDeferDrawing(); + } + oldCursor.EndDeferDrawing(); + + return status; +} + +// +// Routine Description: +// - This is the legacy screen resize with minimal changes +// Arguments: +// - coordNewScreenSize - new size of screen. +// Return Value: +// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const COORD coordNewScreenSize) +{ + return NTSTATUS_FROM_HRESULT(_textBuffer->ResizeTraditional(coordNewScreenSize)); +} + +// +// Routine Description: +// - This routine resizes the screen buffer. +// Arguments: +// - NewScreenSize - new size of screen in characters +// - DoScrollBarUpdate - indicates whether to update scroll bars at the end +// Return Value: +// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::ResizeScreenBuffer(const COORD coordNewScreenSize, + const bool fDoScrollBarUpdate) +{ + // If the size hasn't actually changed, do nothing. + if (coordNewScreenSize == GetBufferSize().Dimensions()) + { + return STATUS_SUCCESS; + } + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + NTSTATUS status = STATUS_SUCCESS; + + // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions + Selection::Instance().ClearSelection(); + + // cancel any popups before resizing or they will not necessarily line up with new buffer positions + CommandLine::Instance().EndAllPopups(); + + const bool fWrapText = gci.GetWrapText(); + if (fWrapText) + { + status = ResizeWithReflow(coordNewScreenSize); + } + else + { + status = NTSTATUS_FROM_HRESULT(ResizeTraditional(coordNewScreenSize)); + } + + if (NT_SUCCESS(status)) + { + NotifyAccessibilityEventing(0, 0, (SHORT)(coordNewScreenSize.X - 1), (SHORT)(coordNewScreenSize.Y - 1)); + + if ((!ConvScreenInfo)) + { + if (FAILED(ConsoleImeResizeCompStrScreenBuffer(coordNewScreenSize))) + { + // If something went wrong, just bail out. + return STATUS_INVALID_HANDLE; + } + } + + // Fire off an event to let accessibility apps know the layout has changed. + if (IsActiveScreenBuffer()) + { + _pAccessibilityNotifier->NotifyConsoleLayoutEvent(); + } + + if (fDoScrollBarUpdate) + { + UpdateScrollBars(); + } + ScreenBufferSizeChange(coordNewScreenSize); + } + + return status; +} + +// Routine Description: +// - Given a rectangle containing screen buffer coordinates (character-level positioning, not pixel) +// This method will trim the rectangle to ensure it is within the buffer. +// For example, if the rectangle given has a right position of 85, but the current screen buffer +// is only reaching from 0-79, then the right position will be set to 79. +// Arguments: +// - psrRect - Pointer to rectangle holding data to be trimmed +// Return Value: +// - +void SCREEN_INFORMATION::ClipToScreenBuffer(_Inout_ SMALL_RECT* const psrClip) const +{ + const auto bufferSize = GetBufferSize(); + + psrClip->Left = std::max(psrClip->Left, bufferSize.Left()); + psrClip->Top = std::max(psrClip->Top, bufferSize.Top()); + psrClip->Right = std::min(psrClip->Right, bufferSize.RightInclusive()); + psrClip->Bottom = std::min(psrClip->Bottom, bufferSize.BottomInclusive()); +} + +void SCREEN_INFORMATION::MakeCurrentCursorVisible() +{ + MakeCursorVisible(_textBuffer->GetCursor().GetPosition()); +} + +// Routine Description: +// - This routine sets the cursor size and visibility both in the data +// structures and on the screen. Also updates the cursor information of +// this buffer's main buffer, if this buffer is an alt buffer. +// Arguments: +// - Size - cursor size +// - Visible - cursor visibility +// Return Value: +// - None +void SCREEN_INFORMATION::SetCursorInformation(const ULONG Size, + const bool Visible) noexcept +{ + Cursor& cursor = _textBuffer->GetCursor(); + + cursor.SetSize(Size); + cursor.SetIsVisible(Visible); + cursor.SetType(CursorType::Legacy); + + // If we're an alt buffer, also update our main buffer. + // Users of the API expect both to be set - this can't be set by VT + if (_psiMainBuffer) + { + _psiMainBuffer->SetCursorInformation(Size, Visible); + } +} + +// Routine Description: +// - This routine sets the cursor color. Also updates the cursor information of +// this buffer's main buffer, if this buffer is an alt buffer. +// Arguments: +// - Color - The new color to set the cursor to +// - setMain - If true, propagate change to main buffer as well. +// Return Value: +// - None +void SCREEN_INFORMATION::SetCursorColor(const unsigned int Color, const bool setMain) noexcept +{ + Cursor& cursor = _textBuffer->GetCursor(); + + cursor.SetColor(Color); + + // If we're an alt buffer, DON'T propagate this setting up to the main buffer. + // We don't want to pollute that buffer with this state, + // UNLESS we're getting called from the propsheet, then we DO want to update this. + if (_psiMainBuffer && setMain) + { + _psiMainBuffer->SetCursorColor(Color); + } +} + +// Routine Description: +// - This routine sets the cursor shape both in the data +// structures and on the screen. Also updates the cursor information of +// this buffer's main buffer, if this buffer is an alt buffer. +// Arguments: +// - Type - The new shape to set the cursor to +// - setMain - If true, propagate change to main buffer as well. +// Return Value: +// - None +void SCREEN_INFORMATION::SetCursorType(const CursorType Type, const bool setMain) noexcept +{ + Cursor& cursor = _textBuffer->GetCursor(); + + cursor.SetType(Type); + + // If we're an alt buffer, DON'T propagate this setting up to the main buffer. + // We don't want to pollute that buffer with this state, + // UNLESS we're getting called from the propsheet, then we DO want to update this. + if (_psiMainBuffer && setMain) + { + _psiMainBuffer->SetCursorType(Type); + } +} + +// Routine Description: +// - This routine sets a flag saying whether the cursor should be displayed +// with it's default size or it should be modified to indicate the +// insert/overtype mode has changed. +// Arguments: +// - ScreenInfo - pointer to screen info structure. +// - DoubleCursor - should we indicated non-normal mode +// Return Value: +// - None +void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) +{ + Cursor& cursor = _textBuffer->GetCursor(); + + if ((cursor.IsDouble() != DoubleCursor)) + { + cursor.SetIsDouble(DoubleCursor); + } + + // If we're an alt buffer, also update our main buffer. + if (_psiMainBuffer) + { + _psiMainBuffer->SetCursorDBMode(DoubleCursor); + } +} + +// Routine Description: +// - This routine sets the cursor position in the data structures and on the screen. +// Arguments: +// - ScreenInfo - pointer to screen info structure. +// - Position - new position of cursor +// - TurnOn - true if cursor should be left on, false if should be left off +// Return Value: +// - Status +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::SetCursorPosition(const COORD Position, const bool TurnOn) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + Cursor& cursor = _textBuffer->GetCursor(); + + // + // Ensure that the cursor position is within the constraints of the screen + // buffer. + // + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + if (Position.X >= coordScreenBufferSize.X || Position.Y >= coordScreenBufferSize.Y || Position.X < 0 || Position.Y < 0) + { + return STATUS_INVALID_PARAMETER; + } + + cursor.SetPosition(Position); + + // if we have the focus, adjust the cursor state + if (gci.Flags & CONSOLE_HAS_FOCUS) + { + if (TurnOn) + { + cursor.SetDelay(false); + cursor.SetIsOn(true); + } + else + { + cursor.SetDelay(true); + } + cursor.SetHasMoved(true); + } + + return STATUS_SUCCESS; +} + +void SCREEN_INFORMATION::MakeCursorVisible(const COORD CursorPosition, const bool updateBottom) +{ + COORD WindowOrigin; + + if (CursorPosition.X > _viewport.RightInclusive()) + { + WindowOrigin.X = CursorPosition.X - _viewport.RightInclusive(); + } + else if (CursorPosition.X < _viewport.Left()) + { + WindowOrigin.X = CursorPosition.X - _viewport.Left(); + } + else + { + WindowOrigin.X = 0; + } + + if (CursorPosition.Y > _viewport.BottomInclusive()) + { + WindowOrigin.Y = CursorPosition.Y - _viewport.BottomInclusive(); + } + else if (CursorPosition.Y < _viewport.Top()) + { + WindowOrigin.Y = CursorPosition.Y - _viewport.Top(); + } + else + { + WindowOrigin.Y = 0; + } + + if (WindowOrigin.X != 0 || WindowOrigin.Y != 0) + { + LOG_IF_FAILED(SetViewportOrigin(false, WindowOrigin, updateBottom)); + } +} + +// Method Description: +// - Sets the scroll margins for this buffer. +// Arguments: +// - margins: The new values of the scroll margins, *relative to the viewport* +void SCREEN_INFORMATION::SetScrollMargins(const Viewport margins) +{ + _scrollMargins = margins; +} + +// Method Description: +// - Returns the scrolling margins boundaries for this screen buffer, relative +// to the origin of the text buffer. Most callers will want the absolute +// positions of the margins, though they are set and stored relative to +// origin of the viewport. +// Arguments: +// - +Viewport SCREEN_INFORMATION::GetAbsoluteScrollMargins() const +{ + return _viewport.ConvertFromOrigin(_scrollMargins); +} + +// Method Description: +// - Returns the scrolling margins boundaries for this screen buffer, relative +// to the current viewport. +// Arguments: +// - +Viewport SCREEN_INFORMATION::GetRelativeScrollMargins() const +{ + return _scrollMargins; +} + +// Routine Description: +// - Retrieves the active buffer of this buffer. If this buffer has an +// alternate buffer, this is the alternate buffer. Otherwise, it is this buffer. +// Parameters: +// - None +// Return value: +// - a reference to this buffer's active buffer. +SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveBuffer() +{ + return const_cast(static_cast(this)->GetActiveBuffer()); +} + +const SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveBuffer() const +{ + if (_psiAlternateBuffer != nullptr) + { + return *_psiAlternateBuffer; + } + return *this; +} + +// Routine Description: +// - Retrieves the main buffer of this buffer. If this buffer has an +// alternate buffer, this is the main buffer. Otherwise, it is this buffer's main buffer. +// The main buffer is not necessarily the active buffer. +// Parameters: +// - None +// Return value: +// - a reference to this buffer's main buffer. +SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() +{ + return const_cast(static_cast(this)->GetMainBuffer()); +} + +const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const +{ + if (_psiMainBuffer != nullptr) + { + return *_psiMainBuffer; + } + return *this; +} + +// Routine Description: +// - Instantiates a new buffer to be used as an alternate buffer. This buffer +// does not have a driver handle associated with it and shares a state +// machine with the main buffer it belongs to. +// TODO: MSFT:19817348 Don't create alt screenbuffer's via an out SCREEN_INFORMATION** +// Parameters: +// - ppsiNewScreenBuffer - a pointer to recieve the newly created buffer. +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer) +{ + // Create new screen buffer. + COORD WindowSize = _viewport.Dimensions(); + + const FontInfo& existingFont = GetCurrentFont(); + + NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(WindowSize, + existingFont, + WindowSize, + GetAttributes(), + *GetPopupAttributes(), + CURSOR_SMALL_SIZE, + ppsiNewScreenBuffer); + if (NT_SUCCESS(Status)) + { + // Update the alt buffer's cursor style to match our own. + auto& myCursor = GetTextBuffer().GetCursor(); + auto* const createdBuffer = *ppsiNewScreenBuffer; + createdBuffer->GetTextBuffer().GetCursor().SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType()); + + s_InsertScreenBuffer(createdBuffer); + + // delete the alt buffer's state machine. We don't want it. + createdBuffer->_FreeOutputStateMachine(); // this has to be done before we give it a main buffer + // we'll attach the GetSet, etc once we successfully make this buffer the active buffer. + + // Set up the new buffers references to our current state machine, dispatcher, getset, etc. + createdBuffer->_stateMachine = _stateMachine; + + // Setup the alt buffer's tabs stops with the default tab stop settings + createdBuffer->SetDefaultVtTabStops(); + } + return Status; +} + +// Routine Description: +// - Creates an "alternate" screen buffer for this buffer. In virtual terminals, there exists both a "main" +// 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 +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +[[nodiscard]] +NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& siMain = GetMainBuffer(); + // If we're in an alt that resized, resize the main before making the new alt + if (siMain._fAltWindowChanged) + { + siMain.ProcessResizeWindow(&(siMain._rcAltSavedClientNew), &(siMain._rcAltSavedClientOld)); + siMain._fAltWindowChanged = false; + } + + SCREEN_INFORMATION* psiNewAltBuffer; + NTSTATUS Status = _CreateAltBuffer(&psiNewAltBuffer); + if (NT_SUCCESS(Status)) + { + // if this is already an alternate buffer, we want to make the new + // buffer the alt on our main buffer, not on ourself, because there + // can only ever be one main and one alternate. + SCREEN_INFORMATION* const psiOldAltBuffer = siMain._psiAlternateBuffer; + + psiNewAltBuffer->_psiMainBuffer = &siMain; + siMain._psiAlternateBuffer = psiNewAltBuffer; + + if (psiOldAltBuffer != nullptr) + { + s_RemoveScreenBuffer(psiOldAltBuffer); // this will also delete the old alt buffer + } + + ::SetActiveScreenBuffer(*psiNewAltBuffer); + + // Kind of a hack until we have proper signal channels: If the client app wants window size events, send one for + // the new alt buffer's size (this is so WSL can update the TTY size when the MainSB.viewportWidth < + // MainSB.bufferWidth (which can happen with wrap text disabled)) + ScreenBufferSizeChange(psiNewAltBuffer->GetBufferSize().Dimensions()); + + // Tell the VT MouseInput handler that we're in the Alt buffer now + gci.terminalMouseInput.UseAlternateScreenBuffer(); + + } + return Status; +} + +// Routine Description: +// - Restores the active buffer to be this buffer's main buffer. If this is the main buffer, then nothing happens. +// Parameters: +// - None +// Return value: +// - STATUS_SUCCESS if handled successfully. Otherwise, an approriate status code indicating the error. +void SCREEN_INFORMATION::UseMainScreenBuffer() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION* psiMain = _psiMainBuffer; + if (psiMain != nullptr) + { + if (psiMain->_fAltWindowChanged) + { + psiMain->ProcessResizeWindow(&(psiMain->_rcAltSavedClientNew), &(psiMain->_rcAltSavedClientOld)); + psiMain->_fAltWindowChanged = false; + } + ::SetActiveScreenBuffer(*psiMain); + psiMain->UpdateScrollBars(); // The alt had disabled scrollbars, re-enable them + + // send a _coordScreenBufferSizeChangeEvent for the new Sb viewport + ScreenBufferSizeChange(psiMain->GetBufferSize().Dimensions()); + + SCREEN_INFORMATION* psiAlt = psiMain->_psiAlternateBuffer; + psiMain->_psiAlternateBuffer = nullptr; + s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer + // deleting the alt buffer will give the GetSet back to it's main + + // Tell the VT MouseInput handler that we're in the main buffer now + gci.terminalMouseInput.UseMainScreenBuffer(); + } +} + +// Routine Description: +// - Helper indicating if the buffer has a main buffer, meaning that this is an alternate buffer. +// Parameters: +// - None +// Return value: +// - true iff this buffer has a main buffer. +bool SCREEN_INFORMATION::_IsAltBuffer() const +{ + return _psiMainBuffer != nullptr; +} + +// Routine Description: +// - Helper indicating if the buffer is acting as a pty - with the screenbuffer +// clamped to the viewport size. This can be the case either when we're in +// VT I/O mode, or when this buffer is an alt buffer. +// Parameters: +// - None +// Return value: +// - true iff this buffer has a main buffer. +bool SCREEN_INFORMATION::_IsInPtyMode() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return _IsAltBuffer() || gci.IsInVtIoMode(); +} + +// Routine Description: +// - Sets a VT tab stop in the column sColumn. If there is already a tab there, it does nothing. +// Parameters: +// - sColumn: the column to add a tab stop to. +// Return value: +// - none +// Note: may throw exception on allocation error +void SCREEN_INFORMATION::AddTabStop(const SHORT sColumn) +{ + if (std::find(_tabStops.begin(), _tabStops.end(), sColumn) == _tabStops.end()) + { + _tabStops.push_back(sColumn); + _tabStops.sort(); + } +} + +// Routine Description: +// - Clears all of the VT tabs that have been set. This also deletes them. +// Parameters: +// +// Return value: +// +void SCREEN_INFORMATION::ClearTabStops() noexcept +{ + _tabStops.clear(); +} + +// Routine Description: +// - Clears the VT tab in the column sColumn (if one has been set). Also deletes it from the heap. +// Parameters: +// - sColumn - The column to clear the tab stop for. +// Return value: +// +void SCREEN_INFORMATION::ClearTabStop(const SHORT sColumn) noexcept +{ + _tabStops.remove(sColumn); +} + +// Routine Description: +// - Places the location that a forwards tab would take cCurrCursorPos to into pcNewCursorPos +// Parameters: +// - cCurrCursorPos - The initial cursor location +// Return value: +// - +COORD SCREEN_INFORMATION::GetForwardTab(const COORD cCurrCursorPos) const noexcept +{ + + COORD cNewCursorPos = cCurrCursorPos; + SHORT sWidth = GetBufferSize().RightInclusive(); + if (cCurrCursorPos.X == sWidth) + { + cNewCursorPos.X = 0; + cNewCursorPos.Y += 1; + } + else if (_tabStops.empty() || cCurrCursorPos.X >= _tabStops.back()) + { + cNewCursorPos.X = sWidth; + } + else + { + // search for next tab stop + for (auto it = _tabStops.cbegin(); it != _tabStops.cend(); ++it) + { + if (*it > cCurrCursorPos.X) + { + cNewCursorPos.X = *it; + break; + } + } + } + return cNewCursorPos; +} + +// Routine Description: +// - Places the location that a backwards tab would take cCurrCursorPos to into pcNewCursorPos +// Parameters: +// - cCurrCursorPos - The initial cursor location +// Return value: +// - +COORD SCREEN_INFORMATION::GetReverseTab(const COORD cCurrCursorPos) const noexcept +{ + COORD cNewCursorPos = cCurrCursorPos; + // if we're at 0, or there are NO tabs, or the first tab is farther right than where we are + if (cCurrCursorPos.X == 0 || _tabStops.empty() || _tabStops.front() >= cCurrCursorPos.X) + { + cNewCursorPos.X = 0; + } + else + { + for (auto it = _tabStops.crbegin(); it != _tabStops.crend(); ++it) + { + if (*it < cCurrCursorPos.X) + { + cNewCursorPos.X = *it; + break; + } + } + } + return cNewCursorPos; +} + +// Routine Description: +// - Returns true if any VT-style tab stops have been set (with AddTabStop) +// Parameters: +// +// Return value: +// - true if any VT-style tab stops have been set +bool SCREEN_INFORMATION::AreTabsSet() const noexcept +{ + return !_tabStops.empty(); +} + +// Routine Description: +// - adds default tab stops for vt mode +void SCREEN_INFORMATION::SetDefaultVtTabStops() +{ + _tabStops.clear(); + const int width = GetBufferSize().RightInclusive(); + FAIL_FAST_IF(width < 0); + for (int pos = 0; pos <= width; pos += TAB_SIZE) + { + _tabStops.push_back(gsl::narrow(pos)); + } + if (_tabStops.back() != width) + { + _tabStops.push_back(gsl::narrow(width)); + } +} + +// Routine Description: +// - Returns the value of the attributes +// Parameters: +// +// Return value: +// - This screen buffer's attributes +TextAttribute SCREEN_INFORMATION::GetAttributes() const +{ + return _textBuffer->GetCurrentAttributes(); +} + +// Routine Description: +// - Returns the value of the popup attributes +// Parameters: +// +// Return value: +// - This screen buffer's popup attributes +const TextAttribute* const SCREEN_INFORMATION::GetPopupAttributes() const +{ + return &_PopupAttributes; +} + +// Routine Description: +// - Sets the value of the attributes on this screen buffer. Also propagates +// the change down to the fill of the text buffer attached to this screen buffer. +// Parameters: +// - attributes - The new value of the attributes to use. +// Return value: +// +void SCREEN_INFORMATION::SetAttributes(const TextAttribute& attributes) +{ + _textBuffer->SetCurrentAttributes(attributes); + + // If we're an alt buffer, DON'T propagate this setting up to the main buffer. + // We don't want to pollute that buffer with this state. +} + +// Method Description: +// - Sets the value of the popup attributes on this screen buffer. +// Parameters: +// - popupAttributes - The new value of the popup attributes to use. +// Return value: +// +void SCREEN_INFORMATION::SetPopupAttributes(const TextAttribute& popupAttributes) +{ + _PopupAttributes = popupAttributes; + + // If we're an alt buffer, DON'T propagate this setting up to the main buffer. + // We don't want to pollute that buffer with this state. +} + +// Method Description: +// - Sets the value of the attributes on this screen buffer. Also propagates +// the change down to the fill of the attached text buffer. +// - Additionally updates any popups to match the new color scheme. +// - Also updates the defaults of the main buffer. This method is called by the +// propsheet menu when you set the colors via the propsheet. In that +// workflow, we want the main buffer's colors changed as well as our own. +// Parameters: +// - attributes - The new value of the attributes to use. +// - popupAttributes - The new value of the popup attributes to use. +// Return value: +// +// Notes: +// This code is merged from the old global function SetScreenColors +void SCREEN_INFORMATION::SetDefaultAttributes(const TextAttribute& attributes, + const TextAttribute& popupAttributes) +{ + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + const TextAttribute oldPrimaryAttributes = GetAttributes(); + const TextAttribute oldPopupAttributes = *GetPopupAttributes(); + + // Quick return if we don't need to do anything. + if (oldPrimaryAttributes == attributes && oldPopupAttributes == popupAttributes) + { + return; + } + + SetAttributes(attributes); + SetPopupAttributes(popupAttributes); + + auto& commandLine = CommandLine::Instance(); + if (commandLine.HasPopup()) + { + commandLine.UpdatePopups(attributes, popupAttributes, oldPrimaryAttributes, oldPopupAttributes); + } + + // force repaint of entire viewport + GetRenderTarget().TriggerRedrawAll(); + + gci.ConsoleIme.RefreshAreaAttributes(); + + // If we're an alt buffer, also update our main buffer. + if (_psiMainBuffer) + { + _psiMainBuffer->SetDefaultAttributes(attributes, popupAttributes); + } +} + +// Method Description: +// - Returns an inclusive rectangle that describes the bounds of the buffer viewport. +// Arguments: +// - +// Return Value: +// - the viewport bounds as an inclusive rect. +const Viewport& SCREEN_INFORMATION::GetViewport() const noexcept +{ + return _viewport; +} + +// Routine Description: +// - This routine updates the size of the rectangle representing the viewport into the text buffer. +// - It is specified in character count within the buffer. +// - It will be corrected to not exceed the limits of the current screen buffer dimensions. +// Arguments: +// newViewport: The new viewport to use. If it's out of bounds in the negative +// direction it will be shifted to positive coordinates. If it's bigger +// that the screen buffer, it will be clamped to the size of the buffer. +// updateBottom: if true, update our virtual bottom. This should be false when +// called from UX interactions, such as scrolling with the mouse wheel, +// and true when called from API endpoints, such as SetConsoleWindowInfo +// Return Value: +// - None +void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, + const bool updateBottom) +{ + // make sure there's something to do + if (newViewport == _viewport) + { + return; + } + + // do adjustments on a copy that's easily manipulated. + SMALL_RECT srCorrected = newViewport.ToInclusive(); + + if (srCorrected.Left < 0) + { + srCorrected.Right -= srCorrected.Left; + srCorrected.Left = 0; + } + if (srCorrected.Top < 0) + { + srCorrected.Bottom -= srCorrected.Top; + srCorrected.Top = 0; + } + + const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); + if (srCorrected.Right >= coordScreenBufferSize.X) + { + srCorrected.Right = coordScreenBufferSize.X; + } + if (srCorrected.Bottom >= coordScreenBufferSize.Y) + { + srCorrected.Bottom = coordScreenBufferSize.Y; + } + + _viewport = Viewport::FromInclusive(srCorrected); + if (updateBottom) + { + UpdateBottom(); + } + + Tracing::s_TraceWindowViewport(_viewport); +} + +// Method Description: +// - Performs a VT Erase All operation. In most terminals, this is done by +// moving the viewport into the scrollback, clearing out the current screen. +// For them, there can never be any characters beneath the viewport, as the +// viewport is always at the bottom. So, we can accomplish the same behavior +// by using the LastNonspaceCharacter as the "bottom", and placing the new +// viewport underneath that character. +// Parameters: +// +// Return value: +// - S_OK if we succeeded, or another status if there was a failure. +[[nodiscard]] +HRESULT SCREEN_INFORMATION::VtEraseAll() +{ + const COORD coordLastChar = _textBuffer->GetLastNonSpaceCharacter(); + short sNewTop = coordLastChar.Y + 1; + const Viewport oldViewport = _viewport; + // Stash away the current position of the cursor within the viewport. + // We'll need to restore the cursor to that same relative position, after + // we move the viewport. + const COORD oldCursorPos = _textBuffer->GetCursor().GetPosition(); + COORD relativeCursor = oldCursorPos; + oldViewport.ConvertToOrigin(&relativeCursor); + + short delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height()); + for (auto i = 0; i < delta; i++) + { + _textBuffer->IncrementCircularBuffer(); + sNewTop--; + } + + const COORD coordNewOrigin = { 0, sNewTop }; + RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true)); + // Restore the relative cursor position + _viewport.ConvertFromOrigin(&relativeCursor); + RETURN_IF_FAILED(SetCursorPosition(relativeCursor, false)); + + // Update all the rows in the current viewport with the currently active attributes. + OutputCellIterator it(GetAttributes()); + WriteRect(it, _viewport); + + return S_OK; +} + +// Method Description: +// - Sets up the Output state machine to be in pty mode. Sequences it doesn't +// understand will be written to tthe pTtyConnection passed in here. +// Arguments: +// - pTtyConnection: This is a TerminaOutputConnection that we can write the +// sequence we didn't understand to. +// Return Value: +// - +void SCREEN_INFORMATION::SetTerminalConnection(_In_ ITerminalOutputConnection* const pTtyConnection) +{ + OutputStateMachineEngine& engine = reinterpret_cast(_stateMachine->Engine()); + if (pTtyConnection) + { + engine.SetTerminalConnection(pTtyConnection, + std::bind(&StateMachine::FlushToTerminal, _stateMachine.get())); + } + else + { + engine.SetTerminalConnection(nullptr, + nullptr); + } +} + +// Routine Description: +// - This routine copies a rectangular region from the screen buffer. no clipping is done. +// Arguments: +// - viewport - rectangle in source buffer to copy +// Return Value: +// - output cell rectangle copy of screen buffer data +// Note: +// - will throw exception on error. +OutputCellRect SCREEN_INFORMATION::ReadRect(const Viewport viewport) const +{ + // If the viewport given doesn't fit inside this screen, it's not a valid argument. + THROW_HR_IF(E_INVALIDARG, !GetBufferSize().IsInBounds(viewport)); + + OutputCellRect result(viewport.Height(), viewport.Width()); + const OutputCell paddingCell{ std::wstring_view{ &UNICODE_SPACE, 1 }, {}, GetAttributes() }; + for (size_t rowIndex = 0; rowIndex < gsl::narrow(viewport.Height()); ++rowIndex) + { + COORD location = viewport.Origin(); + location.Y += (SHORT)rowIndex; + + auto data = GetCellLineDataAt(location); + const auto span = result.GetRow(rowIndex); + auto it = span.begin(); + + // Copy row data while there still is data and we haven't run out of rect to store it into. + while (data && it < span.end()) + { + *it++ = *data++; + } + + // Pad out any remaining space. + while (it < span.end()) + { + *it++ = paddingCell; + } + + // if we're clipping a dbcs char then don't include it, add a space instead + if (span.begin()->DbcsAttr().IsTrailing()) + { + *span.begin() = paddingCell; + } + if (span.rbegin()->DbcsAttr().IsLeading()) + { + *span.rbegin() = paddingCell; + } + + } + + return result; +} + +// Routine Description: +// - Writes cells to the output buffer at the cursor position. +// Arguments: +// - it - Iterator representing output cell data to write. +// Return Value: +// - the iterator at its final position +// Note: +// - will throw exception on error. +OutputCellIterator SCREEN_INFORMATION::Write(const OutputCellIterator it) +{ + return _textBuffer->Write(it); +} + +// Routine Description: +// - Writes cells to the output buffer. +// Arguments: +// - it - Iterator representing output cell data to write. +// - target - The position to start writing at +// Return Value: +// - the iterator at its final position +// Note: +// - will throw exception on error. +OutputCellIterator SCREEN_INFORMATION::Write(const OutputCellIterator it, + const COORD target) +{ + return _textBuffer->Write(it, target); +} + +// Routine Description: +// - This routine writes a rectangular region into the screen buffer. +// Arguments: +// - it - Iterator to the data to insert +// - viewport - rectangular region for insertion +// Return Value: +// - the iterator at its final position +// Note: +// - will throw exception on error. +OutputCellIterator SCREEN_INFORMATION::WriteRect(const OutputCellIterator it, + const Viewport viewport) +{ + THROW_HR_IF(E_INVALIDARG, viewport.Height() <= 0); + THROW_HR_IF(E_INVALIDARG, viewport.Width() <= 0); + + OutputCellIterator iter = it; + for (auto i = viewport.Top(); i < viewport.BottomExclusive(); i++) + { + iter = _textBuffer->WriteLine(iter, { viewport.Left(), i }, false, viewport.RightInclusive()); + } + + return iter; +} + +// Routine Description: +// - This routine writes a rectangular region into the screen buffer. +// Arguments: +// - data - rectangular data in memory buffer +// - location - origin point (top left corner) of where to write rectangular data +// Return Value: +// - +// Note: +// - will throw exception on error. +void SCREEN_INFORMATION::WriteRect(const OutputCellRect& data, + const COORD location) +{ + for (size_t i = 0; i < data.Height(); i++) + { + const auto iter = data.GetRowIter(i); + + COORD point; + point.X = location.X; + point.Y = location.Y + static_cast(i); + + _textBuffer->WriteLine(iter, point); + } +} + +// Routine Description: +// - Clears out the entire text buffer with the default character and +// the current default attribute applied to this screen. +void SCREEN_INFORMATION::ClearTextData() +{ + // Clear the text buffer. + _textBuffer->Reset(); +} + +// Routine Description: +// - finds the boundaries of the word at the given position on the screen +// Arguments: +// - position - location on the screen to get the word boundary for +// Return Value: +// - word boundary positions +std::pair SCREEN_INFORMATION::GetWordBoundary(const COORD position) const +{ + COORD clampedPosition = position; + GetBufferSize().Clamp(clampedPosition); + + COORD start{ clampedPosition }; + COORD end{ clampedPosition }; + + // find the start of the word + auto startIt = GetTextLineDataAt(clampedPosition); + while (startIt) + { + --startIt; + if (!startIt || IsWordDelim(*startIt)) + { + break; + } + --start.X; + } + + // find the end of the word + auto endIt = GetTextLineDataAt(clampedPosition); + while (endIt) + { + if (IsWordDelim(*endIt)) + { + break; + } + ++endIt; + ++end.X; + } + + // trim leading zeros if we need to + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.GetTrimLeadingZeros()) + { + // Trim the leading zeros: 000fe12 -> fe12, except 0x and 0n. + // Useful for debugging + + // Get iterator from the start of the selection + auto trimIt = GetTextLineDataAt(start); + + // Advance to the second character to check if it's an x or n. + trimIt++; + + // Only process if it's a single character. If it's a complicated run, then it's not an x or n. + if (trimIt->size() == 1) + { + // Get the single character + const auto wch = trimIt->front(); + + // If the string is long enough to have stuff after the 0x/0n and it doesn't have one... + if (end.X > start.X + 2 && + wch != L'x' && + wch != L'X' && + wch != L'n') + { + trimIt--; // Back up to the first character again + + // Now loop through and advance the selection forward each time + // we find a single character '0' to Trim off the leading zeroes. + while (trimIt->size() == 1 && + trimIt->front() == L'0' && + start.X < end.X - 1) + { + start.X++; + trimIt++; + } + } + } + } + return { start, end }; +} + +TextBuffer& SCREEN_INFORMATION::GetTextBuffer() noexcept +{ + return *_textBuffer; +} + +const TextBuffer& SCREEN_INFORMATION::GetTextBuffer() const noexcept +{ + return *_textBuffer; +} + +TextBufferTextIterator SCREEN_INFORMATION::GetTextDataAt(const COORD at) const +{ + return _textBuffer->GetTextDataAt(at); +} + +TextBufferCellIterator SCREEN_INFORMATION::GetCellDataAt(const COORD at) const +{ + return _textBuffer->GetCellDataAt(at); +} + +TextBufferTextIterator SCREEN_INFORMATION::GetTextLineDataAt(const COORD at) const +{ + return _textBuffer->GetTextLineDataAt(at); +} + +TextBufferCellIterator SCREEN_INFORMATION::GetCellLineDataAt(const COORD at) const +{ + return _textBuffer->GetCellLineDataAt(at); +} + +TextBufferTextIterator SCREEN_INFORMATION::GetTextDataAt(const COORD at, const Viewport limit) const +{ + return _textBuffer->GetTextDataAt(at, limit); +} + +TextBufferCellIterator SCREEN_INFORMATION::GetCellDataAt(const COORD at, const Viewport limit) const +{ + return _textBuffer->GetCellDataAt(at, limit); +} + +// Method Description: +// - Updates our internal "virtual bottom" tracker with wherever the viewport +// currently is. +// - +// Return Value: +// - +void SCREEN_INFORMATION::UpdateBottom() +{ + _virtualBottom = _viewport.BottomInclusive(); +} + +// Method Description: +// - Initialize the row with the cursor on it to the current text attributes. +// This is executed when we move the cursor below the current viewport in +// VT mode. When that happens in a real terminal, the line is brand new, +// so it gets initialized for the first time with the current attributes. +// Our rows are usually pre-initialized, so re-initialize it here to +// emulate that behavior. +// See MSFT:17415310. +// Arguments: +// - +// Return Value: +// - +void SCREEN_INFORMATION::InitializeCursorRowAttributes() +{ + if (_textBuffer) + { + const auto& cursor = _textBuffer->GetCursor(); + ROW& row = _textBuffer->GetRowByOffset(cursor.GetPosition().Y); + row.GetAttrRow().SetAttrToEnd(0, GetAttributes()); + } +} + +// Method Description: +// - Moves the viewport to where we last believed the "virtual bottom" was. This +// emulates linux terminal behavior, where there's no buffer, only a +// viewport. This is called by WriteChars, on output from an application in +// VT mode, before the output is processed by the state machine. +// This ensures that if a user scrolls around in the buffer, and a client +// application uses VT to control the cursor/buffer, those commands are +// still processed relative to the coordinates before the user scrolled the buffer. +// Arguments: +// - +// Return Value: +// - +void SCREEN_INFORMATION::MoveToBottom() +{ + const auto virtualView = GetVirtualViewport(); + LOG_IF_NTSTATUS_FAILED(SetViewportOrigin(true, virtualView.Origin(), true)); +} + +// Method Description: +// - Returns the "virtual" Viewport - the viewport with it's bottom at +// `_virtualBottom`. For VT operations, this is essentially the mutable +// section of the buffer. +// Arguments: +// - +// Return Value: +// - the virtual terminal viewport +Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept +{ + const short newTop = _virtualBottom - _viewport.Height() + 1; + return Viewport::FromDimensions({0, newTop}, _viewport.Dimensions()); +} + +// Method Description: +// - Returns true if the character at the cursor's current position is wide. +// See IsGlyphFullWidth +// Arguments: +// - +// Return Value: +// - true if the character at the cursor's current position is wide +bool SCREEN_INFORMATION::CursorIsDoubleWidth() const +{ + const auto& buffer = GetTextBuffer(); + const auto position = buffer.GetCursor().GetPosition(); + TextBufferTextIterator it(TextBufferCellIterator(buffer, position)); + return IsGlyphFullWidth(*it); +} + +// Method Description: +// - Retrieves this buffer's current render target. +// Arguments: +// - +// Return Value: +// - This buffer's current render target. +IRenderTarget& SCREEN_INFORMATION::GetRenderTarget() noexcept +{ + return _renderTarget; +} + +// Method Description: +// - Gets the current font of the screen buffer. +// Arguments: +// - +// Return Value: +// - A FontInfo describing our current font. +FontInfo& SCREEN_INFORMATION::GetCurrentFont() noexcept +{ + return _currentFont; +} + +// Method Description: +// See the non-const version of this function. +const FontInfo& SCREEN_INFORMATION::GetCurrentFont() const noexcept +{ + return _currentFont; +} + +// Method Description: +// - Gets the desired font of the screen buffer. If we try loading this font and +// have to fallback to another, then GetCurrentFont()!=GetDesiredFont(). +// We store this seperately, so that if we need to reload the font, we can +// try again with our prefered font info (in the desired font info) instead +// of re-using the looked up value from before. +// Arguments: +// - +// Return Value: +// - A FontInfo describing our desired font. +FontInfoDesired& SCREEN_INFORMATION::GetDesiredFont() noexcept +{ + return _desiredFont; +} + +// Method Description: +// See the non-const version of this function. +const FontInfoDesired& SCREEN_INFORMATION::GetDesiredFont() const noexcept +{ + return _desiredFont; +} + +// Method Description: +// - Returns true iff the scroll margins have been set. +// Arguments: +// - +// Return Value: +// - true iff the scroll margins have been set. +bool SCREEN_INFORMATION::AreMarginsSet() const noexcept +{ + return _scrollMargins.BottomInclusive() > _scrollMargins.Top(); +} + +// Method Description: +// - Gets the region of the buffer that should be used for scrolling within the +// scroll margins. If the scroll margins aren't set, it returns the entire +// buffer size. +// Arguments: +// - +// Return Value: +// - The area of the buffer within the scroll margins +Viewport SCREEN_INFORMATION::GetScrollingRegion() const noexcept +{ + const auto buffer = GetBufferSize(); + const bool marginsSet = AreMarginsSet(); + const auto marginRect = GetAbsoluteScrollMargins().ToInclusive(); + const auto margin = Viewport::FromInclusive({ buffer.Left(), + marginsSet ? marginRect.Top : buffer.Top(), + buffer.RightInclusive(), + marginsSet ? marginRect.Bottom : buffer.BottomInclusive()}); + return margin; +} diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp new file mode 100644 index 000000000..aaa3bd485 --- /dev/null +++ b/src/host/screenInfo.hpp @@ -0,0 +1,331 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- screenInfo.hpp + +Abstract: +- This module represents the structures and functions required + for rendering one screen of the console host window. + +Author(s): +- Michael Niksa (MiNiksa) 10-Apr-2014 +- Paul Campbell (PaulCam) 10-Apr-2014 + +Revision History: +- From components of output.h/.c and resize.c by Therese Stowell (ThereseS) 1990-1991 +--*/ + +#pragma once + +#include "conapi.h" +#include "settings.hpp" +#include "outputStream.hpp" +#include "ScreenBufferRenderTarget.hpp" + +#include "../buffer/out/OutputCellRect.hpp" +#include "../buffer/out/TextAttribute.hpp" +#include "../buffer/out/textBuffer.hpp" +#include "../buffer/out/textBufferCellIterator.hpp" +#include "../buffer/out/textBufferTextIterator.hpp" + +#include "IIoProvider.hpp" +#include "outputStream.hpp" +#include "../terminal/adapter/adaptDispatch.hpp" +#include "../terminal/parser/stateMachine.hpp" +#include "../terminal/parser/OutputStateMachineEngine.hpp" + +#include "../server/ObjectHeader.h" + +#include "../interactivity/inc/IAccessibilityNotifier.hpp" +#include "../interactivity/inc/IConsoleWindow.hpp" +#include "../interactivity/inc/IWindowMetrics.hpp" + +#include "../inc/ITerminalOutputConnection.hpp" + +#include "../renderer/inc/FontInfo.hpp" +#include "../renderer/inc/FontInfoDesired.hpp" + +#include "../types/inc/Viewport.hpp" + +using namespace Microsoft::Console::Interactivity; +using namespace Microsoft::Console::VirtualTerminal; + +class ConversionAreaInfo; // forward decl window. circular reference + +class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console::IIoProvider +{ +public: + + [[nodiscard]] + static NTSTATUS CreateInstance(_In_ COORD coordWindowSize, + const FontInfo fontInfo, + _In_ COORD coordScreenBufferSize, + const TextAttribute defaultAttributes, + const TextAttribute popupAttributes, + const UINT uiCursorSize, + _Outptr_ SCREEN_INFORMATION ** const ppScreen); + + ~SCREEN_INFORMATION(); + + void GetScreenBufferInformation(_Out_ PCOORD pcoordSize, + _Out_ PCOORD pcoordCursorPosition, + _Out_ PSMALL_RECT psrWindow, + _Out_ PWORD pwAttributes, + _Out_ PCOORD pcoordMaximumWindowSize, + _Out_ PWORD pwPopupAttributes, + _Out_writes_(COLOR_TABLE_SIZE) LPCOLORREF lpColorTable) const; + + void GetRequiredConsoleSizeInPixels(_Out_ PSIZE const pRequiredSize) const; + + void MakeCurrentCursorVisible(); + + void ClipToScreenBuffer(_Inout_ SMALL_RECT* const psrClip) const; + + COORD GetMinWindowSizeInCharacters(const COORD coordFontSize = { 1, 1 }) const; + COORD GetMaxWindowSizeInCharacters(const COORD coordFontSize = { 1, 1 }) const; + COORD GetLargestWindowSizeInCharacters(const COORD coordFontSize = { 1, 1 }) const; + COORD GetScrollBarSizesInCharacters() const; + + Microsoft::Console::Types::Viewport GetBufferSize() const; + Microsoft::Console::Types::Viewport GetTerminalBufferSize() const; + + COORD GetScreenFontSize() const; + void UpdateFont(const FontInfo* const pfiNewFont); + void RefreshFontWithRenderer(); + + [[nodiscard]] + NTSTATUS ResizeScreenBuffer(const COORD coordNewScreenSize, const bool fDoScrollBarUpdate); + + void NotifyAccessibilityEventing(const short sStartX, const short sStartY, const short sEndX, const short sEndY); + + void UpdateScrollBars(); + void InternalUpdateScrollBars(); + + bool IsMaximizedBoth() const; + bool IsMaximizedX() const; + bool IsMaximizedY() const; + + const Microsoft::Console::Types::Viewport& GetViewport() const noexcept; + void SetViewport(const Microsoft::Console::Types::Viewport& newViewport, const bool updateBottom); + Microsoft::Console::Types::Viewport GetVirtualViewport() const noexcept; + + void ProcessResizeWindow(const RECT* const prcClientNew, const RECT* const prcClientOld); + void SetViewportSize(const COORD* const pcoordSize); + + // Forwarders to Window if we're the active buffer. + [[nodiscard]] + NTSTATUS SetViewportOrigin(const bool fAbsolute, const COORD coordWindowOrigin, const bool updateBottom); + + bool SendNotifyBeep() const; + bool PostUpdateWindowSize() const; + + bool InVTMode() const; + + // TODO: MSFT 9355062 these methods should probably be a part of construction/destruction. http://osgvsowi/9355062 + static void s_InsertScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); + static void s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); + + OutputCellRect ReadRect(const Microsoft::Console::Types::Viewport location) const; + + TextBufferCellIterator GetCellDataAt(const COORD at) const; + TextBufferCellIterator GetCellLineDataAt(const COORD at) const; + TextBufferCellIterator GetCellDataAt(const COORD at, const Microsoft::Console::Types::Viewport limit) const; + TextBufferTextIterator GetTextDataAt(const COORD at) const; + TextBufferTextIterator GetTextLineDataAt(const COORD at) const; + TextBufferTextIterator GetTextDataAt(const COORD at, const Microsoft::Console::Types::Viewport limit) const; + + OutputCellIterator Write(const OutputCellIterator it); + + OutputCellIterator Write(const OutputCellIterator it, + const COORD target); + + OutputCellIterator WriteRect(const OutputCellIterator it, + const Microsoft::Console::Types::Viewport viewport); + + void WriteRect(const OutputCellRect& data, + const COORD location); + + void ClearTextData(); + + std::pair GetWordBoundary(const COORD position) const; + + TextBuffer& GetTextBuffer() noexcept; + const TextBuffer& GetTextBuffer() const noexcept; + +#pragma region IIoProvider + SCREEN_INFORMATION& GetActiveOutputBuffer() override; + const SCREEN_INFORMATION& GetActiveOutputBuffer() const override; + InputBuffer* const GetActiveInputBuffer() const override; +#pragma endregion + + bool CursorIsDoubleWidth() const; + + DWORD OutputMode; + WORD ResizingWindow; // > 0 if we should ignore WM_SIZE messages + + short WheelDelta; + short HWheelDelta; +private: + std::unique_ptr _textBuffer; +public: + SCREEN_INFORMATION *Next; + BYTE WriteConsoleDbcsLeadByte[2]; + BYTE FillOutDbcsLeadChar; + WCHAR LineChar[6]; +#define UPPER_LEFT_CORNER 0 +#define UPPER_RIGHT_CORNER 1 +#define HORIZONTAL_LINE 2 +#define VERTICAL_LINE 3 +#define BOTTOM_LEFT_CORNER 4 +#define BOTTOM_RIGHT_CORNER 5 + + // non ownership pointer + ConversionAreaInfo* ConvScreenInfo; + + UINT ScrollScale; + + bool IsActiveScreenBuffer() const; + + const StateMachine& GetStateMachine() const; + StateMachine& GetStateMachine(); + + void SetCursorInformation(const ULONG Size, + const bool Visible) noexcept; + + void SetCursorColor(const unsigned int Color, const bool setMain = false) noexcept; + + void SetCursorType(const CursorType Type, const bool setMain = false) noexcept; + + void SetCursorDBMode(const bool DoubleCursor); + [[nodiscard]] + NTSTATUS SetCursorPosition(const COORD Position, const bool TurnOn); + + void MakeCursorVisible(const COORD CursorPosition, const bool updateBottom = true); + + Microsoft::Console::Types::Viewport GetRelativeScrollMargins() const; + Microsoft::Console::Types::Viewport GetAbsoluteScrollMargins() const; + void SetScrollMargins(const Microsoft::Console::Types::Viewport margins); + bool AreMarginsSet() const noexcept; + Microsoft::Console::Types::Viewport GetScrollingRegion() const noexcept; + + [[nodiscard]] + NTSTATUS UseAlternateScreenBuffer(); + void UseMainScreenBuffer(); + + SCREEN_INFORMATION& GetMainBuffer(); + const SCREEN_INFORMATION& GetMainBuffer() const; + + SCREEN_INFORMATION& GetActiveBuffer(); + const SCREEN_INFORMATION& GetActiveBuffer() const; + + void AddTabStop(const SHORT sColumn); + void ClearTabStops() noexcept; + void ClearTabStop(const SHORT sColumn) noexcept; + COORD GetForwardTab(const COORD cCurrCursorPos) const noexcept; + COORD GetReverseTab(const COORD cCurrCursorPos) const noexcept; + bool AreTabsSet() const noexcept; + void SetDefaultVtTabStops(); + + TextAttribute GetAttributes() const; + const TextAttribute* const GetPopupAttributes() const; + + void SetAttributes(const TextAttribute& attributes); + void SetPopupAttributes(const TextAttribute& popupAttributes); + void SetDefaultAttributes(const TextAttribute& attributes, + const TextAttribute& popupAttributes); + + [[nodiscard]] + HRESULT VtEraseAll(); + + void SetTerminalConnection(_In_ Microsoft::Console::ITerminalOutputConnection* const pTtyConnection); + + void UpdateBottom(); + void MoveToBottom(); + + Microsoft::Console::Render::IRenderTarget& GetRenderTarget() noexcept; + + FontInfo& GetCurrentFont() noexcept; + const FontInfo& GetCurrentFont() const noexcept; + + FontInfoDesired& GetDesiredFont() noexcept; + const FontInfoDesired& GetDesiredFont() const noexcept; + + void InitializeCursorRowAttributes(); + +private: + SCREEN_INFORMATION(_In_ IWindowMetrics *pMetrics, + _In_ IAccessibilityNotifier *pNotifier, + const TextAttribute popupAttributes, + const FontInfo fontInfo); + + IWindowMetrics *_pConsoleWindowMetrics; + IAccessibilityNotifier *_pAccessibilityNotifier; + + [[nodiscard]] + HRESULT _AdjustScreenBufferHelper(const RECT* const prcClientNew, + const COORD coordBufferOld, + _Out_ COORD* const pcoordClientNewCharacters); + [[nodiscard]] + HRESULT _AdjustScreenBuffer(const RECT* const prcClientNew); + void _CalculateViewportSize(const RECT* const prcClientArea, _Out_ COORD* const pcoordSize); + void _AdjustViewportSize(const RECT* const prcClientNew, const RECT* const prcClientOld, const COORD* const pcoordSize); + void _InternalSetViewportSize(const COORD* const pcoordSize, const bool fResizeFromTop, const bool fResizeFromLeft); + + static void s_CalculateScrollbarVisibility(const RECT* const prcClientArea, + const COORD* const pcoordBufferSize, + const COORD* const pcoordFontSize, + _Out_ bool* const pfIsHorizontalVisible, + _Out_ bool* const pfIsVerticalVisible); + + [[nodiscard]] + NTSTATUS ResizeWithReflow(const COORD coordnewScreenSize); + [[nodiscard]] + NTSTATUS ResizeTraditional(const COORD coordNewScreenSize); + + [[nodiscard]] + NTSTATUS _InitializeOutputStateMachine(); + void _FreeOutputStateMachine(); + + [[nodiscard]] + NTSTATUS _CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer); + + bool _IsAltBuffer() const; + bool _IsInPtyMode() const; + + std::shared_ptr _stateMachine; + + Microsoft::Console::Types::Viewport _scrollMargins; //The margins of the VT specified scroll region. Left and Right are currently unused, but could be in the future. + + // Specifies which coordinates of the screen buffer are visible in the + // window client (the "viewport" into the buffer) + Microsoft::Console::Types::Viewport _viewport; + + SCREEN_INFORMATION* _psiAlternateBuffer; // The VT "Alternate" screen buffer. + SCREEN_INFORMATION* _psiMainBuffer; // A pointer to the main buffer, if this is the alternate buffer. + + RECT _rcAltSavedClientNew; + RECT _rcAltSavedClientOld; + bool _fAltWindowChanged; + + std::list _tabStops; + + TextAttribute _PopupAttributes; + + FontInfo _currentFont; + FontInfoDesired _desiredFont; + + // Tracks the last virtual position the viewport was at. This is not + // affected by the user scrolling the viewport, only when API calls cause + // the viewport to move (SetBufferInfo, WriteConsole, etc) + short _virtualBottom; + + ScreenBufferRenderTarget _renderTarget; + +#ifdef UNIT_TESTING + friend class TextBufferIteratorTests; + friend class ScreenBufferTests; + friend class CommonState; +#endif +}; diff --git a/src/host/scrolling.cpp b/src/host/scrolling.cpp new file mode 100644 index 000000000..fdf249fb8 --- /dev/null +++ b/src/host/scrolling.cpp @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "scrolling.hpp" + +#include "selection.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +ULONG Scrolling::s_ucWheelScrollLines = 0; +ULONG Scrolling::s_ucWheelScrollChars = 0; + +void Scrolling::s_UpdateSystemMetrics() +{ + s_ucWheelScrollLines = ServiceLocator::LocateSystemConfigurationProvider()->GetNumberOfWheelScrollLines(); + s_ucWheelScrollChars = ServiceLocator::LocateSystemConfigurationProvider()->GetNumberOfWheelScrollCharacters(); +} + +bool Scrolling::s_IsInScrollMode() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return WI_IsFlagSet(gci.Flags, CONSOLE_SCROLLING); +} + +void Scrolling::s_DoScroll() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (!s_IsInScrollMode()) + { + // clear any selection we may have -- can't scroll and select at the same time + Selection::Instance().ClearSelection(); + + WI_SetFlag(gci.Flags, CONSOLE_SCROLLING); + + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + } + } +} + +void Scrolling::s_ClearScroll() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + WI_ClearFlag(gci.Flags, CONSOLE_SCROLLING); + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + } +} + +void Scrolling::s_ScrollIfNecessary(const SCREEN_INFORMATION& ScreenInfo) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + FAIL_FAST_IF_NULL(pWindow); + + Selection* const pSelection = &Selection::Instance(); + + if (pSelection->IsInSelectingState() && pSelection->IsMouseButtonDown()) + { + POINT CursorPos; + if (!pWindow->GetCursorPosition(&CursorPos)) + { + return; + } + + RECT ClientRect; + if (!pWindow->GetClientRectangle(&ClientRect)) + { + return; + } + + pWindow->MapPoints((LPPOINT) & ClientRect, 2); + if (!(s_IsPointInRectangle(&ClientRect, CursorPos))) + { + pWindow->ConvertScreenToClient(&CursorPos); + + COORD MousePosition; + MousePosition.X = (SHORT)CursorPos.x; + MousePosition.Y = (SHORT)CursorPos.y; + + COORD coordFontSize = ScreenInfo.GetScreenFontSize(); + + MousePosition.X /= coordFontSize.X; + MousePosition.Y /= coordFontSize.Y; + + MousePosition.X += ScreenInfo.GetViewport().Left(); + MousePosition.Y += ScreenInfo.GetViewport().Top(); + + pSelection->ExtendSelection(MousePosition); + } + } +} + +void Scrolling::s_HandleMouseWheel(_In_ bool isMouseWheel, + _In_ bool isMouseHWheel, + _In_ short wheelDelta, + _In_ bool hasShift, + SCREEN_INFORMATION& ScreenInfo) +{ + COORD NewOrigin = ScreenInfo.GetViewport().Origin(); + + // s_ucWheelScrollLines == 0 means that it is turned off. + if (isMouseWheel && s_ucWheelScrollLines > 0) + { + // Rounding could cause this to be zero if gucWSL is bigger than 240 or so. + ULONG const ulActualDelta = std::max(WHEEL_DELTA / s_ucWheelScrollLines, 1ul); + + // If we change direction we need to throw away any remainder we may have in the other direction. + if ((ScreenInfo.WheelDelta > 0) == (wheelDelta > 0)) + { + ScreenInfo.WheelDelta += wheelDelta; + } + else + { + ScreenInfo.WheelDelta = wheelDelta; + } + + if ((ULONG)abs(ScreenInfo.WheelDelta) >= ulActualDelta) + { + /* + * By default, SHIFT + WM_MOUSEWHEEL will scroll 1/2 the + * screen size. A ScrollScale of 1 indicates 1/2 the screen + * size. This value can be modified in the registry. + */ + SHORT delta = 1; + if (hasShift) + { + delta = gsl::narrow(std::max((ScreenInfo.GetViewport().Height() * ScreenInfo.ScrollScale) / 2, 1u)); + + // Account for scroll direction changes by adjusting delta if there was a direction change. + delta *= (ScreenInfo.WheelDelta < 0 ? -1 : 1); + ScreenInfo.WheelDelta %= delta; + } + else + { + delta *= (ScreenInfo.WheelDelta / (short)ulActualDelta); + ScreenInfo.WheelDelta %= ulActualDelta; + } + + NewOrigin.Y -= delta; + const COORD coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); + if (NewOrigin.Y < 0) + { + NewOrigin.Y = 0; + } + else if (NewOrigin.Y + ScreenInfo.GetViewport().Height() > coordBufferSize.Y) + { + NewOrigin.Y = coordBufferSize.Y - ScreenInfo.GetViewport().Height(); + } + LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); + } + } + else if (isMouseHWheel && s_ucWheelScrollChars > 0) + { + ULONG const ulActualDelta = std::max(WHEEL_DELTA / s_ucWheelScrollChars, 1ul); + + if ((ScreenInfo.HWheelDelta > 0) == (wheelDelta > 0)) + { + ScreenInfo.HWheelDelta += wheelDelta; + } + else + { + ScreenInfo.HWheelDelta = wheelDelta; + } + + if ((ULONG)abs(ScreenInfo.HWheelDelta) >= ulActualDelta) + { + SHORT delta = 1; + + if (hasShift) + { + delta = std::max(ScreenInfo.GetViewport().RightInclusive(), 1i16); + } + + delta *= (ScreenInfo.HWheelDelta / (short)ulActualDelta); + ScreenInfo.HWheelDelta %= ulActualDelta; + + NewOrigin.X += delta; + const COORD coordBufferSize = ScreenInfo.GetBufferSize().Dimensions(); + if (NewOrigin.X < 0) + { + NewOrigin.X = 0; + } + else if (NewOrigin.X + ScreenInfo.GetViewport().Width() > coordBufferSize.X) + { + NewOrigin.X = coordBufferSize.X - ScreenInfo.GetViewport().Width(); + } + + LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); + } + } +} + +bool Scrolling::s_HandleKeyScrollingEvent(const INPUT_KEY_INFO* const pKeyInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + FAIL_FAST_IF_NULL(pWindow); + + const WORD VirtualKeyCode = pKeyInfo->GetVirtualKey(); + const bool fIsCtrlPressed = pKeyInfo->IsCtrlPressed(); + const bool fIsEditLineEmpty = CommandLine::Instance().IsEditLineEmpty(); + + // If escape, enter or ctrl-c, cancel scroll. + if (VirtualKeyCode == VK_ESCAPE || + VirtualKeyCode == VK_RETURN || + (VirtualKeyCode == 'C' && fIsCtrlPressed)) + { + Scrolling::s_ClearScroll(); + } + else + { + WORD ScrollCommand; + BOOL Horizontal = FALSE; + switch (VirtualKeyCode) + { + case VK_UP: + { + ScrollCommand = SB_LINEUP; + break; + } + case VK_DOWN: + { + ScrollCommand = SB_LINEDOWN; + break; + } + case VK_LEFT: + { + ScrollCommand = SB_LINEUP; + Horizontal = TRUE; + break; + } + case VK_RIGHT: + { + ScrollCommand = SB_LINEDOWN; + Horizontal = TRUE; + break; + } + case VK_NEXT: + { + ScrollCommand = SB_PAGEDOWN; + break; + } + case VK_PRIOR: + { + ScrollCommand = SB_PAGEUP; + break; + } + case VK_END: + { + if (fIsCtrlPressed) + { + if (fIsEditLineEmpty) + { + // Ctrl-End when edit line is empty will scroll to last line in the buffer. + gci.GetActiveOutputBuffer().MakeCurrentCursorVisible(); + return true; + } + else + { + // If edit line is non-empty, we won't handle this so it can modify the edit line (trim characters to end of line from cursor pos). + return false; + } + } + else + { + ScrollCommand = SB_PAGEDOWN; + Horizontal = TRUE; + } + break; + } + case VK_HOME: + { + if (fIsCtrlPressed) + { + if (fIsEditLineEmpty) + { + // Ctrl-Home when edit line is empty will scroll to top of buffer. + ScrollCommand = SB_TOP; + } + else + { + // If edit line is non-empty, we won't handle this so it can modify the edit line (trim characters to beginning of line from cursor pos). + return false; + } + } + else + { + ScrollCommand = SB_PAGEUP; + Horizontal = TRUE; + } + break; + } + case VK_SHIFT: + case VK_CONTROL: + case VK_MENU: + { + return true; + } + default: + { + pWindow->SendNotifyBeep(); + return true; + } + } + + if (Horizontal) + { + pWindow->HorizontalScroll(ScrollCommand, 0); + } + else + { + pWindow->VerticalScroll(ScrollCommand, 0); + } + } + + return true; +} + +BOOL Scrolling::s_IsPointInRectangle(const RECT* const prc, const POINT pt) +{ + return ((pt.x >= prc->left) && (pt.x < prc->right) && + (pt.y >= prc->top) && (pt.y < prc->bottom)); +} diff --git a/src/host/scrolling.hpp b/src/host/scrolling.hpp new file mode 100644 index 000000000..71b10cc42 --- /dev/null +++ b/src/host/scrolling.hpp @@ -0,0 +1,46 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- scrolling.hpp + +Abstract: +- This module is used for managing the scrolling state and process + +Author(s): +- Michael Niksa (MiNiksa) 4-Jun-2014 +- Paul Campbell (PaulCam) 4-Jun-2014 + +Revision History: +- From components of clipbrd.h/.c and input.h/.c +--*/ + +#pragma once + +#include "input.h" + +// TODO: Static methods generally mean they're getting their state globally and not from this object _yet_ +class Scrolling +{ +public: + static void s_UpdateSystemMetrics(); + + // legacy methods (not refactored yet) + static bool s_IsInScrollMode(); + static void s_DoScroll(); + static void s_ClearScroll(); + static void s_ScrollIfNecessary(const SCREEN_INFORMATION& ScreenInfo); + static void s_HandleMouseWheel(_In_ bool isMouseWheel, + _In_ bool isMouseHWheel, + _In_ short wheelDelta, + _In_ bool hasShift, + SCREEN_INFORMATION& ScreenInfo); + static bool s_HandleKeyScrollingEvent(const INPUT_KEY_INFO* const pKeyInfo); + +private: + static BOOL s_IsPointInRectangle(const RECT* const prc, const POINT pt); + + static ULONG s_ucWheelScrollLines; + static ULONG s_ucWheelScrollChars; +}; diff --git a/src/host/search.cpp b/src/host/search.cpp new file mode 100644 index 000000000..2d80933f8 --- /dev/null +++ b/src/host/search.cpp @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "search.h" + +#include "dbcs.h" +#include "../buffer/out/CharRow.hpp" +#include "../types/inc/Utf16Parser.hpp" +#include "../types/inc/GlyphWidth.hpp" + +// Routine Description: +// - Constructs a Search object. +// - Make a Search object then call .FindNext() to locate items. +// - Once you've found something, you can perfom actions like .Select() or .Color() +// Arguments: +// - screenInfo - The screen buffer to search through (the "haystack") +// - str - The search term you want to find (the "needle") +// - direction - The direction to search (upward or downward) +// - sensitivity - Whether or not you care about case +Search::Search(const SCREEN_INFORMATION& screenInfo, + const std::wstring& str, + const Direction direction, + const Sensitivity sensitivity) : + _direction(direction), + _sensitivity(sensitivity), + _screenInfo(screenInfo), + _needle(s_CreateNeedleFromString(str)), + _coordAnchor(s_GetInitialAnchor(screenInfo, direction)) +{ + _coordNext = _coordAnchor; +} + +// Routine Description: +// - Constructs a Search object. +// - Make a Search object then call .FindNext() to locate items. +// - Once you've found something, you can perfom actions like .Select() or .Color() +// Arguments: +// - screenInfo - The screen buffer to search through (the "haystack") +// - str - The search term you want to find (the "needle") +// - direction - The direction to search (upward or downward) +// - sensitivity - Whether or not you care about case +// - anchor - starting search location in screenInfo +Search::Search(const SCREEN_INFORMATION& screenInfo, + const std::wstring& str, + const Direction direction, + const Sensitivity sensitivity, + const COORD anchor) : + _direction(direction), + _sensitivity(sensitivity), + _screenInfo(screenInfo), + _needle(s_CreateNeedleFromString(str)), + _coordAnchor(anchor) +{ + _coordNext = _coordAnchor; +} + +// Routine Description +// - Locates the next instance of the search term within the screen buffer. +// Arguments: +// - - Uses internal state from constructor +// Return Value: +// - True if we found another item. False if we've reached the end of the buffer. +// - NOTE: You can FindNext() again after False to go around the buffer again. +bool Search::FindNext() +{ + if (_reachedEnd) + { + _reachedEnd = false; + return false; + } + + do + { + if (_FindNeedleInHaystackAt(_coordNext, _coordSelStart, _coordSelEnd)) + { + _UpdateNextPosition(); + _reachedEnd = _coordNext == _coordAnchor; + return true; + } + else + { + _UpdateNextPosition(); + } + + } while (_coordNext != _coordAnchor); + + return false; +} + +// Routine Description: +// - Takes the found word and selects it in the screen buffer +void Search::Select() const +{ + // Only select if we've found something. + if (_coordSelStart != _coordSelEnd) + { + Selection::Instance().SelectNewRegion(_coordSelStart, _coordSelEnd); + } +} + +// Routine Description: +// - Takes the found word and applies the given color to it in the screen buffer +// Arguments: +// - ulAttr - The legacy color attribute to apply to the word +void Search::Color(const TextAttribute attr) const +{ + // Only select if we've found something. + if (_coordSelStart != _coordSelEnd) + { + Selection::Instance().ColorSelection(_coordSelStart, _coordSelEnd, attr); + } +} + +// Routine Description: +// - gets start and end position of text sound by search. only guaranteed to have valid data if FindNext has +// been called and returned true. +// Return Value: +// - pair containing [start, end] coord positions of text found by search +std::pair Search::GetFoundLocation() const noexcept +{ + return { _coordSelStart, _coordSelEnd }; +} + +// Routine Description: +// - Finds the anchor position where we will start searches from. +// - This position will represent the "wrap around" point in the buffer or where +// we reach the end of our search. +// - If the screen buffer given already has a selection in it, it will be used to determine the anchor. +// - Otherwise, we will choose one of the ends of the screen buffer depending on direction. +// Arguments: +// - screenInfo - The screen buffer for determining the anchor +// - direction - The intended direction of the search +// Return Value: +// - Coordinate to start the search from. +COORD Search::s_GetInitialAnchor(const SCREEN_INFORMATION& screenInfo, const Direction direction) +{ + if (Selection::Instance().IsInSelectingState()) + { + auto anchor = Selection::Instance().GetSelectionAnchor(); + if (direction == Direction::Forward) + { + screenInfo.GetBufferSize().IncrementInBoundsCircular(anchor); + } + else + { + screenInfo.GetBufferSize().DecrementInBoundsCircular(anchor); + } + return anchor; + } + else + { + if (direction == Direction::Forward) + { + return { 0, 0 }; + } + else + { + const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); + return { bufferSize.X - 1, bufferSize.Y - 1 }; + } + } +} + +// Routine Description: +// - Attempts to compare the search term (the needle) to the screen buffer (the haystack) +// at the given coordinate position of the screen buffer. +// - Performs one comparison. Call again with new positions to check other spots. +// Arguments: +// - pos - The position in the haystack (screen buffer) to compare +// - start - If we found it, this is filled with the coordinate of the first character of the needle. +// - end - If we found it, this is filled with the coordinate of the last character of the needle. +// Return Value: +// - True if we found it. False if not. +bool Search::_FindNeedleInHaystackAt(const COORD pos, COORD& start, COORD& end) const +{ + start = { 0 }; + end = { 0 }; + + COORD bufferPos = pos; + + for (const auto& needleCell : _needle) + { + // Haystack is the buffer. Needle is the string we were given. + const auto hayIter = _screenInfo.GetTextDataAt(bufferPos); + const auto hayChars = *hayIter; + const auto needleChars = std::wstring_view(needleCell.data(), needleCell.size()); + + // If we didn't match at any point of the needle, return false. + if (!_CompareChars(hayChars, needleChars)) + { + return false; + } + + _IncrementCoord(bufferPos); + } + + _DecrementCoord(bufferPos); + + // If we made it the whole way through the needle, then it was in the haystack. + // Fill out the span that we found the result at and return true. + start = pos; + end = bufferPos; + + return true; +} + +// Routine Description: +// - Provides an abstraction for comparing two spans of text. +// - Internally handles case sensitivity based on object construction. +// Arguments: +// - one - String view representing the first string of text +// - two - String view representing the second string of text +// Return Value: +// - True if they are the same. False otherwise. +bool Search::_CompareChars(const std::wstring_view one, const std::wstring_view two) const +{ + if (one.size() != two.size()) + { + return false; + } + + for (size_t i = 0; i < one.size(); i++) + { + if (_ApplySensitivity(one[i]) != _ApplySensitivity(two[i])) + { + return false; + } + } + + return true; +} + +// Routine Description: +// - Provides an abstraction for conditionally applying case sensitivity +// based on object construction +// Arguments: +// - wch - Character to adjust if necessary +// Return Value: +// - Adjusted value (or not). +wchar_t Search::_ApplySensitivity(const wchar_t wch) const +{ + if (_sensitivity == Sensitivity::CaseInsensitive) + { + return ::towlower(wch); + } + else + { + return wch; + } +} + +// Routine Description: +// - Helper to increment a coordinate in respect to the associated screen buffer +// Arguments +// - coord - Updated by function to increment one position (will wrap X and Y direction) +void Search::_IncrementCoord(COORD& coord) const +{ + _screenInfo.GetBufferSize().IncrementInBoundsCircular(coord); +} + +// Routine Description: +// - Helper to decrement a coordinate in respect to the associated screen buffer +// Arguments +// - coord - Updated by function to decrement one position (will wrap X and Y direction) +void Search::_DecrementCoord(COORD& coord) const +{ + _screenInfo.GetBufferSize().DecrementInBoundsCircular(coord); +} + +// Routine Description: +// - Helper to update the coordinate position to the next point to be searched +// Return Value: +// - True if we haven't reached the end of the buffer. False otherwise. +void Search::_UpdateNextPosition() +{ + if (_direction == Direction::Forward) + { + _IncrementCoord(_coordNext); + } + else if (_direction == Direction::Backward) + { + _DecrementCoord(_coordNext); + } + else + { + THROW_HR(E_NOTIMPL); + } +} + +// Routine Description: +// - Creates a "needle" of the correct format for comparison to the screen buffer text data +// that we can use for our search +// Arguments: +// - wstr - String that will be our search term +// Return Value: +// - Structured text data for comparison to screen buffer text data. +std::vector> Search::s_CreateNeedleFromString(const std::wstring& wstr) +{ + const auto charData = Utf16Parser::Parse(wstr); + std::vector> cells; + for (const auto chars : charData) + { + if (IsGlyphFullWidth(std::wstring_view{ chars.data(), chars.size() })) + { + cells.emplace_back(chars); + } + cells.emplace_back(chars); + } + return cells; +} diff --git a/src/host/search.h b/src/host/search.h new file mode 100644 index 000000000..50ce34db5 --- /dev/null +++ b/src/host/search.h @@ -0,0 +1,82 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- search.h + +Abstract: +- This module is used for searching through the screen for a substring + +Author(s): +- Michael Niksa (MiNiksa) 20-Apr-2018 + +Revision History: +- From components of find.c by Jerry Shea (jerrysh) 1-May-1997 +--*/ + +#pragma once + +// This used to be in find.h. +#define SEARCH_STRING_LENGTH (80) + +class Search final +{ +public: + enum class Direction + { + Forward, + Backward + }; + + enum class Sensitivity + { + CaseInsensitive, + CaseSensitive + }; + + Search(const SCREEN_INFORMATION& ScreenInfo, + const std::wstring& str, + const Direction dir, + const Sensitivity sensitivity); + + Search(const SCREEN_INFORMATION& ScreenInfo, + const std::wstring& str, + const Direction dir, + const Sensitivity sensitivity, + const COORD anchor); + + bool FindNext(); + void Select() const; + void Color(const TextAttribute attr) const; + + std::pair GetFoundLocation() const noexcept; + +private: + + wchar_t _ApplySensitivity(const wchar_t wch) const; + bool Search::_FindNeedleInHaystackAt(const COORD pos, COORD& start, COORD& end) const; + bool _CompareChars(const std::wstring_view one, const std::wstring_view two) const; + void _UpdateNextPosition(); + + void _IncrementCoord(COORD& coord) const; + void _DecrementCoord(COORD& coord) const; + + static COORD s_GetInitialAnchor(const SCREEN_INFORMATION& screenInfo, const Direction dir); + static std::vector> s_CreateNeedleFromString(const std::wstring& wstr); + + bool _reachedEnd = false; + COORD _coordNext = { 0 }; + COORD _coordSelStart = { 0 }; + COORD _coordSelEnd = { 0 }; + + const COORD _coordAnchor; + const std::vector> _needle; + const Direction _direction; + const Sensitivity _sensitivity; + const SCREEN_INFORMATION& _screenInfo; + +#ifdef UNIT_TESTING + friend class SearchTests; +#endif +}; diff --git a/src/host/selection.cpp b/src/host/selection.cpp new file mode 100644 index 000000000..839ccf556 --- /dev/null +++ b/src/host/selection.cpp @@ -0,0 +1,759 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "_output.h" +#include "stream.h" +#include "scrolling.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" + + +std::unique_ptr Selection::_instance; + +Selection::Selection() : + _fSelectionVisible(false), + _ulSavedCursorSize(0), + _fSavedCursorVisible(false), + _savedCursorColor(INVALID_COLOR), + _savedCursorType(CursorType::Legacy), + _dwSelectionFlags(0), + _fLineSelection(true), + _fUseAlternateSelection(false), + _allowMouseDragSelection{ true } +{ + ZeroMemory((void*)&_srSelectionRect, sizeof(_srSelectionRect)); + ZeroMemory((void*)&_coordSelectionAnchor, sizeof(_coordSelectionAnchor)); + ZeroMemory((void*)&_coordSavedCursorPosition, sizeof(_coordSavedCursorPosition)); +} + +Selection& Selection::Instance() +{ + if (!_instance) + { + _instance.reset(new Selection()); + } + return *_instance; +} + +// Routine Description: +// - Detemines the line-by-line selection rectangles based on global selection state. +// Arguments: +// - selectionRect - The selection rectangle outlining the region to be selected +// - selectionAnchor - The corner of the selection rectangle that selection started from +// - lineSelection - True to process in line mode. False to process in block mode. +// Return Value: +// - Returns a vector where each SMALL_RECT is one Row worth of the area to be selected. +// - Returns empty vector if no rows are selected. +// - Throws exceptions for out of memory issues +std::vector Selection::s_GetSelectionRects(const SMALL_RECT& selectionRect, + const COORD selectionAnchor, + const bool lineSelection) +{ + std::vector selectionAreas; + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& screenInfo = gci.GetActiveOutputBuffer(); + + // if the anchor (start of select) was in the top right or bottom left of the box, + // we need to remove rectangular overlap in the middle. + // e.g. + // For selections with the anchor in the top left (A) or bottom right (B), + // it is valid to maintain the inner rectangle (+) as part of the selection + // A+++++++================ + // ==============++++++++B + // + and = are valid highlights in this scenario. + // For selections with the anchor in in the top right (A) or bottom left (B), + // we must remove a portion of the first/last line that lies within the rectangle (+) + // +++++++A================= + // ==============B+++++++ + // Only = is valid for highlight in this scenario. + // This is only needed for line selection. Box selection doesn't need to account for this. + + bool removeRectPortion = false; + + if (lineSelection) + { + const auto selectionStart = selectionAnchor; + + // only if top and bottom aren't the same line... we need the whole rectangle if we're on the same line. + // e.g. A++++++++++++++B + // All the + are valid select points. + if (selectionRect.Top != selectionRect.Bottom) + { + if ((selectionStart.X == selectionRect.Right && selectionStart.Y == selectionRect.Top) || + (selectionStart.X == selectionRect.Left && selectionStart.Y == selectionRect.Bottom)) + { + removeRectPortion = true; + } + } + } + + // for each row within the selection rectangle + for (short i = selectionRect.Top; i <= selectionRect.Bottom; i++) + { + // create a rectangle representing the highlight on one row + SMALL_RECT highlightRow; + highlightRow.Top = i; + highlightRow.Bottom = i; + highlightRow.Left = selectionRect.Left; + highlightRow.Right = selectionRect.Right; + + // compensate for line selection by extending one or both ends of the rectangle to the edge + if (lineSelection) + { + // if not the first row, pad the left selection to the buffer edge + if (i != selectionRect.Top) + { + highlightRow.Left = 0; + } + + // if not the last row, pad the right selection to the buffer edge + if (i != selectionRect.Bottom) + { + highlightRow.Right = screenInfo.GetBufferSize().RightInclusive(); + } + + // if we've determined we're in a scenario where we must remove the inner rectangle from the lines... + if (removeRectPortion) + { + if (i == selectionRect.Top) + { + // from the top row, move the left edge of the highlight line to the right edge of the rectangle + highlightRow.Left = selectionRect.Right; + } + else if (i == selectionRect.Bottom) + { + // from the bottom row, move the right edge of the highlight line to the left edge of the rectangle + highlightRow.Right = selectionRect.Left; + } + } + } + + // compensate for double width characters by calling double-width measuring/limiting function + const COORD targetPoint{ highlightRow.Left, highlightRow.Top }; + const SHORT stringLength = highlightRow.Right - highlightRow.Left + 1; + highlightRow = s_BisectSelection(stringLength, targetPoint, screenInfo, highlightRow); + + selectionAreas.emplace_back(highlightRow); + } + + return selectionAreas; +} + +// Routine Description: +// - Detemines the line-by-line selection rectangles based on global selection state. +// Arguments: +// - - Uses internal state to know what area is selected already. +// Return Value: +// - Returns a vector where each SMALL_RECT is one Row worth of the area to be selected. +// - Returns empty vector if no rows are selected. +// - Throws exceptions for out of memory issues +std::vector Selection::GetSelectionRects() const +{ + if (!_fSelectionVisible) + { + return std::vector(); + } + + return s_GetSelectionRects(_srSelectionRect, _coordSelectionAnchor, IsLineSelection()); +} + +// Routine Description: +// - This routine checks to ensure that clipboard selection isn't trying to cut a double byte character in half. +// It will adjust the SmallRect rectangle size to ensure this. +// Arguments: +// - sStringLength - The length of the string we're attempting to clip. +// - coordTargetPoint - The row/column position within the text buffer that we're about to try to clip. +// - screenInfo - Screen information structure containing relevant text and dimension information. +// - rect - The region of the text that we want to clip, and then adjusted to the region that should be +// clipped without splicing double-width characters. +// Return Value: +// - the clipped region +SMALL_RECT Selection::s_BisectSelection(const short sStringLength, + const COORD coordTargetPoint, + const SCREEN_INFORMATION& screenInfo, + const SMALL_RECT rect) +{ + SMALL_RECT outRect = rect; + try + { + auto iter = screenInfo.GetCellDataAt(coordTargetPoint); + if (iter->DbcsAttr().IsTrailing()) + { + if (coordTargetPoint.X == 0) + { + outRect.Left++; + } + else + { + outRect.Left--; + } + } + + // Check end position of strings + if (coordTargetPoint.X + sStringLength < screenInfo.GetBufferSize().Width()) + { + iter += sStringLength; + if (iter->DbcsAttr().IsTrailing()) + { + outRect.Right++; + } + } + else + { + if (coordTargetPoint.Y + 1 < screenInfo.GetBufferSize().Height()) + { + const auto nextLineIter = screenInfo.GetCellDataAt({ 0, coordTargetPoint.Y + 1 }); + if (nextLineIter->DbcsAttr().IsTrailing()) + { + outRect.Right--; + } + } + } + } + CATCH_LOG(); + + return outRect; +} + +// Routine Description: +// - Shows the selection area in the window if one is available and not already showing. +// Arguments: +// +// Return Value: +// +void Selection::ShowSelection() +{ + _SetSelectionVisibility(true); +} + +// Routine Description: +// - Hides the selection area in the window if one is available and already showing. +// Arguments: +// +// Return Value: +// +void Selection::HideSelection() +{ + _SetSelectionVisibility(false); +} + +// Routine Description: +// - Changes the visibility of the selection area on the screen. +// - Used to turn the selection area on or off. +// Arguments: +// - fMakeVisible - If TRUE, we're turning the selection ON. +// - If FALSE, we're turning the selection OFF. +// Return Value: +// +void Selection::_SetSelectionVisibility(const bool fMakeVisible) +{ + if (IsInSelectingState() && IsAreaSelected()) + { + if (fMakeVisible == _fSelectionVisible) + { + return; + } + + _fSelectionVisible = fMakeVisible; + + _PaintSelection(); + } + LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); +} + +// Routine Description: +// - Inverts the selected region on the current screen buffer. +// - Reads the selected area, selection mode, and active screen buffer +// from the global properties and dispatches a GDI invert on the selected text area. +// Arguments: +// - +// Return Value: +// - +void Selection::_PaintSelection() const +{ + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender->TriggerSelection(); + } +} + +// Routine Description: +// - Starts the selection with the given initial position +// Arguments: +// - coordBufferPos - Position in which user started a selection +// Return Value: +// - +void Selection::InitializeMouseSelection(const COORD coordBufferPos) +{ + Scrolling::s_ClearScroll(); + + // set flags + _SetSelectingState(true); + _dwSelectionFlags = CONSOLE_MOUSE_SELECTION | CONSOLE_SELECTION_NOT_EMPTY; + + // store anchor and rectangle of selection + _coordSelectionAnchor = coordBufferPos; + + // since we've started with just a point, the rectangle is 1x1 on the point given + _srSelectionRect.Left = coordBufferPos.X; + _srSelectionRect.Right = coordBufferPos.X; + _srSelectionRect.Top = coordBufferPos.Y; + _srSelectionRect.Bottom = coordBufferPos.Y; + + // Check for ALT-Mouse Down "use alternate selection" + // If in box mode, use line mode. If in line mode, use box mode. + CheckAndSetAlternateSelection(); + + // set window title to mouse selection mode + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + LOG_IF_FAILED(pWindow->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } + + // Fire off an event to let accessibility apps know the selection has changed. + ServiceLocator::LocateAccessibilityNotifier()->NotifyConsoleCaretEvent(IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection, PACKCOORD(coordBufferPos)); +} + +// Routine Description: +// - Modifies both ends of the current selection. +// - Intended for use with functions that help auto-complete a selection area (e.g. double clicking) +// Arguments: +// - coordSelectionStart - Replaces the selection anchor, a.k.a. where the selection started from originally. +// - coordSelectionEnd - The linear final position or opposite corner of the anchor to represent the complete selection area. +// Return Value: +// - +void Selection::AdjustSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd) +{ + // modify the anchor and then just use extend to adjust the other portion of the selection rectangle + _coordSelectionAnchor = coordSelectionStart; + ExtendSelection(coordSelectionEnd); + _allowMouseDragSelection = false; +} + +// Routine Description: +// - Extends the selection out to the given position from the initial anchor point. +// This means that a coordinate farther away will make the rectangle larger and a closer one will shrink it. +// Arguments: +// - coordBufferPos - Position to extend/contract the current selection up to. +// Return Value: +// - +void Selection::ExtendSelection(_In_ COORD coordBufferPos) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + _allowMouseDragSelection = true; + + // ensure position is within buffer bounds. Not less than 0 and not greater than the screen buffer size. + try + { + screenInfo.GetTerminalBufferSize().Clamp(coordBufferPos); + } + CATCH_LOG_RETURN(); + + if (!IsAreaSelected()) + { + // we should only be extending a selection that has no area yet if we're coming from mark mode. + // if not, just return. + if (IsMouseInitiatedSelection()) + { + return; + } + + // scroll if necessary to make cursor visible. + screenInfo.MakeCursorVisible(coordBufferPos, false); + + _dwSelectionFlags |= CONSOLE_SELECTION_NOT_EMPTY; + _srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X; + _srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y; + + ShowSelection(); + } + else + { + // scroll if necessary to make cursor visible. + screenInfo.MakeCursorVisible(coordBufferPos, false); + } + + // remember previous selection rect + SMALL_RECT srNewSelection = _srSelectionRect; + + // update selection rect + // this adjusts the rectangle dimensions based on which way the move was requested + // in respect to the original selection position (the anchor) + if (coordBufferPos.X <= _coordSelectionAnchor.X) + { + srNewSelection.Left = coordBufferPos.X; + srNewSelection.Right = _coordSelectionAnchor.X; + } + else if (coordBufferPos.X > _coordSelectionAnchor.X) + { + srNewSelection.Right = coordBufferPos.X; + srNewSelection.Left = _coordSelectionAnchor.X; + } + if (coordBufferPos.Y <= _coordSelectionAnchor.Y) + { + srNewSelection.Top = coordBufferPos.Y; + srNewSelection.Bottom = _coordSelectionAnchor.Y; + } + else if (coordBufferPos.Y > _coordSelectionAnchor.Y) + { + srNewSelection.Bottom = coordBufferPos.Y; + srNewSelection.Top = _coordSelectionAnchor.Y; + } + + // call special update method to modify the displayed selection in-place + // NOTE: Using HideSelection, editing the rectangle, then ShowSelection will cause flicker. + //_PaintUpdateSelection(&srNewSelection); + _srSelectionRect = srNewSelection; + _PaintSelection(); + + // Fire off an event to let accessibility apps know the selection has changed. + ServiceLocator::LocateAccessibilityNotifier()->NotifyConsoleCaretEvent(IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection, PACKCOORD(coordBufferPos)); + LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); +} + +// Routine Description: +// - Cancels any mouse selection state to return to normal mode. +// Arguments: +// - (Uses global state) +// Return Value: +// - +void Selection::_CancelMouseSelection() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + + // invert old select rect. if we're selecting by mouse, we + // always have a selection rect. + HideSelection(); + + // turn off selection flag + _SetSelectingState(false); + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + } + + // Mark the cursor position as changed so we'll fire off a win event. + ScreenInfo.GetTextBuffer().GetCursor().SetHasMoved(true); +} + +// Routine Description: +// - Cancels any mark mode selection state to return to normal mode. +// Arguments: +// - +// Return Value: +// - +void Selection::_CancelMarkSelection() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + + // Hide existing selection, if we have one. + if (IsAreaSelected()) + { + HideSelection(); + } + + // Turn off selection flag. + _SetSelectingState(false); + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + } + + // restore text cursor + _RestoreDataToCursor(ScreenInfo.GetTextBuffer().GetCursor()); +} + +// Routine Description: +// - If a selection exists, clears it and restores the state. +// Will also unblock a blocked write if one exists. +// Arguments: +// - (Uses global state) +// Return Value: +// - +void Selection::ClearSelection() +{ + ClearSelection(false); +} + +// Routine Description: +// - If a selection exists, clears it and restores the state. +// - Will only unblock a write if not starting a new selection. +// Arguments: +// - fStartingNewSelection - If we're going to start another selection right away, we'll keep the write blocked. +// Return Value: +// - +void Selection::ClearSelection(const bool fStartingNewSelection) +{ + if (IsInSelectingState()) + { + if (IsMouseInitiatedSelection()) + { + _CancelMouseSelection(); + } + else + { + _CancelMarkSelection(); + } + LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); + + _dwSelectionFlags = 0; + + // If we were using alternate selection, cancel it here before starting a new area. + _fUseAlternateSelection = false; + + // Only unblock if we're not immediately starting a new selection. Otherwise stay blocked. + if (!fStartingNewSelection) + { + UnblockWriteConsole(CONSOLE_SELECTING); + } + } +} + +// Routine Description: +// - Colors all text in the given rectangle with the color attribute provided. +// - This does not validate whether there is a valid selection right now or not. +// It is assumed to already be in a proper selecting state and the given rectangle should be highlighted with the given color unconditionally. +// Arguments: +// - psrRect - Rectangular area to fill with color +// - attr - The color attributes to apply +void Selection::ColorSelection(const SMALL_RECT& srRect, const TextAttribute attr) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // Read selection rectangle, assumed already clipped to buffer. + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + COORD coordTargetSize; + coordTargetSize.X = CalcWindowSizeX(srRect); + coordTargetSize.Y = CalcWindowSizeY(srRect); + + COORD coordTarget; + coordTarget.X = srRect.Left; + coordTarget.Y = srRect.Top; + + // Now color the selection a line at a time. + for (; (coordTarget.Y < srRect.Top + coordTargetSize.Y); ++coordTarget.Y) + { + const size_t cchWrite = gsl::narrow(coordTargetSize.X); + + try + { + screenInfo.Write(OutputCellIterator(attr, cchWrite), coordTarget); + } + CATCH_LOG(); + } +} + +// Routine Description: +// - Given two points in the buffer space, color the selection between the two with the given attribute. +// - This will create an internal selection rectangle covering the two points, assume a line selection, +// and use the first point as the anchor for the selection (as if the mouse click started at that point) +// Arguments: +// - coordSelectionStart - Anchor point (start of selection) for the region to be colored +// - coordSelectionEnd - Other point referencing the rectangle inscribing the selection area +// - attr - Color to apply to region. +void Selection::ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr) +{ + // Make a rectangle for the region as if it were selected by a mouse. + // We will use the first one as the "anchor" to represent where the mouse went down. + SMALL_RECT srSelection; + srSelection.Top = std::min(coordSelectionStart.Y, coordSelectionEnd.Y); + srSelection.Bottom = std::max(coordSelectionStart.Y, coordSelectionEnd.Y); + srSelection.Left = std::min(coordSelectionStart.X, coordSelectionEnd.X); + srSelection.Right = std::max(coordSelectionStart.X, coordSelectionEnd.X); + + // Extract row-by-row selection rectangles for the selection area. + try + { + const auto rectangles = s_GetSelectionRects(srSelection, coordSelectionStart, true); + for (const auto& rect : rectangles) + { + ColorSelection(rect, attr); + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Enters mark mode selection. Prepares the cursor to move around to select a region and sets up state variables. +// Arguments: +// - +// Return Value: +// - +void Selection::InitializeMarkSelection() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // clear any existing selection. + ClearSelection(true); + + Scrolling::s_ClearScroll(); + + // set flags + _SetSelectingState(true); + _dwSelectionFlags = 0; + + // save old cursor position and make console cursor into selection cursor. + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + const auto& cursor = screenInfo.GetTextBuffer().GetCursor(); + _SaveCursorData(cursor); + screenInfo.SetCursorInformation(100, TRUE); + + const COORD coordPosition = cursor.GetPosition(); + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordPosition, true)); + + // set the cursor position as the anchor position + // it will get updated as the cursor moves for mark mode, + // but it serves to prepare us for the inevitable start of the selection with Shift+Arrow Key + _coordSelectionAnchor = coordPosition; + + // set frame title text + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->UpdateWindowText(); + LOG_IF_FAILED(pWindow->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } +} + +// Routine Description: +// - Resets the current selection and selects a new region from the start to end coordinates +// Arguments: +// - coordStart - Position to start selection area from +// - coordEnd - Position to select up to +// Return Value: +// - +void Selection::SelectNewRegion(const COORD coordStart, const COORD coordEnd) +{ + // clear existing selection if applicable + ClearSelection(); + + // initialize selection + InitializeMouseSelection(coordStart); + + ShowSelection(); + + // extend selection + ExtendSelection(coordEnd); +} + +// Routine Description: +// - Creates a new selection region of "all" available text. +// The meaning of "all" can vary. If we have input text, then "all" is just the input text. +// If we have no input text, "all" is the entire valid screen buffer (output text and the prompt) +// Arguments: +// - (Uses global state) +// Return Value: +// - +void Selection::SelectAll() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // save the old window position + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + COORD coordWindowOrigin = screenInfo.GetViewport().Origin(); + + // Get existing selection rectangle parameters + const bool fOldSelectionExisted = IsAreaSelected(); + const SMALL_RECT srOldSelection = _srSelectionRect; + const COORD coordOldAnchor = _coordSelectionAnchor; + + // Attempt to get the boundaries of the current input line. + COORD coordInputStart; + COORD coordInputEnd; + const bool fHasInputArea = s_GetInputLineBoundaries(&coordInputStart, &coordInputEnd); + + // These variables will be used to specify the new selection area when we're done + COORD coordNewSelStart; + COORD coordNewSelEnd; + + // Now evaluate conditions and attempt to assign a new selection area. + if (!fHasInputArea) + { + // If there's no input area, just select the entire valid text region. + GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); + } + else + { + if (!fOldSelectionExisted) + { + // Temporary workaround until MSFT: 614579 is completed. + const auto bufferSize = screenInfo.GetBufferSize(); + COORD coordOneAfterEnd = coordInputEnd; + bufferSize.IncrementInBounds(coordOneAfterEnd); + + if (s_IsWithinBoundaries(screenInfo.GetTextBuffer().GetCursor().GetPosition(), coordInputStart, coordInputEnd)) + { + // If there was no previous selection and the cursor is within the input line, select the input line only + coordNewSelStart = coordInputStart; + coordNewSelEnd = coordInputEnd; + } + else if (s_IsWithinBoundaries(screenInfo.GetTextBuffer().GetCursor().GetPosition(), coordOneAfterEnd, coordOneAfterEnd)) + { + // Temporary workaround until MSFT: 614579 is completed. + // Select only the input line if the cursor is one after the final position of the input line. + coordNewSelStart = coordInputStart; + coordNewSelEnd = coordInputEnd; + } + else + { + // otherwise if the cursor is elsewhere, select everything + GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); + } + } + else + { + // This is the complex case. We had an existing selection and we have an input area. + + // To figure this out, we need the anchor (the point where the selection starts) and its opposite corner + COORD coordOldAnchorOpposite = Utils::s_GetOppositeCorner(srOldSelection, coordOldAnchor); + + // Check if both anchor and opposite corner fall within the input line + const bool fIsOldSelWithinInput = + s_IsWithinBoundaries(coordOldAnchor, coordInputStart, coordInputEnd) && + s_IsWithinBoundaries(coordOldAnchorOpposite, coordInputStart, coordInputEnd); + + // Check if both anchor and opposite corner are exactly the bounds of the input line + const bool fAllInputSelected = + ((Utils::s_CompareCoords(coordInputStart, coordOldAnchor) == 0 && Utils::s_CompareCoords(coordInputEnd, coordOldAnchorOpposite) == 0) || + (Utils::s_CompareCoords(coordInputStart, coordOldAnchorOpposite) == 0 && Utils::s_CompareCoords(coordInputEnd, coordOldAnchor) == 0)); + + if (fIsOldSelWithinInput && !fAllInputSelected) + { + // If it's within the input area and the whole input is not selected, then select just the input + coordNewSelStart = coordInputStart; + coordNewSelEnd = coordInputEnd; + } + else + { + // Otherwise just select the whole valid area + GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); + } + } + } + + // If we're in box selection, adjust end coordinate to end of line and start coordinate to start of line + // or it won't be selecting all the text. + if (!IsLineSelection()) + { + coordNewSelStart.X = 0; + coordNewSelEnd.X = screenInfo.GetBufferSize().RightInclusive(); + } + + SelectNewRegion(coordNewSelStart, coordNewSelEnd); + + // restore the old window position + LOG_IF_FAILED(screenInfo.SetViewportOrigin(true, coordWindowOrigin, true)); +} diff --git a/src/host/selection.hpp b/src/host/selection.hpp new file mode 100644 index 000000000..434734b94 --- /dev/null +++ b/src/host/selection.hpp @@ -0,0 +1,201 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- selection.hpp + +Abstract: +- This module is used for managing the selection region + +Author(s): +- Michael Niksa (MiNiksa) 4-Jun-2014 +- Paul Campbell (PaulCam) 4-Jun-2014 + +Revision History: +- From components of clipbrd.h/.c and input.h/.c of v1 console. +--*/ + +#pragma once + +#include "input.h" + +#include "..\interactivity\inc\IAccessibilityNotifier.hpp" +#include "..\interactivity\inc\IConsoleWindow.hpp" + +using namespace Microsoft::Console::Interactivity; + +class Selection +{ +public: + ~Selection() = default; + + std::vector GetSelectionRects() const; + + void ShowSelection(); + void HideSelection(); + + static Selection& Instance(); + + // Key selection generally refers to "mark mode" selection where + // the cursor is present and used to navigate 100% with the + // keyboard. + // + // Mouse selection means either the block or line mode selection + // usually initiated by the mouse. + // + // However, Mouse mode can also mean initiated with our + // shift+directional commands as no block cursor is required for + // navigation. + + void InitializeMarkSelection(); + void InitializeMouseSelection(const COORD coordBufferPos); + + void SelectNewRegion(const COORD coordStart, const COORD coordEnd); + void SelectAll(); + + void ExtendSelection(_In_ COORD coordBufferPos); + void AdjustSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd); + + void ClearSelection(); + void ClearSelection(const bool fStartingNewSelection); + void ColorSelection(const SMALL_RECT& srRect, const TextAttribute attr); + void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr); + + // delete these or we can accidentally get copies of the singleton + Selection(Selection const&) = delete; + void operator=(Selection const&) = delete; + +protected: + Selection(); + +private: + void _SetSelectionVisibility(const bool fMakeVisible); + + void _PaintSelection() const; + + static SMALL_RECT s_BisectSelection(const short sStringLength, + const COORD coordTargetPoint, + const SCREEN_INFORMATION& screenInfo, + const SMALL_RECT rect); + + static std::vector s_GetSelectionRects(const SMALL_RECT& selectionRect, + const COORD selectionAnchor, + const bool lineSelection); + + void _CancelMarkSelection(); + void _CancelMouseSelection(); + + static std::unique_ptr _instance; + +// ------------------------------------------------------------------------------------------------------- +// input handling (selectionInput.cpp) +public: + + // key handling + + // N.B.: This enumeration helps push up calling clipboard functions into + // the caller. This way, all of the selection code is independent of + // the clipboard and thus more easily shareable with Windows editions + // that do not have a clipboard (i.e. OneCore). + enum class KeySelectionEventResult + { + EventHandled, + EventNotHandled, + CopyToClipboard + }; + + KeySelectionEventResult HandleKeySelectionEvent(const INPUT_KEY_INFO* const pInputKeyInfo); + static bool s_IsValidKeyboardLineSelection(const INPUT_KEY_INFO* const pInputKeyInfo); + bool HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pInputKeyInfo); + + void CheckAndSetAlternateSelection(); + + // calculation functions + [[nodiscard]] + static bool s_GetInputLineBoundaries(_Out_opt_ COORD* const pcoordInputStart, _Out_opt_ COORD* const pcoordInputEnd); + void GetValidAreaBoundaries(_Out_opt_ COORD* const pcoordValidStart, _Out_opt_ COORD* const pcoordValidEnd) const; + static bool s_IsWithinBoundaries(const COORD coordPosition, const COORD coordStart, const COORD coordEnd); + +private: + // key handling + bool _HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo); + bool _HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKeyInfo); + COORD WordByWordSelection(const bool fPrevious, + const Microsoft::Console::Types::Viewport& bufferSize, + const COORD coordAnchor, + const COORD coordSelPoint) const; + + +// ------------------------------------------------------------------------------------------------------- +// selection state (selectionState.cpp) +public: + bool IsKeyboardMarkSelection() const; + bool IsMouseInitiatedSelection() const; + + bool IsLineSelection() const; + + bool IsInSelectingState() const; + bool IsInQuickEditMode() const; + + bool IsAreaSelected() const; + bool IsMouseButtonDown() const; + + DWORD GetPublicSelectionFlags() const noexcept; + COORD GetSelectionAnchor() const noexcept; + SMALL_RECT GetSelectionRectangle() const noexcept; + + void SetLineSelection(const bool fLineSelectionOn); + + bool ShouldAllowMouseDragSelection(const COORD mousePosition) const noexcept; + + // TODO: these states likely belong somewhere else + void MouseDown(); + void MouseUp(); + +private: + void _SaveCursorData(const Cursor& cursor) noexcept; + void _RestoreDataToCursor(Cursor& cursor) noexcept; + + void _AlignAlternateSelection(const bool fAlignToLineSelect); + + void _SetSelectingState(const bool fSelectingOn); + + // TODO: extended edit key should probably be in here (remaining code is in cmdline.cpp) + // TODO: trim leading zeros should probably be in here (pending move of reactive code from input.cpp to selectionInput.cpp) + // TODO: enable color selection should be in here + // TODO: quick edit mode should be in here + // TODO: console selection mode should be in here + // TODO: consider putting word delims in here + + // -- State/Flags -- + // This replaces/deprecates CONSOLE_SELECTION_INVERTED on gci->SelectionFlags + bool _fSelectionVisible; + + bool _fLineSelection; // whether to use line selection or block selection + bool _fUseAlternateSelection; // whether the user has triggered the alternate selection method + bool _allowMouseDragSelection; // true if the dragging the mouse should change the selection + + // Flags for this DWORD are defined in wincon.h. Search for def:CONSOLE_SELECTION_IN_PROGRESS, etc. + DWORD _dwSelectionFlags; + + // -- Current Selection Data -- + // Anchor is the point the selection was started from (and will be one of the corners of the rectangle). + COORD _coordSelectionAnchor; + // Rectangle is the area inscribing the selection. It is extended to screen edges in a particular way for line selection. + SMALL_RECT _srSelectionRect; + + // -- Saved Cursor Data -- + // Saved when a selection is started for restoration later. Position is in character coordinates, not pixels. + COORD _coordSavedCursorPosition; + ULONG _ulSavedCursorSize; + bool _fSavedCursorVisible; + COLORREF _savedCursorColor; + CursorType _savedCursorType; + +#ifdef UNIT_TESTING + friend class SelectionTests; + friend class SelectionInputTests; + friend class ClipboardTests; +#endif +}; diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp new file mode 100644 index 000000000..6124ea5a4 --- /dev/null +++ b/src/host/selectionInput.cpp @@ -0,0 +1,1056 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "search.h" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../types/inc/convert.hpp" + +#include + +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Handles a keyboard event for extending the current selection +// - Must be called when the console is in selecting state. +// Arguments: +// - pInputKeyInfo : The key press state information from the keyboard +// Return Value: +// - True if the event is handled. False otherwise. +Selection::KeySelectionEventResult Selection::HandleKeySelectionEvent(const INPUT_KEY_INFO* const pInputKeyInfo) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto inputServices = ServiceLocator::LocateInputServices(); + FAIL_FAST_IF(!IsInSelectingState()); + + const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey(); + const bool ctrlPressed = WI_IsFlagSet(inputServices->GetKeyState(VK_CONTROL), KEY_PRESSED); + + // if escape or ctrl-c, cancel selection + if (!IsMouseButtonDown()) + { + if (wVirtualKeyCode == VK_ESCAPE) + { + ClearSelection(); + return Selection::KeySelectionEventResult::EventHandled; + } + else if (wVirtualKeyCode == VK_RETURN || + // C-c, C-Ins. C-S-c Is also handled by this case. + ((ctrlPressed) && (wVirtualKeyCode == 'C' || wVirtualKeyCode == VK_INSERT))) + { + Telemetry::Instance().SetKeyboardTextEditingUsed(); + + // copy selection + return Selection::KeySelectionEventResult::CopyToClipboard; + } + else if (gci.GetEnableColorSelection() && + ('0' <= wVirtualKeyCode) && + ('9' >= wVirtualKeyCode)) + { + if (_HandleColorSelection(pInputKeyInfo)) + { + return Selection::KeySelectionEventResult::EventHandled; + } + } + } + + if (!IsMouseInitiatedSelection()) + { + if (_HandleMarkModeSelectionNav(pInputKeyInfo)) + { + return Selection::KeySelectionEventResult::EventHandled; + } + } + else if (!IsMouseButtonDown()) + { + // if the existing selection is a line selection + if (IsLineSelection()) + { + // try to handle it first if we've used a valid keyboard command to extend the selection + if (HandleKeyboardLineSelectionEvent(pInputKeyInfo)) + { + return Selection::KeySelectionEventResult::EventHandled; + } + } + + // if in mouse selection mode and user hits a key, cancel selection + if (!IsSystemKey(wVirtualKeyCode)) { + ClearSelection(); + } + } + + return Selection::KeySelectionEventResult::EventNotHandled; +} + +// Routine Description: +// - Checks if a keyboard event can be handled by HandleKeyboardLineSelectionEvent +// Arguments: +// - pInputKeyInfo : The key press state information from the keyboard +// Return Value: +// - True if the event can be handled. False otherwise. +// NOTE: +// - Keyboard handling cases in this function should be synchronized with HandleKeyboardLineSelectionEvent +bool Selection::s_IsValidKeyboardLineSelection(const INPUT_KEY_INFO* const pInputKeyInfo) +{ + bool fIsValidCombination = false; + + const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey(); + + if (pInputKeyInfo->IsShiftOnly()) + { + switch (wVirtualKeyCode) + { + case VK_LEFT: + case VK_RIGHT: + case VK_UP: + case VK_DOWN: + case VK_NEXT: + case VK_PRIOR: + case VK_HOME: + case VK_END: + fIsValidCombination = true; + } + } + else if (pInputKeyInfo->IsShiftAndCtrlOnly()) + { + switch (wVirtualKeyCode) + { + case VK_LEFT: + case VK_RIGHT: + case VK_UP: + case VK_DOWN: + case VK_HOME: + case VK_END: + fIsValidCombination = true; + } + } + + return fIsValidCombination; +} + +// Routine Description: +// - Modifies the given selection point to the edge of the next (or previous) word. +// - By default operates in a left-to-right fashion. +// Arguments: +// - fReverse: Specifies that this function should operate in reverse. E.g. Right-to-left. +// - bufferSize: The dimensions of the screen buffer. +// - coordAnchor: The point within the buffer (inside the edges) where this selection started. +// - coordSelPoint: Defines selection region from coordAnchor to this point. Modified to define the new selection region. +// Return Value: +// - +COORD Selection::WordByWordSelection(const bool fReverse, + const Viewport& bufferSize, + const COORD coordAnchor, + const COORD coordSelPoint) const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + COORD outCoord = coordSelPoint; + + // first move one character in the requested direction + if (!fReverse) + { + bufferSize.IncrementInBounds(outCoord); + } + else + { + bufferSize.DecrementInBounds(outCoord); + } + + // get the character at the new position + auto charData = *screenInfo.GetTextDataAt(outCoord); + + // we want to go until the state change from delim to non-delim + bool fCurrIsDelim = IsWordDelim(charData); + bool fPrevIsDelim; + + // find the edit-line boundaries that we can highlight + COORD coordMaxLeft; + COORD coordMaxRight; + const bool fSuccess = s_GetInputLineBoundaries(&coordMaxLeft, &coordMaxRight); + + // if line boundaries fail, then set them to the buffer corners so they don't restrict anything. + if (!fSuccess) + { + coordMaxLeft.X = bufferSize.Left(); + coordMaxLeft.Y = bufferSize.Top(); + + coordMaxRight.X = bufferSize.RightInclusive(); + coordMaxRight.Y = bufferSize.BottomInclusive(); + } + + // track whether we failed to move during an operation + // if we failed to move, we hit the end of the buffer and should just highlight to there and be done. + bool fMoveSucceeded = false; + + // determine if we're highlighting more text or unhighlighting already selected text. + bool fUnhighlighting; + if (!fReverse) + { + // if the selection point is left of the anchor, then we're unhighlighting when moving right + fUnhighlighting = Utils::s_CompareCoords(outCoord, coordAnchor) < 0; + } + else + { + // if the selection point is right of the anchor, then we're unhighlighting when moving left + fUnhighlighting = Utils::s_CompareCoords(outCoord, coordAnchor) > 0; + } + + do + { + // store previous state + fPrevIsDelim = fCurrIsDelim; + + // to make us "sticky" within the edit line, stop moving once we've reached a given max position left/right + // users can repeat the command to move past the line and continue word selecting + // if we're at the max position left, stop moving + if (Utils::s_CompareCoords(outCoord, coordMaxLeft) == 0) + { + // set move succeeded to false as we can't move any further + fMoveSucceeded = false; + break; + } + + // if we're at the max position right, stop moving. + // we don't want them to "word select" past the end of the edit line as there's likely nothing there. + // (thus >= and not == like left) + if (Utils::s_CompareCoords(outCoord, coordMaxRight) >= 0) + { + // set move succeeded to false as we can't move any further. + fMoveSucceeded = false; + break; + } + + if (!fReverse) + { + fMoveSucceeded = bufferSize.IncrementInBounds(outCoord); + } + else + { + fMoveSucceeded = bufferSize.DecrementInBounds(outCoord); + } + + if (!fMoveSucceeded) + { + break; + } + + // get the character associated with the new position + charData = *screenInfo.GetTextDataAt(outCoord); + fCurrIsDelim = IsWordDelim(charData); + + // This is a bit confusing. + // If we're going Left to Right (!fReverse)... + // - Then we want to keep going UNTIL (!) we move from a delimiter (fPrevIsDelim) to a normal character (!fCurrIsDelim) + // This will then eat up all delimiters after a word and stop once we reach the first letter of the next word. + // If we're going Right to Left (fReverse)... + // - Then we want to keep going UNTIL (!) we move from a normal character (!fPrevIsDelim) to a delimeter (fCurrIsDelim) + // This will eat up all letters of the word and stop once we see the delimiter before the word. + } while (!fReverse ? !(fPrevIsDelim && !fCurrIsDelim) : !(!fPrevIsDelim && fCurrIsDelim)); + + // To stop the loop, we had to move the cursor one too far to figure out that the delta occurred from delimeter to not (or vice versa) + // Therefore move back by one character after proceeding through the loop. + // EXCEPT: + // 1. If we broke out of the loop by reaching the beginning of the buffer, leave it alone. + // 2. If we're un-highlighting a region, also leave it alone. + // This is an oddity that occurs because our cursor is on a character, not between two characters like most text editors. + // We want the current position to be ON the first letter of the word (or the last delimeter after the word) so it stays highlighted. + if (fMoveSucceeded && !fUnhighlighting) + { + if (!fReverse) + { + bufferSize.DecrementInBounds(outCoord); + } + else + { + bufferSize.IncrementInBounds(outCoord); + } + + FAIL_FAST_IF(!fMoveSucceeded); // we should never fail to move forward after having moved backward + } + return outCoord; +} + +// Routine Description: +// - Handles a keyboard event for manipulating line-mode selection with the keyboard +// - If called when console isn't in selecting state, will start a new selection. +// Arguments: +// - inputKeyInfo : The key press state information from the keyboard +// Return Value: +// - True if the event is handled. False otherwise. +// NOTE: +// - Keyboard handling cases in this function should be synchronized with IsValidKeyboardLineSelection +bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pInputKeyInfo) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey(); + + // if this isn't a valid key combination for this function, exit quickly. + if (!s_IsValidKeyboardLineSelection(pInputKeyInfo)) + { + return false; + } + + Telemetry::Instance().SetKeyboardTextSelectionUsed(); + + // if we're not currently selecting anything, start a new mouse selection + if (!IsInSelectingState()) + { + InitializeMouseSelection(gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition()); + + // force that this is a line selection + _AlignAlternateSelection(true); + + ShowSelection(); + + // if we did shift+left/right, then just exit + if (pInputKeyInfo->IsShiftOnly()) + { + switch (wVirtualKeyCode) + { + case VK_LEFT: + case VK_RIGHT: + return true; + } + } + } + + // anchor is the first clicked position + const COORD coordAnchor = _coordSelectionAnchor; + + // rect covers the entire selection + const SMALL_RECT rectSelection = _srSelectionRect; + + // the selection point is the other corner of the rectangle from the anchor that we're about to manipulate + COORD coordSelPoint; + coordSelPoint.X = coordAnchor.X == rectSelection.Left ? rectSelection.Right : rectSelection.Left; + coordSelPoint.Y = coordAnchor.Y == rectSelection.Top ? rectSelection.Bottom : rectSelection.Top; + + // this is the maximum size of the buffer + const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize(); + + const SHORT sWindowHeight = gci.GetActiveOutputBuffer().GetViewport().Height(); + + FAIL_FAST_IF(!bufferSize.IsInBounds(coordSelPoint)); + + // retrieve input line information. If we are selecting from within the input line, we need + // to bound ourselves within the input data first and not move into the back buffer. + + COORD coordInputLineStart; + COORD coordInputLineEnd; + bool fHaveInputLine = s_GetInputLineBoundaries(&coordInputLineStart, &coordInputLineEnd); + + if (pInputKeyInfo->IsShiftOnly()) + { + switch (wVirtualKeyCode) + { + // shift + left/right extends the selection by one character, wrapping at screen edge + case VK_LEFT: + { + bufferSize.DecrementInBounds(coordSelPoint); + break; + } + case VK_RIGHT: + { + bufferSize.IncrementInBounds(coordSelPoint); + + // if we're about to split a character in half, keep moving right + try + { + const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr(); + if (attr.IsTrailing()) + { + bufferSize.IncrementInBounds(coordSelPoint); + } + } + CATCH_LOG(); + + break; + } + // shift + up/down extends the selection by one row, stopping at top or bottom of screen + case VK_UP: + { + if (coordSelPoint.Y > bufferSize.Top()) + { + coordSelPoint.Y--; + } + break; + } + case VK_DOWN: + { + if (coordSelPoint.Y < bufferSize.BottomInclusive()) + { + coordSelPoint.Y++; + } + break; + } + // shift + pgup/pgdn extends selection up or down one full screen + case VK_NEXT: + { + coordSelPoint.Y += sWindowHeight; // TODO: potential overflow + if (coordSelPoint.Y > bufferSize.BottomInclusive()) + { + coordSelPoint.Y = bufferSize.BottomInclusive(); + } + break; + } + case VK_PRIOR: + { + coordSelPoint.Y -= sWindowHeight; // TODO: potential underflow + if (coordSelPoint.Y < bufferSize.Top()) + { + coordSelPoint.Y = bufferSize.Top(); + } + break; + } + // shift + home/end extends selection to beginning or end of line + case VK_HOME: + { + /* + Prompt sample: + qwertyuiopasdfg + C:\>dir /p /w C + :\windows\syste + m32 + + The input area runs from the d in "dir" to the space after the 2 in "32" + + We want to stop the HOME command from running to the beginning of the line only + if we're on the first input line because then it would capture the prompt. + + So if the selection point we're manipulating is currently anywhere in the + "dir /p /w C" area, then pressing home should only move it on top of the "d" in "dir". + + But if it's already at the "d" in dir, pressing HOME again should move us to the + beginning of the line anyway to collect up the prompt as well. + */ + + // if we're in the input line + if (fHaveInputLine) + { + // and the selection point is inside the input line area + if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) > 0) + { + // and we're on the same line as the beginning of the input + if (coordInputLineStart.Y == coordSelPoint.Y) + { + // then only back up to the start of the input + coordSelPoint.X = coordInputLineStart.X; + break; + } + } + } + + // otherwise, fall through and select to the head of the line. + coordSelPoint.X = 0; + break; + } + case VK_END: + { + /* + Prompt sample: + qwertyuiopasdfg + C:\>dir /p /w C + :\windows\syste + m32 + + The input area runs from the d in "dir" to the space after the 2 in "32" + + We want to stop the END command from running to the space after the "32" because + that's just where the cursor lies to let more text get entered and not actually + a valid selection area. + + So if the selection point is anywhere on the "m32", pressing end should move it + to on top of the "2". + + Additionally, if we're starting within the output buffer (qwerty, etc. and C:\>), then + pressing END should stop us before we enter the input line the first time. + + So if we're anywhere on "C:\", we should select up to the ">" character and no further + until a subsequent press of END. + + At the subsequent press of END when we're on the ">", we should move to the end of the input + line or the end of the screen, whichever comes first. + */ + + // if we're in the input line + if (fHaveInputLine) + { + // and the selection point is inside the input area + if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) >= 0) + { + // and we're on the same line as the end of the input + if (coordInputLineEnd.Y == coordSelPoint.Y) + { + // and we're not already on the end of the input... + if (coordSelPoint.X < coordInputLineEnd.X) + { + // then only use end to the end of the input + coordSelPoint.X = coordInputLineEnd.X; + break; + } + } + } + else + { + // otherwise if we're outside and on the same line as the start of the input + if (coordInputLineStart.Y == coordSelPoint.Y) + { + // calculate the end of the outside/output buffer position + const short sEndOfOutputPos = coordInputLineStart.X - 1; + + // if we're not already on the very last character... + if (coordSelPoint.X < sEndOfOutputPos) + { + // then only move to just before the beginning of the input + coordSelPoint.X = sEndOfOutputPos; + break; + } + else if (coordSelPoint.X == sEndOfOutputPos) + { + // if we were on the last character, + // then if the end of the input line is also on this current line, + // move to that. + if (coordSelPoint.Y == coordInputLineEnd.Y) + { + coordSelPoint.X = coordInputLineEnd.X; + break; + } + } + } + } + } + + // otherwise, fall through and go to selecting the whole line to the end. + coordSelPoint.X = bufferSize.RightInclusive(); + break; + } + } + } + else if (pInputKeyInfo->IsShiftAndCtrlOnly()) + { + switch (wVirtualKeyCode) + { + // shift + ctrl + left/right extends selection to next/prev word boundary + case VK_LEFT: + { + coordSelPoint = WordByWordSelection(true, bufferSize, coordAnchor, coordSelPoint); + break; + } + case VK_RIGHT: + { + coordSelPoint = WordByWordSelection(false, bufferSize, coordAnchor, coordSelPoint); + break; + } + // shift + ctrl + up/down does the same thing that shift + up/down does + case VK_UP: + { + if (coordSelPoint.Y > bufferSize.Top()) + { + coordSelPoint.Y--; + } + break; + } + case VK_DOWN: + { + if (coordSelPoint.Y < bufferSize.BottomInclusive()) + { + coordSelPoint.Y++; + } + break; + } + // shift + ctrl + home/end extends selection to top or bottom of buffer from selection + case VK_HOME: + { + COORD coordValidStart; + GetValidAreaBoundaries(&coordValidStart, nullptr); + coordSelPoint = coordValidStart; + break; + } + case VK_END: + { + COORD coordValidEnd; + GetValidAreaBoundaries(nullptr, &coordValidEnd); + coordSelPoint = coordValidEnd; + break; + } + } + } + + // ensure we're not planting the cursor in the middle of a double-wide character. + try + { + const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr(); + if (attr.IsTrailing()) + { + // try to move off by highlighting the lead half too. + bool fSuccess = bufferSize.DecrementInBounds(coordSelPoint); + + // if that fails, move off to the next character + if (!fSuccess) + { + bufferSize.IncrementInBounds(coordSelPoint); + } + } + } + CATCH_LOG(); + + ExtendSelection(coordSelPoint); + + return true; +} + +// Routine Description: +// - Checks whether the ALT key was pressed when this method was called. +// - ALT is the modifier for the alternate selection mode, so this will set state accordingly. +// Arguments: +// - (Uses global key state) +// Return Value: +// - +void Selection::CheckAndSetAlternateSelection() +{ + _fUseAlternateSelection = !!(ServiceLocator::LocateInputServices()->GetKeyState(VK_MENU) & KEY_PRESSED); +} + +// Routine Description: +// - Handles a keyboard event for manipulating color selection +// - If called when console isn't in selecting state, will start a new selection. +// Arguments: +// - pInputKeyInfo : The key press state information from the keyboard +// Return Value: +// - True if the event is handled. False otherwise. +bool Selection::_HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey(); + + // It's a numeric key, a text mode buffer and the color selection regkey is set, + // then check to see if the user want's to color the selection or search and + // highlight the selection. + bool fAltPressed = pInputKeyInfo->IsAltPressed(); + bool fShiftPressed = pInputKeyInfo->IsShiftPressed(); + bool fCtrlPressed = false; + + // Shift implies a find-and-color operation. + // We only support finding a string, not a block. + // If it is line selection, we can assemble that across multiple lines to make a search term. + // But if it is block selection and the selected area is > 1 line in height, ignore the shift because we can't search. + // Also ignore if there is no current selection. + if ((fShiftPressed) && (!IsAreaSelected() || (!IsLineSelection() && (_srSelectionRect.Top != _srSelectionRect.Bottom )))) + { + fShiftPressed = false; + } + + // If CTRL + ALT together, then we interpret as ALT (eg on French + // keyboards AltGr == RALT+LCTRL, but we want it to behave as ALT). + if (!fAltPressed) + { + fCtrlPressed = pInputKeyInfo->IsCtrlPressed(); + } + + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + // Clip the selection to within the console buffer + screenInfo.ClipToScreenBuffer(&_srSelectionRect); + + // If ALT or CTRL are pressed, then color the selected area. + // ALT+n => fg, CTRL+n => bg + if (fAltPressed || fCtrlPressed) + { + ULONG ulAttr = wVirtualKeyCode - '0' + 6; + + if (fCtrlPressed) + { + // Setting background color. Set fg color to black. + ulAttr <<= 4; + } + else + { + // Set foreground color. Maintain the current console bg color. + ulAttr |= gci.GetActiveOutputBuffer().GetAttributes().GetLegacyAttributes() & 0xf0; + } + + // If shift was pressed as well, then this is actually a + // find-and-color request. Otherwise just color the selection. + if (fShiftPressed) + { + try + { + const auto selectionRects = GetSelectionRects(); + if (selectionRects.size() > 0) + { + // Pull the selection out of the buffer to pass to the + // search function. Clamp to max search string length. + // We just copy the bytes out of the row buffer. + + std::wstring str; + for (const auto& selectRect : selectionRects) + { + auto it = screenInfo.GetTextDataAt(COORD{ selectRect.Left, selectRect.Top }); + + for (SHORT i = 0; i < (selectRect.Right - selectRect.Left + 1); ++i) + { + str.append((*it).begin(), (*it).end()); + it++; + } + } + + // Clear the selection and call the search / mark function. + ClearSelection(); + + Telemetry::Instance().LogColorSelectionUsed(); + + Search search(screenInfo, str, Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); + while (search.FindNext()) + { + search.Color(TextAttribute{ static_cast(ulAttr) }); + } + } + } + CATCH_LOG(); + } + else + { + ColorSelection(_srSelectionRect, TextAttribute{ static_cast(ulAttr) }); + ClearSelection(); + } + + return true; + } + + return false; +} + +// Routine Description: +// - Handles a keyboard event for selection in mark mode +// Arguments: +// - pInputKeyInfo : The key press state information from the keyboard +// Return Value: +// - True if the event is handled. False otherwise. +bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKeyInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey(); + + // we're selecting via keyboard -- handle keystrokes + if (wVirtualKeyCode == VK_RIGHT || + wVirtualKeyCode == VK_LEFT || + wVirtualKeyCode == VK_UP || + wVirtualKeyCode == VK_DOWN || + wVirtualKeyCode == VK_NEXT || + wVirtualKeyCode == VK_PRIOR || + wVirtualKeyCode == VK_END || + wVirtualKeyCode == VK_HOME) + { + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + TextBuffer& textBuffer = ScreenInfo.GetTextBuffer(); + SHORT iNextRightX = 0; + SHORT iNextLeftX = 0; + + const COORD cursorPos = textBuffer.GetCursor().GetPosition(); + + try + { + auto it = ScreenInfo.GetCellLineDataAt(cursorPos); + + // calculate next right + if (it->DbcsAttr().IsLeading()) + { + iNextRightX = 2; + } + else + { + iNextRightX = 1; + } + + // calculate next left + if (cursorPos.X > 0) + { + it--; + if (it->DbcsAttr().IsTrailing()) + { + iNextLeftX = 2; + } + else if (it->DbcsAttr().IsLeading()) + { + if (cursorPos.X - 1 > 0) + { + it--; + if (it->DbcsAttr().IsTrailing()) + { + iNextLeftX = 3; + } + else + { + iNextLeftX = 2; + } + } + else + { + iNextLeftX = 1; + } + } + else + { + iNextLeftX = 1; + } + } + } + CATCH_LOG(); + + Cursor& cursor = textBuffer.GetCursor(); + switch (wVirtualKeyCode) + { + case VK_RIGHT: + { + if (cursorPos.X + iNextRightX < ScreenInfo.GetBufferSize().Width()) + { + cursor.IncrementXPosition(iNextRightX); + } + break; + } + + case VK_LEFT: + { + if (cursorPos.X > 0) + { + cursor.DecrementXPosition(iNextLeftX); + } + break; + } + + case VK_UP: + { + if (cursorPos.Y > 0) + { + cursor.DecrementYPosition(1); + } + break; + } + + case VK_DOWN: + { + if (cursorPos.Y + 1 < ScreenInfo.GetTerminalBufferSize().Height()) + { + cursor.IncrementYPosition(1); + } + break; + } + + case VK_NEXT: + { + cursor.IncrementYPosition(ScreenInfo.GetViewport().Height() - 1); + const COORD coordBufferSize = ScreenInfo.GetTerminalBufferSize().Dimensions(); + if (cursor.GetPosition().Y >= coordBufferSize.Y) + { + cursor.SetYPosition(coordBufferSize.Y - 1); + } + break; + } + + case VK_PRIOR: + { + cursor.DecrementYPosition(ScreenInfo.GetViewport().Height() - 1); + if (cursor.GetPosition().Y < 0) + { + cursor.SetYPosition(0); + } + break; + } + + case VK_END: + { + // End by itself should go to end of current line. Ctrl-End should go to end of buffer. + cursor.SetXPosition(ScreenInfo.GetBufferSize().RightInclusive()); + + if (pInputKeyInfo->IsCtrlPressed()) + { + COORD coordValidEnd; + GetValidAreaBoundaries(nullptr, &coordValidEnd); + + // Adjust Y position of cursor to the final line with valid text + cursor.SetYPosition(coordValidEnd.Y); + } + break; + } + + case VK_HOME: + { + // Home by itself should go to the beginning of the current line. Ctrl-Home should go to the beginning of + // the buffer + cursor.SetXPosition(0); + + if (pInputKeyInfo->IsCtrlPressed()) + { + cursor.SetYPosition(0); + } + break; + } + + default: + FAIL_FAST_HR(E_NOTIMPL); + } + + // see if shift is down. if so, we're extending the selection. otherwise, we're resetting the anchor + if (ServiceLocator::LocateInputServices()->GetKeyState(VK_SHIFT) & KEY_PRESSED) + { + // if we're just starting to "extend" our selection from moving around as a cursor + // then attempt to set the alternate selection state based on the ALT key right now + if (!IsAreaSelected()) + { + CheckAndSetAlternateSelection(); + } + + ExtendSelection(cursor.GetPosition()); + } + else + { + // if the selection was not empty, reset the anchor + if (IsAreaSelected()) + { + HideSelection(); + _dwSelectionFlags &= ~CONSOLE_SELECTION_NOT_EMPTY; + _fUseAlternateSelection = false; + } + + cursor.SetHasMoved(true); + _coordSelectionAnchor = textBuffer.GetCursor().GetPosition(); + ScreenInfo.MakeCursorVisible(_coordSelectionAnchor, false); + _srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X; + _srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y; + } + return true; + } + + return false; +} + +#pragma region Calculation/Support for keyboard selection + +// Routine Description: +// - Retrieves the boundaries of the input line (first and last char positions) +// Arguments: +// - pcoordInputStart - Position of the first character in the input line +// - pcoordInputEnd - Position of the last character in the input line +// Return Value: +// - If true, the boundaries returned are valid. If false, they should be discarded. +[[nodiscard]] +bool Selection::s_GetInputLineBoundaries(_Out_opt_ COORD* const pcoordInputStart, _Out_opt_ COORD* const pcoordInputEnd) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize(); + + auto& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); + + const auto pendingCookedRead = gci.HasPendingCookedRead(); + const auto isVisible = CommandLine::Instance().IsVisible(); + + // if we have no read data, we have no input line. + if (!pendingCookedRead || gci.CookedReadData().VisibleCharCount() == 0 || !isVisible) + { + return false; + } + + const auto& cookedRead = gci.CookedReadData(); + const COORD coordStart = cookedRead.OriginalCursorPosition(); + COORD coordEnd = cookedRead.OriginalCursorPosition(); + + if (coordEnd.X < 0 && coordEnd.Y < 0) + { + // if the original cursor position from the input line data is invalid, then the buffer cursor position is the final position + coordEnd = textBuffer.GetCursor().GetPosition(); + } + else + { + // otherwise, we need to add the number of characters in the input line to the original cursor position + bufferSize.MoveInBounds(cookedRead.VisibleCharCount(), coordEnd); + } + + // - 1 so the coordinate is on top of the last position of the text, not one past it. + bufferSize.MoveInBounds(-1, coordEnd); + + if (pcoordInputStart != nullptr) + { + pcoordInputStart->X = coordStart.X; + pcoordInputStart->Y = coordStart.Y; + } + + if (pcoordInputEnd != nullptr) + { + pcoordInputEnd->X = coordEnd.X; + pcoordInputEnd->Y = coordEnd.Y; + } + + return true; +} + +// Routine Description: +// - Gets the boundaries of all valid text on the screen. +// Includes the output/back buffer as well as the input line text. +// Arguments: +// - pcoordInputStart - Position of the first character in the buffer +// - pcoordInputEnd - Position of the last character in the buffer +// Return Value: +// - If true, the boundaries returned are valid. If false, they should be discarded. +void Selection::GetValidAreaBoundaries(_Out_opt_ COORD* const pcoordValidStart, _Out_opt_ COORD* const pcoordValidEnd) const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + COORD coordEnd; + coordEnd.X = 0; + coordEnd.Y = 0; + + const bool fHaveInput = s_GetInputLineBoundaries(nullptr, &coordEnd); + + if (!fHaveInput) + { + if (IsInSelectingState() && IsKeyboardMarkSelection()) + { + coordEnd = _coordSavedCursorPosition; + } + else + { + coordEnd = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition(); + } + } + + if (pcoordValidStart != nullptr) + { + // valid area always starts at 0,0 + pcoordValidStart->X = 0; + pcoordValidStart->Y = 0; + } + + if (pcoordValidEnd != nullptr) + { + pcoordValidEnd->X = coordEnd.X; + pcoordValidEnd->Y = coordEnd.Y; + } +} + +// Routine Description: +// - Determines if a coordinate lies between the start and end positions +// - NOTE: Is inclusive of the edges of the boundary. +// Arguments: +// - coordPosition - The position to test +// - coordFirst - The start or left most edge of the regional boundary. +// - coordSecond - The end or right most edge of the regional boundary. +// Return Value: +// - True if it's within the bounds (inclusive). False otherwise. +bool Selection::s_IsWithinBoundaries(const COORD coordPosition, const COORD coordStart, const COORD coordEnd) +{ + bool fInBoundaries = false; + + if (Utils::s_CompareCoords(coordStart, coordPosition) <= 0) + { + if (Utils::s_CompareCoords(coordPosition, coordEnd) <= 0) + { + fInBoundaries = true; + } + } + + return fInBoundaries; +} + +#pragma endregion diff --git a/src/host/selectionState.cpp b/src/host/selectionState.cpp new file mode 100644 index 000000000..ded6b4c5c --- /dev/null +++ b/src/host/selectionState.cpp @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#include "../types/inc/viewport.hpp" + +using namespace Microsoft::Console::Types; + + +// Routine Description: +// - Determines whether the console is in a selecting state +// Arguments: +// - (gets global state) +// Return Value: +// - True if the console is in a selecting state. False otherwise. +bool Selection::IsInSelectingState() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return WI_IsFlagSet(gci.Flags, CONSOLE_SELECTING); +} + +// Routine Description: +// - Helps set the global selecting state. +// Arguments: +// - fSelectingOn - Set true to set the global flag on. False to turn the global flag off. +// Return Value: +// - +void Selection::_SetSelectingState(const bool fSelectingOn) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WI_UpdateFlag(gci.Flags, CONSOLE_SELECTING, fSelectingOn); +} + +// Routine Description: +// - Determines whether the console should do selections with the mouse +// a.k.a. "Quick Edit" mode +// Arguments: +// - (gets global state) +// Return Value: +// - True if quick edit mode is enabled. False otherwise. +bool Selection::IsInQuickEditMode() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE); +} + +// Routine Description: +// - Determines whether we are performing a line selection right now +// Arguments: +// - +// Return Value: +// - True if the selection is to be treated line by line. False if it is to be a block. +bool Selection::IsLineSelection() const +{ + // if line selection is on and alternate is off -OR- + // if line selection is off and alternate is on... + + return (_fLineSelection != _fUseAlternateSelection); +} + +// Routine Description: +// - Assures that the alternate selection flag is flipped in line with the requested format. +// If true, we'll align to ensure line selection is used. If false, we'll make sure box selection is used. +// Arguments: +// - fAlignToLineSelect - whether or not to use line selection +// Return Value: +// - +void Selection::_AlignAlternateSelection(const bool fAlignToLineSelect) +{ + if (fAlignToLineSelect) + { + // states are opposite when in line selection. + // e.g. Line = true, Alternate = false. + // and Line = false, Alternate = true. + _fUseAlternateSelection = !_fLineSelection; + } + else + { + // states are the same when in box selection. + // e.g. Line = true, Alternate = true. + // and Line = false, Alternate = false. + _fUseAlternateSelection = _fLineSelection; + } +} + +// Routine Description: +// - Determines whether the selection area is empty. +// Arguments: +// - +// Return Value: +// - True if the selection variables contain valid selection data. False otherwise. +bool Selection::IsAreaSelected() const +{ + return WI_IsFlagSet(_dwSelectionFlags, CONSOLE_SELECTION_NOT_EMPTY); +} + +// Routine Description: +// - Determines whether mark mode specifically started this selction. +// Arguments: +// - +// Return Value: +// - True if the selection was started as mark mode. False otherwise. +bool Selection::IsKeyboardMarkSelection() const +{ + return WI_IsFlagClear(_dwSelectionFlags, CONSOLE_MOUSE_SELECTION); +} + +// Routine Description: +// - Determines whether a mouse event was responsible for initiating this selection. +// - This primarily refers to mouse drag in QuickEdit mode. +// - However, it refers to any non-mark-mode selection, whether the mouse actually started it or not. +// Arguments: +// - +// Return Value: +// - True if the selection is mouse-initiated. False otherwise. +bool Selection::IsMouseInitiatedSelection() const +{ + return WI_IsFlagSet(_dwSelectionFlags, CONSOLE_MOUSE_SELECTION); +} + +// Routine Description: +// - Determines whether the mouse button is currently being held down +// to extend or otherwise manipulate the selection area. +// Arguments: +// - +// Return Value: +// - True if the mouse button is currently down. False otherwise. +bool Selection::IsMouseButtonDown() const +{ + return WI_IsFlagSet(_dwSelectionFlags, CONSOLE_MOUSE_DOWN); +} + +void Selection::MouseDown() +{ + WI_SetFlag(_dwSelectionFlags, CONSOLE_MOUSE_DOWN); + + // We must capture the mouse on button down to ensure we receive messages if + // it comes back up outside the window. + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->CaptureMouse(); + } +} + +void Selection::MouseUp() +{ + WI_ClearFlag(_dwSelectionFlags, CONSOLE_MOUSE_DOWN); + + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->ReleaseMouse(); + } +} + +// Routine Description: +// - Saves the current cursor position data so it can be manipulated during selection. +// Arguments: +// - textBuffer - text buffer to set cursor data +// Return Value: +// - +void Selection::_SaveCursorData(const Cursor& cursor) noexcept +{ + _coordSavedCursorPosition = cursor.GetPosition(); + _ulSavedCursorSize = cursor.GetSize(); + _fSavedCursorVisible = cursor.IsVisible(); + _savedCursorColor = cursor.GetColor(); + _savedCursorType = cursor.GetType(); +} + +// Routine Description: +// - Restores the cursor position data that was captured when selection was started. +// Arguments: +// - (Restores global state) +// Return Value: +// - +void Selection::_RestoreDataToCursor(Cursor& cursor) noexcept +{ + cursor.SetSize(_ulSavedCursorSize); + cursor.SetIsVisible(_fSavedCursorVisible); + cursor.SetColor(_savedCursorColor); + cursor.SetType(_savedCursorType); + cursor.SetIsOn(true); + cursor.SetPosition(_coordSavedCursorPosition); +} + +// Routine Description: +// - Gets the current selection anchor position +// Arguments: +// - none +// Return Value: +// - current selection anchor +COORD Selection::GetSelectionAnchor() const noexcept +{ + return _coordSelectionAnchor; +} + +// Routine Description: +// - Gets the current selection rectangle +// Arguments: +// - none +// Return Value: +// - The rectangle to fill with selection data. +SMALL_RECT Selection::GetSelectionRectangle() const noexcept +{ + return _srSelectionRect; +} + +// Routine Description: +// - Gets the publically facing set of selection flags. +// Strips out any internal flags in use. +// Arguments: +// - none +// Return Value: +// - The public selection flags +DWORD Selection::GetPublicSelectionFlags() const noexcept +{ + // CONSOLE_SELECTION_VALID is the union (binary OR) of all externally valid flags in wincon.h + return (_dwSelectionFlags & CONSOLE_SELECTION_VALID); +} + +// Routine Description: +// - Sets the line selection status. +// If true, we'll use line selection. If false, we'll use traditional box selection. +// Arguments: +// - fLineSelectionOn - whether or not to use line selection +// Return Value: +// - +void Selection::SetLineSelection(const bool fLineSelectionOn) +{ + if (_fLineSelection != fLineSelectionOn) + { + // Ensure any existing selections are cleared so the draw state is updated appropriately. + ClearSelection(); + + _fLineSelection = fLineSelectionOn; + } +} + +// Routine Description: +// - checks if the selection can be changed by a mouse drag. +// - this is to allow double-click selection and click-mouse-drag selection to play nice together instead of +// the click-mouse-drag selection overwriting the double-click selection in case the user moves the mouse +// while double-clicking. +// Arguments: +// - mousePosition - current mouse position +// Return Value: +// - true if the selection can be changed by a mouse drag +bool Selection::ShouldAllowMouseDragSelection(const COORD mousePosition) const noexcept +{ + const Viewport viewport = Viewport::FromInclusive(_srSelectionRect); + const bool selectionContainsMouse = viewport.IsInBounds(mousePosition); + return _allowMouseDragSelection || !selectionContainsMouse; +} diff --git a/src/host/server.h b/src/host/server.h new file mode 100644 index 000000000..820c15f93 --- /dev/null +++ b/src/host/server.h @@ -0,0 +1,178 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- server.h + +Abstract: +- This module contains the internal structures and definitions used by the console server. + +Author: +- Therese Stowell (ThereseS) 12-Nov-1990 + +Revision History: +--*/ + +#pragma once + +#include "IIoProvider.hpp" + +#include "settings.hpp" + +#include "conimeinfo.h" +#include "..\terminal\adapter\MouseInput.hpp" +#include "VtIo.hpp" +#include "CursorBlinker.hpp" + +#include "..\server\ProcessList.h" +#include "..\server\WaitQueue.h" + +#include "..\host\RenderData.hpp" + +#include "..\inc\IDefaultColorProvider.hpp" + +// Flags flags +#define CONSOLE_IS_ICONIC 0x00000001 +#define CONSOLE_OUTPUT_SUSPENDED 0x00000002 +#define CONSOLE_HAS_FOCUS 0x00000004 +#define CONSOLE_IGNORE_NEXT_MOUSE_INPUT 0x00000008 +#define CONSOLE_SELECTING 0x00000010 +#define CONSOLE_SCROLLING 0x00000020 +// unused (CONSOLE_DISABLE_CLOSE) 0x00000040 +// unused (CONSOLE_USE_POLY_TEXT) 0x00000080 + +// Removed Oct 2017 - added a headless mode, which revealed that the consumption +// of this flag was redundant. +// unused (CONSOLE_NO_WINDOW) 0x00000100 + +// unused (CONSOLE_VDM_REGISTERED) 0x00000200 +#define CONSOLE_UPDATING_SCROLL_BARS 0x00000400 +#define CONSOLE_QUICK_EDIT_MODE 0x00000800 +#define CONSOLE_CONNECTED_TO_EMULATOR 0x00002000 +// unused (CONSOLE_FULLSCREEN_NOPAINT) 0x00004000 +#define CONSOLE_QUIT_POSTED 0x00008000 +#define CONSOLE_AUTO_POSITION 0x00010000 +#define CONSOLE_IGNORE_NEXT_KEYUP 0x00020000 +// unused (CONSOLE_WOW_REGISTERED) 0x00040000 +#define CONSOLE_HISTORY_NODUP 0x00100000 +#define CONSOLE_SCROLLBAR_TRACKING 0x00200000 +#define CONSOLE_SETTING_WINDOW_SIZE 0x00800000 +// unused (CONSOLE_VDM_HIDDEN_WINDOW) 0x01000000 +// unused (CONSOLE_OS2_REGISTERED) 0x02000000 +// unused (CONSOLE_OS2_OEM_FORMAT) 0x04000000 +// unused (CONSOLE_JUST_VDM_UNREGISTERED) 0x08000000 +// unused (CONSOLE_FULLSCREEN_INITIALIZED) 0x10000000 +#define CONSOLE_USE_PRIVATE_FLAGS 0x20000000 +// unused (CONSOLE_TSF_ACTIVATED) 0x40000000 +#define CONSOLE_INITIALIZED 0x80000000 + +#define CONSOLE_SUSPENDED (CONSOLE_OUTPUT_SUSPENDED) + +class COOKED_READ_DATA; +class CommandHistory; + +class CONSOLE_INFORMATION : + public Settings, + public Microsoft::Console::IIoProvider, + public Microsoft::Console::IDefaultColorProvider +{ +public: + CONSOLE_INFORMATION(); + ~CONSOLE_INFORMATION(); + CONSOLE_INFORMATION(const CONSOLE_INFORMATION& c) = delete; + CONSOLE_INFORMATION& operator=(const CONSOLE_INFORMATION& c) = delete; + + ConsoleProcessList ProcessHandleList; + InputBuffer* pInputBuffer; + + SCREEN_INFORMATION* ScreenBuffers; // singly linked list + ConsoleWaitQueue OutputQueue; + + DWORD Flags; + + std::atomic PopupCount; + + // the following fields are used for ansi-unicode translation + UINT CP; + UINT OutputCP; + + ULONG CtrlFlags; // indicates outstanding ctrl requests + ULONG LimitingProcessId; + + CPINFO CPInfo; + CPINFO OutputCPInfo; + + ConsoleImeInfo ConsoleIme; + + Microsoft::Console::VirtualTerminal::MouseInput terminalMouseInput; + + void LockConsole(); + bool TryLockConsole(); + void UnlockConsole(); + bool IsConsoleLocked() const; + ULONG GetCSRecursionCount(); + + Microsoft::Console::VirtualTerminal::VtIo* GetVtIo(); + + static void HandleTerminalKeyEventCallback(_Inout_ std::deque>& events); + + SCREEN_INFORMATION& GetActiveOutputBuffer() override; + const SCREEN_INFORMATION& GetActiveOutputBuffer() const override; + bool HasActiveOutputBuffer() const; + + InputBuffer* const GetActiveInputBuffer() const; + + bool IsInVtIoMode() const; + bool HasPendingCookedRead() const noexcept; + const COOKED_READ_DATA& CookedReadData() const noexcept; + COOKED_READ_DATA& CookedReadData() noexcept; + void SetCookedReadData(COOKED_READ_DATA* readData) noexcept; + + COLORREF GetDefaultForeground() const noexcept; + COLORREF GetDefaultBackground() const noexcept; + + void SetTitle(const std::wstring_view newTitle); + void SetTitlePrefix(const std::wstring& newTitlePrefix); + void SetOriginalTitle(const std::wstring& originalTitle); + void SetLinkTitle(const std::wstring& linkTitle); + const std::wstring& GetTitle() const noexcept; + const std::wstring& GetOriginalTitle() const noexcept; + const std::wstring& GetLinkTitle() const noexcept; + const std::wstring GetTitleAndPrefix() const; + + [[nodiscard]] + static NTSTATUS AllocateConsole(const std::wstring_view title); + // MSFT:16886775 : get rid of friends + friend void SetActiveScreenBuffer(_Inout_ SCREEN_INFORMATION& screenInfo); + friend class SCREEN_INFORMATION; + friend class CommonState; + Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept; + + CHAR_INFO AsCharInfo(const OutputCellView& cell) const noexcept; + + RenderData renderData; + +private: + CRITICAL_SECTION _csConsoleLock; // serialize input and output using this + std::wstring _Title; + std::wstring _TitlePrefix; // Eg Select, Mark - things that we manually prepend to the title. + std::wstring _OriginalTitle; + std::wstring _LinkTitle; // Path to .lnk file + SCREEN_INFORMATION* pCurrentScreenBuffer; + COOKED_READ_DATA* _cookedReadData; // non-ownership pointer + + Microsoft::Console::VirtualTerminal::VtIo _vtIo; + Microsoft::Console::CursorBlinker _blinker; + +}; + +#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread) + +#define CONSOLE_STATUS_WAIT 0xC0030001 +#define CONSOLE_STATUS_READ_COMPLETE 0xC0030002 +#define CONSOLE_STATUS_WAIT_NO_BLOCK 0xC0030003 + +#include "..\server\ObjectHandle.h" + +void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo); diff --git a/src/host/settings.cpp b/src/host/settings.cpp new file mode 100644 index 000000000..5fb730505 --- /dev/null +++ b/src/host/settings.cpp @@ -0,0 +1,948 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "settings.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "../types/inc/utils.hpp" + +#pragma hdrstop + +#define DEFAULT_NUMBER_OF_COMMANDS 25 +#define DEFAULT_NUMBER_OF_BUFFERS 4 + +Settings::Settings() : + _dwHotKey(0), + _dwStartupFlags(0), + _wFillAttribute(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE), // White (not bright) on black by default + _wPopupFillAttribute(FOREGROUND_RED | FOREGROUND_BLUE | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY), // Purple on white (bright) by default + _wShowWindow(SW_SHOWNORMAL), + _wReserved(0), + // dwScreenBufferSize initialized below + // dwWindowSize initialized below + // dwWindowOrigin initialized below + _nFont(0), + // dwFontSize initialized below + _uFontFamily(0), + _uFontWeight(0), + // FaceName initialized below + _uCursorSize(CURSOR_SMALL_SIZE), + _bFullScreen(false), + _bQuickEdit(true), + _bInsertMode(true), + _bAutoPosition(true), + _uHistoryBufferSize(DEFAULT_NUMBER_OF_COMMANDS), + _uNumberOfHistoryBuffers(DEFAULT_NUMBER_OF_BUFFERS), + _bHistoryNoDup(false), + // ColorTable initialized below + _uCodePage(ServiceLocator::LocateGlobals().uiOEMCP), + _uScrollScale(1), + _bLineSelection(true), + _bWrapText(true), + _fCtrlKeyShortcutsDisabled(false), + _bWindowAlpha(BYTE_MAX), // 255 alpha = opaque. 0 = transparent. + _fFilterOnPaste(false), + _fTrimLeadingZeros(FALSE), + _fEnableColorSelection(FALSE), + _fAllowAltF4Close(true), + _dwVirtTermLevel(0), + _fUseWindowSizePixels(false), + _fAutoReturnOnNewline(true), // the historic Windows behavior defaults this to on. + _fRenderGridWorldwide(false), // historically grid lines were only rendered in DBCS codepages, so this is false by default unless otherwise specified. + // window size pixels initialized below + _fInterceptCopyPaste(0), + _DefaultForeground(INVALID_COLOR), + _DefaultBackground(INVALID_COLOR), + _fUseDx(false), + _fCopyColor(false) +{ + _dwScreenBufferSize.X = 80; + _dwScreenBufferSize.Y = 25; + + _dwWindowSize.X = _dwScreenBufferSize.X; + _dwWindowSize.Y = _dwScreenBufferSize.Y; + + _dwWindowSizePixels = { 0 }; + + _dwWindowOrigin.X = 0; + _dwWindowOrigin.Y = 0; + + _dwFontSize.X = 0; + _dwFontSize.Y = 16; + + ZeroMemory((void*)&_FaceName, sizeof(_FaceName)); + wcscpy_s(_FaceName, DEFAULT_TT_FONT_FACENAME); + + ZeroMemory((void*)&_LaunchFaceName, sizeof(_LaunchFaceName)); + + _CursorColor = Cursor::s_InvertCursorColor; + _CursorType = CursorType::Legacy; + + + gsl::span tableView = { _ColorTable, gsl::narrow(COLOR_TABLE_SIZE) }; + gsl::span xtermTableView = { _XtermColorTable, gsl::narrow(XTERM_COLOR_TABLE_SIZE) }; + ::Microsoft::Console::Utils::Initialize256ColorTable(xtermTableView); + ::Microsoft::Console::Utils::InitializeCampbellColorTable(tableView); + +} + + +// Routine Description: +// - Applies hardcoded default settings that are in line with what is defined +// in our Windows edition manifest (living in win32k-settings.man). +// - NOTE: This exists in case we cannot access the registry on desktop platforms. +// We will use this to provide better defaults than the constructor values which +// are optimized for OneCore. +// Arguments: +// - +// Return Value: +// - - Adjusts internal state only. +void Settings::ApplyDesktopSpecificDefaults() +{ + _dwFontSize.X = 0; + _dwFontSize.Y = 16; + _uFontFamily = 0; + _dwScreenBufferSize.X = 120; + _dwScreenBufferSize.Y = 9001; + _uCursorSize = 25; + _dwWindowSize.X = 120; + _dwWindowSize.Y = 30; + _wFillAttribute = 0x7; + _wPopupFillAttribute = 0xf5; + wcscpy_s(_FaceName, L"__DefaultTTFont__"); + _uFontWeight = 0; + _bInsertMode = TRUE; + _bFullScreen = FALSE; + _fCtrlKeyShortcutsDisabled = false; + _bWrapText = true; + _bLineSelection = TRUE; + _bWindowAlpha = 255; + _fFilterOnPaste = TRUE; + _bQuickEdit = TRUE; + _uHistoryBufferSize = 50; + _uNumberOfHistoryBuffers = 4; + _bHistoryNoDup = FALSE; + + gsl::span tableView = { _ColorTable, gsl::narrow(COLOR_TABLE_SIZE) }; + ::Microsoft::Console::Utils::InitializeCampbellColorTable(tableView); + + _fTrimLeadingZeros = false; + _fEnableColorSelection = false; + _uScrollScale = 1; +} + +void Settings::ApplyStartupInfo(const Settings* const pStartupSettings) +{ + const DWORD dwFlags = pStartupSettings->_dwStartupFlags; + + // See: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686331(v=vs.85).aspx + + // Note: These attributes do not get sent to us if we started conhost + // directly. See minkernel/console/client/dllinit for the + // initialization of these values for cmdline applications. + + if (WI_IsFlagSet(dwFlags, STARTF_USECOUNTCHARS)) + { + _dwScreenBufferSize = pStartupSettings->_dwScreenBufferSize; + } + + if (WI_IsFlagSet(dwFlags, STARTF_USESIZE)) + { + // WARNING: This size is in pixels when passed in the create process call. + // It will need to be divided by the font size before use. + // All other Window Size values (from registry/shortcuts) are stored in characters. + _dwWindowSizePixels = pStartupSettings->_dwWindowSize; + _fUseWindowSizePixels = true; + } + + if (WI_IsFlagSet(dwFlags, STARTF_USEPOSITION)) + { + _dwWindowOrigin = pStartupSettings->_dwWindowOrigin; + _bAutoPosition = FALSE; + } + + if (WI_IsFlagSet(dwFlags, STARTF_USEFILLATTRIBUTE)) + { + _wFillAttribute = pStartupSettings->_wFillAttribute; + } + + if (WI_IsFlagSet(dwFlags, STARTF_USESHOWWINDOW)) + { + _wShowWindow = pStartupSettings->_wShowWindow; + } +} + +// Method Description: +// - Applies settings passed on the commandline to this settings structure. +// Currently, the only settings that can be passed on the commandline are +// the initial width and height of the screenbuffer/viewport. +// Arguments: +// - consoleArgs: A reference to a parsed command-line args object. +// Return Value: +// - +void Settings::ApplyCommandlineArguments(const ConsoleArguments& consoleArgs) +{ + const short width = consoleArgs.GetWidth(); + const short height = consoleArgs.GetHeight(); + + if (width > 0 && height > 0) + { + _dwScreenBufferSize.X = width; + _dwWindowSize.X = width; + + _dwScreenBufferSize.Y = height; + _dwWindowSize.Y = height; + } + else if (ServiceLocator::LocateGlobals().getConsoleInformation().IsInVtIoMode()) + { + // If we're a PTY but we weren't explicitly told a size, use the window size as the buffer size. + _dwScreenBufferSize = _dwWindowSize; + } +} + +// WARNING: this function doesn't perform any validation or conversion +void Settings::InitFromStateInfo(_In_ PCONSOLE_STATE_INFO pStateInfo) +{ + _wFillAttribute = pStateInfo->ScreenAttributes; + _wPopupFillAttribute = pStateInfo->PopupAttributes; + _dwScreenBufferSize = pStateInfo->ScreenBufferSize; + _dwWindowSize = pStateInfo->WindowSize; + _dwWindowOrigin.X = (SHORT)pStateInfo->WindowPosX; + _dwWindowOrigin.Y = (SHORT)pStateInfo->WindowPosY; + _dwFontSize = pStateInfo->FontSize; + _uFontFamily = pStateInfo->FontFamily; + _uFontWeight = pStateInfo->FontWeight; + StringCchCopyW(_FaceName, ARRAYSIZE(_FaceName), pStateInfo->FaceName); + _uCursorSize = pStateInfo->CursorSize; + _bFullScreen = pStateInfo->FullScreen; + _bQuickEdit = pStateInfo->QuickEdit; + _bAutoPosition = pStateInfo->AutoPosition; + _bInsertMode = pStateInfo->InsertMode; + _bHistoryNoDup = pStateInfo->HistoryNoDup; + _uHistoryBufferSize = pStateInfo->HistoryBufferSize; + _uNumberOfHistoryBuffers = pStateInfo->NumberOfHistoryBuffers; + memmove(_ColorTable, pStateInfo->ColorTable, sizeof(_ColorTable)); + _uCodePage = pStateInfo->CodePage; + _bWrapText = !!pStateInfo->fWrapText; + _fFilterOnPaste = pStateInfo->fFilterOnPaste; + _fCtrlKeyShortcutsDisabled = pStateInfo->fCtrlKeyShortcutsDisabled; + _bLineSelection = pStateInfo->fLineSelection; + _bWindowAlpha = pStateInfo->bWindowTransparency; + _CursorColor = pStateInfo->CursorColor; + _CursorType = static_cast(pStateInfo->CursorType); + _fInterceptCopyPaste = pStateInfo->InterceptCopyPaste; + _DefaultForeground = pStateInfo->DefaultForeground; + _DefaultBackground = pStateInfo->DefaultBackground; + _TerminalScrolling = pStateInfo->TerminalScrolling; +} + +// Method Description: +// - Create a CONSOLE_STATE_INFO with the current state of this settings structure. +// Arguments: +// - +// Return Value: +// - a CONSOLE_STATE_INFO with the current state of this settings structure. +CONSOLE_STATE_INFO Settings::CreateConsoleStateInfo() const +{ + CONSOLE_STATE_INFO csi = {0}; + csi.ScreenAttributes = _wFillAttribute; + csi.PopupAttributes = _wPopupFillAttribute; + csi.ScreenBufferSize = _dwScreenBufferSize; + csi.WindowSize = _dwWindowSize; + csi.WindowPosX = (SHORT)_dwWindowOrigin.X; + csi.WindowPosY = (SHORT)_dwWindowOrigin.Y; + csi.FontSize = _dwFontSize; + csi.FontFamily = _uFontFamily; + csi.FontWeight = _uFontWeight; + StringCchCopyW(csi.FaceName, ARRAYSIZE(_FaceName), _FaceName); + csi.CursorSize = _uCursorSize; + csi.FullScreen = _bFullScreen; + csi.QuickEdit = _bQuickEdit; + csi.AutoPosition = _bAutoPosition; + csi.InsertMode = _bInsertMode; + csi.HistoryNoDup = _bHistoryNoDup; + csi.HistoryBufferSize = _uHistoryBufferSize; + csi.NumberOfHistoryBuffers = _uNumberOfHistoryBuffers; + memmove(csi.ColorTable, _ColorTable, sizeof(_ColorTable)); + csi.CodePage = _uCodePage; + csi.fWrapText = !!_bWrapText; + csi.fFilterOnPaste = _fFilterOnPaste; + csi.fCtrlKeyShortcutsDisabled = _fCtrlKeyShortcutsDisabled; + csi.fLineSelection = _bLineSelection; + csi.bWindowTransparency = _bWindowAlpha; + csi.CursorColor = _CursorColor; + csi.CursorType = static_cast(_CursorType); + csi.InterceptCopyPaste = _fInterceptCopyPaste; + csi.DefaultForeground = _DefaultForeground; + csi.DefaultBackground = _DefaultBackground; + csi.TerminalScrolling = _TerminalScrolling; + return csi; +} + + +void Settings::Validate() +{ + // If we were explicitly given a size in pixels from the startup info, divide by the font to turn it into characters. + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms686331%28v=vs.85%29.aspx + if (WI_IsFlagSet(_dwStartupFlags, STARTF_USESIZE)) + { + // TODO: FIX + //// Get the font that we're going to use to convert pixels to characters. + //DWORD const dwFontIndexWant = FindCreateFont(_uFontFamily, + // _FaceName, + // _dwFontSize, + // _uFontWeight, + // _uCodePage); + + //_dwWindowSize.X /= g_pfiFontInfo[dwFontIndexWant].Size.X; + //_dwWindowSize.Y /= g_pfiFontInfo[dwFontIndexWant].Size.Y; + } + + // minimum screen buffer size 1x1 + _dwScreenBufferSize.X = std::max(_dwScreenBufferSize.X, 1i16); + _dwScreenBufferSize.Y = std::max(_dwScreenBufferSize.Y, 1i16); + + // minimum window size size 1x1 + _dwWindowSize.X = std::max(_dwWindowSize.X, 1i16); + _dwWindowSize.Y = std::max(_dwWindowSize.Y, 1i16); + + // if buffer size is less than window size, increase buffer size to meet window size + _dwScreenBufferSize.X = std::max(_dwWindowSize.X, _dwScreenBufferSize.X); + _dwScreenBufferSize.Y = std::max(_dwWindowSize.Y, _dwScreenBufferSize.Y); + + // ensure that the window alpha value is not below the minimum. (no invisible windows) + // if it's below minimum, just set it to the opaque value + if (_bWindowAlpha < MIN_WINDOW_OPACITY) + { + _bWindowAlpha = BYTE_MAX; + } + + // If text wrapping is on, ensure that the window width is the same as the buffer width. + if (_bWrapText) + { + _dwWindowSize.X = _dwScreenBufferSize.X; + } + + // Ensure that our fill attributes only contain colors and not any box drawing or invert attributes. + WI_ClearAllFlags(_wFillAttribute, ~(FG_ATTRS | BG_ATTRS)); + WI_ClearAllFlags(_wPopupFillAttribute, ~(FG_ATTRS | BG_ATTRS)); + + FAIL_FAST_IF(!(_dwWindowSize.X > 0)); + FAIL_FAST_IF(!(_dwWindowSize.Y > 0)); + FAIL_FAST_IF(!(_dwScreenBufferSize.X > 0)); + FAIL_FAST_IF(!(_dwScreenBufferSize.Y > 0)); +} + +DWORD Settings::GetVirtTermLevel() const +{ + return _dwVirtTermLevel; +} +void Settings::SetVirtTermLevel(const DWORD dwVirtTermLevel) +{ + _dwVirtTermLevel = dwVirtTermLevel; +} + +bool Settings::IsAltF4CloseAllowed() const +{ + return _fAllowAltF4Close; +} +void Settings::SetAltF4CloseAllowed(const bool fAllowAltF4Close) +{ + _fAllowAltF4Close = fAllowAltF4Close; +} + +bool Settings::IsReturnOnNewlineAutomatic() const +{ + return _fAutoReturnOnNewline; +} +void Settings::SetAutomaticReturnOnNewline(const bool fAutoReturnOnNewline) +{ + _fAutoReturnOnNewline = fAutoReturnOnNewline; +} + +bool Settings::IsGridRenderingAllowedWorldwide() const +{ + return _fRenderGridWorldwide; +} +void Settings::SetGridRenderingAllowedWorldwide(const bool fGridRenderingAllowed) +{ + // Only trigger a notification and update the status if something has changed. + if (_fRenderGridWorldwide != fGridRenderingAllowed) + { + _fRenderGridWorldwide = fGridRenderingAllowed; + + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender->TriggerRedrawAll(); + } + } +} + +bool Settings::GetFilterOnPaste() const +{ + return _fFilterOnPaste; +} +void Settings::SetFilterOnPaste(const bool fFilterOnPaste) +{ + _fFilterOnPaste = fFilterOnPaste; +} + +const WCHAR* const Settings::GetLaunchFaceName() const +{ + return _LaunchFaceName; +} +void Settings::SetLaunchFaceName(_In_ PCWSTR const LaunchFaceName, const size_t cchLength) +{ + StringCchCopyW(_LaunchFaceName, cchLength, LaunchFaceName); +} + +UINT Settings::GetCodePage() const +{ + return _uCodePage; +} +void Settings::SetCodePage(const UINT uCodePage) +{ + _uCodePage = uCodePage; +} + +UINT Settings::GetScrollScale() const +{ + return _uScrollScale; +} +void Settings::SetScrollScale(const UINT uScrollScale) +{ + _uScrollScale = uScrollScale; +} + +bool Settings::GetTrimLeadingZeros() const +{ + return _fTrimLeadingZeros; +} +void Settings::SetTrimLeadingZeros(const bool fTrimLeadingZeros) +{ + _fTrimLeadingZeros = fTrimLeadingZeros; +} + +bool Settings::GetEnableColorSelection() const +{ + return _fEnableColorSelection; +} +void Settings::SetEnableColorSelection(const bool fEnableColorSelection) +{ + _fEnableColorSelection = fEnableColorSelection; +} + +bool Settings::GetLineSelection() const +{ + return _bLineSelection; +} +void Settings::SetLineSelection(const bool bLineSelection) +{ + _bLineSelection = bLineSelection; +} + +bool Settings::GetWrapText () const +{ + return _bWrapText; +} +void Settings::SetWrapText (const bool bWrapText ) +{ + _bWrapText = bWrapText; +} + +bool Settings::GetCtrlKeyShortcutsDisabled () const +{ + return _fCtrlKeyShortcutsDisabled; +} +void Settings::SetCtrlKeyShortcutsDisabled (const bool fCtrlKeyShortcutsDisabled ) +{ + _fCtrlKeyShortcutsDisabled = fCtrlKeyShortcutsDisabled; +} + +BYTE Settings::GetWindowAlpha() const +{ + return _bWindowAlpha; +} +void Settings::SetWindowAlpha(const BYTE bWindowAlpha) +{ + // if we're out of bounds, set it to 100% opacity so it appears as if nothing happened. + _bWindowAlpha = (bWindowAlpha < MIN_WINDOW_OPACITY)? BYTE_MAX : bWindowAlpha; +} + +DWORD Settings::GetHotKey() const +{ + return _dwHotKey; +} +void Settings::SetHotKey(const DWORD dwHotKey) +{ + _dwHotKey = dwHotKey; +} + +DWORD Settings::GetStartupFlags() const +{ + return _dwStartupFlags; +} +void Settings::SetStartupFlags(const DWORD dwStartupFlags) +{ + _dwStartupFlags = dwStartupFlags; +} + +WORD Settings::GetFillAttribute() const +{ + return _wFillAttribute; +} +void Settings::SetFillAttribute(const WORD wFillAttribute) +{ + _wFillAttribute = wFillAttribute; + + // Do not allow the default fill attribute to use any attrs other than fg/bg colors. + // This prevents us from accidentally inverting everything or suddenly drawing lines + // everywhere by default. + WI_ClearAllFlags(_wFillAttribute, ~(FG_ATTRS | BG_ATTRS)); +} + +WORD Settings::GetPopupFillAttribute() const +{ + return _wPopupFillAttribute; +} +void Settings::SetPopupFillAttribute(const WORD wPopupFillAttribute) +{ + _wPopupFillAttribute = wPopupFillAttribute; + + // Do not allow the default popup fill attribute to use any attrs other than fg/bg colors. + // This prevents us from accidentally inverting everything or suddenly drawing lines + // everywhere by defualt. + WI_ClearAllFlags(_wPopupFillAttribute, ~(FG_ATTRS | BG_ATTRS)); +} + +WORD Settings::GetShowWindow() const +{ + return _wShowWindow; +} +void Settings::SetShowWindow(const WORD wShowWindow) +{ + _wShowWindow = wShowWindow; +} + +WORD Settings::GetReserved() const +{ + return _wReserved; +} +void Settings::SetReserved(const WORD wReserved) +{ + _wReserved = wReserved; +} + +COORD Settings::GetScreenBufferSize() const +{ + return _dwScreenBufferSize; +} +void Settings::SetScreenBufferSize(const COORD dwScreenBufferSize) +{ + _dwScreenBufferSize = dwScreenBufferSize; +} + +COORD Settings::GetWindowSize() const +{ + return _dwWindowSize; +} +void Settings::SetWindowSize(const COORD dwWindowSize) +{ + _dwWindowSize = dwWindowSize; +} + +bool Settings::IsWindowSizePixelsValid() const +{ + return _fUseWindowSizePixels; +} +COORD Settings::GetWindowSizePixels() const +{ + return _dwWindowSizePixels; +} +void Settings::SetWindowSizePixels(const COORD dwWindowSizePixels) +{ + _dwWindowSizePixels = dwWindowSizePixels; +} + +COORD Settings::GetWindowOrigin() const +{ + return _dwWindowOrigin; +} +void Settings::SetWindowOrigin(const COORD dwWindowOrigin) +{ + _dwWindowOrigin = dwWindowOrigin; +} + +DWORD Settings::GetFont() const +{ + return _nFont; +} +void Settings::SetFont(const DWORD nFont) +{ + _nFont = nFont; +} + +COORD Settings::GetFontSize() const +{ + return _dwFontSize; +} +void Settings::SetFontSize(const COORD dwFontSize) +{ + _dwFontSize = dwFontSize; +} + +UINT Settings::GetFontFamily() const +{ + return _uFontFamily; +} +void Settings::SetFontFamily(const UINT uFontFamily) +{ + _uFontFamily = uFontFamily; +} + +UINT Settings::GetFontWeight() const +{ + return _uFontWeight; +} +void Settings::SetFontWeight(const UINT uFontWeight) +{ + _uFontWeight = uFontWeight; +} + +const WCHAR* const Settings::GetFaceName() const +{ + return _FaceName; +} +void Settings::SetFaceName(_In_ PCWSTR const pcszFaceName, const size_t cchLength) +{ + StringCchCopyW(_FaceName, cchLength, pcszFaceName); +} + +UINT Settings::GetCursorSize() const +{ + return _uCursorSize; +} +void Settings::SetCursorSize(const UINT uCursorSize) +{ + _uCursorSize = uCursorSize; +} + +bool Settings::GetFullScreen() const +{ + return _bFullScreen; +} +void Settings::SetFullScreen(const bool bFullScreen) +{ + _bFullScreen = bFullScreen; +} + +bool Settings::GetQuickEdit() const +{ + return _bQuickEdit; +} +void Settings::SetQuickEdit(const bool bQuickEdit) +{ + _bQuickEdit = bQuickEdit; +} + +bool Settings::GetInsertMode() const +{ + return _bInsertMode; +} +void Settings::SetInsertMode(const bool bInsertMode) +{ + _bInsertMode = bInsertMode; +} + +bool Settings::GetAutoPosition() const +{ + return _bAutoPosition; +} +void Settings::SetAutoPosition(const bool bAutoPosition) +{ + _bAutoPosition = bAutoPosition; +} + +UINT Settings::GetHistoryBufferSize() const +{ + return _uHistoryBufferSize; +} +void Settings::SetHistoryBufferSize(const UINT uHistoryBufferSize) +{ + _uHistoryBufferSize = uHistoryBufferSize; +} + +UINT Settings::GetNumberOfHistoryBuffers() const +{ + return _uNumberOfHistoryBuffers; +} +void Settings::SetNumberOfHistoryBuffers(const UINT uNumberOfHistoryBuffers) +{ + _uNumberOfHistoryBuffers = uNumberOfHistoryBuffers; +} + +bool Settings::GetHistoryNoDup() const +{ + return _bHistoryNoDup; +} +void Settings::SetHistoryNoDup(const bool bHistoryNoDup) +{ + _bHistoryNoDup = bHistoryNoDup; +} + +const COLORREF* const Settings::GetColorTable() const +{ + return _ColorTable; +} +void Settings::SetColorTable(_In_reads_(cSize) const COLORREF* const pColorTable, const size_t cSize) +{ + size_t cSizeWritten = std::min(cSize, static_cast(COLOR_TABLE_SIZE)); + + memmove(_ColorTable, pColorTable, cSizeWritten * sizeof(COLORREF)); +} +void Settings::SetColorTableEntry(const size_t index, const COLORREF ColorValue) +{ + if (index < ARRAYSIZE(_ColorTable)) + { + _ColorTable[index] = ColorValue; + } + else + { + _XtermColorTable[index] = ColorValue; + } +} + +bool Settings::IsStartupTitleIsLinkNameSet() const +{ + return WI_IsFlagSet(_dwStartupFlags, STARTF_TITLEISLINKNAME); +} + +bool Settings::IsFaceNameSet() const +{ + return GetFaceName()[0] != '\0'; +} + +void Settings::UnsetStartupFlag(const DWORD dwFlagToUnset) +{ + _dwStartupFlags &= ~dwFlagToUnset; +} + +const size_t Settings::GetColorTableSize() const +{ + return ARRAYSIZE(_ColorTable); +} + +COLORREF Settings::GetColorTableEntry(const size_t index) const +{ + if (index < ARRAYSIZE(_ColorTable)) + { + return _ColorTable[index]; + } + else + { + return _XtermColorTable[index]; + } +} + +// Routine Description: +// - Generates a legacy attribute from the given TextAttributes. +// This needs to be a method on the Settings because the generated index +// is dependent upon the particular values of the color table at the time of reading. +// Parameters: +// - attributes - The TextAttributes to generate a legacy attribute for. +// Return value: +// - A WORD representing the entry in the color table that most closely represents the given fullcolor attributes. +WORD Settings::GenerateLegacyAttributes(const TextAttribute attributes) const +{ + const WORD wLegacyOriginal = attributes.GetLegacyAttributes(); + if (attributes.IsLegacy()) + { + return wLegacyOriginal; + } + // We need to construct the legacy attributes manually + // First start with whatever our default legacy attributes are + BYTE fgIndex = static_cast((_wFillAttribute & FG_ATTRS)); + BYTE bgIndex = static_cast((_wFillAttribute & BG_ATTRS) >> 4); + // If the attributes have any RGB components, we need to match that RGB + // color to a color table value. + if (attributes.IsRgb()) + { + // If the attribute doesn't have a "default" colored *ground, look up + // the nearest color table value for it's *ground. + const COLORREF rgbForeground = LookupForegroundColor(attributes); + fgIndex = attributes.ForegroundIsDefault() ? + fgIndex : + static_cast(FindNearestTableIndex(rgbForeground)); + + const COLORREF rgbBackground = LookupBackgroundColor(attributes); + bgIndex = attributes.BackgroundIsDefault() ? + bgIndex : + static_cast(FindNearestTableIndex(rgbBackground)); + } + + // TextAttribute::GetLegacyAttributes(BYTE, BYTE) will use the legacy value + // it has if the component is a legacy index, otherwise it will use the + // provided byte for each index. In this way, we can provide a value to + // use should it not already have one. + const WORD wCompleteAttr = attributes.GetLegacyAttributes(fgIndex, bgIndex); + return wCompleteAttr; +} + +//Routine Description: +// For a given RGB color Color, finds the nearest color from the array ColorTable, and returns the index of that match. +//Arguments: +// - Color - The RGB color to fine the nearest color to. +// - ColorTable - The array of colors to find a nearest color from. +// - cColorTable - The number of elements in ColorTable +// Return value: +// The index in ColorTable of the nearest match to Color. +WORD Settings::FindNearestTableIndex(const COLORREF Color) const +{ + return ::FindNearestTableIndex(Color, _ColorTable, ARRAYSIZE(_ColorTable)); +} + +COLORREF Settings::GetCursorColor() const noexcept +{ + return _CursorColor; +} + +CursorType Settings::GetCursorType() const noexcept +{ + return _CursorType; +} + +void Settings::SetCursorColor(const COLORREF CursorColor) noexcept +{ + _CursorColor = CursorColor; +} + +void Settings::SetCursorType(const CursorType cursorType) noexcept +{ + _CursorType = cursorType; +} + +bool Settings::GetInterceptCopyPaste() const noexcept +{ + return _fInterceptCopyPaste; +} + +void Settings::SetInterceptCopyPaste(const bool interceptCopyPaste) noexcept +{ + _fInterceptCopyPaste = interceptCopyPaste; +} + +COLORREF Settings::GetDefaultForegroundColor() const noexcept +{ + return _DefaultForeground; +} + +void Settings::SetDefaultForegroundColor(const COLORREF defaultForeground) noexcept +{ + _DefaultForeground = defaultForeground; +} + +COLORREF Settings::GetDefaultBackgroundColor() const noexcept +{ + return _DefaultBackground; +} + +void Settings::SetDefaultBackgroundColor(const COLORREF defaultBackground) noexcept +{ + _DefaultBackground = defaultBackground; +} + +TextAttribute Settings::GetDefaultAttributes() const noexcept +{ + auto attrs = TextAttribute{ _wFillAttribute }; + if (_DefaultForeground != INVALID_COLOR) + { + attrs.SetDefaultForeground(); + } + if (_DefaultBackground != INVALID_COLOR) + { + attrs.SetDefaultBackground(); + } + return attrs; +} + +bool Settings::IsTerminalScrolling() const noexcept +{ + return _TerminalScrolling; +} + +void Settings::SetTerminalScrolling(const bool terminalScrollingEnabled) noexcept +{ + _TerminalScrolling = terminalScrollingEnabled; +} + +// Routine Description: +// - Determines whether our primary renderer should be DirectX or GDI. +// - This is based on user preference and velocity hold back state. +// Return Value: +// - True means use DirectX renderer. False means use GDI renderer. +bool Settings::GetUseDx() const noexcept +{ + return _fUseDx; +} + +// Method Description: +// - Return the default foreground color of the console. If the settings are +// configured to have a default foreground color (separate from the color +// table), this will return that value. Otherwise it will return the value +// from the colortable corresponding to our default legacy attributes. +// Arguments: +// - +// Return Value: +// - the default foreground color of the console. +COLORREF Settings::CalculateDefaultForeground() const noexcept +{ + const auto fg = GetDefaultForegroundColor(); + return fg != INVALID_COLOR ? fg : ForegroundColor(GetFillAttribute(), GetColorTable(), GetColorTableSize()); +} + +// Method Description: +// - Return the default background color of the console. If the settings are +// configured to have a default background color (separate from the color +// table), this will return that value. Otherwise it will return the value +// from the colortable corresponding to our default legacy attributes. +// Arguments: +// - +// Return Value: +// - the default background color of the console. +COLORREF Settings::CalculateDefaultBackground() const noexcept +{ + const auto bg = GetDefaultBackgroundColor(); + return bg != INVALID_COLOR ? bg : BackgroundColor(GetFillAttribute(), GetColorTable(), GetColorTableSize()); +} + +// Method Description: +// - Get the foregroud color of a particular text attribute, using our color +// table, and our configured default attributes. +// Arguments: +// - attr: the TextAttribute to retrieve the foreground color of. +// Return Value: +// - The color value of the attribute's foreground TextColor. +COLORREF Settings::LookupForegroundColor(const TextAttribute& attr) const noexcept +{ + const auto tableView = std::basic_string_view(&GetColorTable()[0], GetColorTableSize()); + return attr.CalculateRgbForeground(tableView, CalculateDefaultForeground(), CalculateDefaultBackground()); +} + +// Method Description: +// - Get the background color of a particular text attribute, using our color +// table, and our configured default attributes. +// Arguments: +// - attr: the TextAttribute to retrieve the background color of. +// Return Value: +// - The color value of the attribute's background TextColor. +COLORREF Settings::LookupBackgroundColor(const TextAttribute& attr) const noexcept +{ + const auto tableView = std::basic_string_view(&GetColorTable()[0], GetColorTableSize()); + return attr.CalculateRgbBackground(tableView, CalculateDefaultForeground(), CalculateDefaultBackground()); +} + +bool Settings::GetCopyColor() const noexcept +{ + return _fCopyColor; +} diff --git a/src/host/settings.hpp b/src/host/settings.hpp new file mode 100644 index 000000000..d16f2ac1a --- /dev/null +++ b/src/host/settings.hpp @@ -0,0 +1,259 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- settings.hpp + +Abstract: +- This module is used for all configurable settings in the console + +Author(s): +- Michael Niksa (MiNiksa) 23-Jul-2014 +- Paul Campbell (PaulCam) 23-Jul-2014 + +Revision History: +- From components of consrv.h +- This is a reduced/de-duplicated version of settings that were stored in the registry, link files, and in the console information state. +--*/ +#pragma once + +#include "../buffer/out/TextAttribute.hpp" + +// To prevent invisible windows, set a lower threshold on window alpha channel. +#define MIN_WINDOW_OPACITY 0x4D // 0x4D is approximately 30% visible/opaque (70% transparent). Valid range is 0x00-0xff. + +#include "ConsoleArguments.hpp" +#include "../inc/conattrs.hpp" + +class Settings +{ +public: + Settings(); + + void ApplyDesktopSpecificDefaults(); + + void ApplyStartupInfo(const Settings* const pStartupSettings); + void ApplyCommandlineArguments(const ConsoleArguments& consoleArgs); + void InitFromStateInfo(_In_ PCONSOLE_STATE_INFO pStateInfo); + void Validate(); + + CONSOLE_STATE_INFO CreateConsoleStateInfo() const; + + DWORD GetVirtTermLevel() const; + void SetVirtTermLevel(const DWORD dwVirtTermLevel); + + bool IsAltF4CloseAllowed() const; + void SetAltF4CloseAllowed(const bool fAllowAltF4Close); + + bool IsReturnOnNewlineAutomatic() const; + void SetAutomaticReturnOnNewline(const bool fAutoReturnOnNewline); + + bool IsGridRenderingAllowedWorldwide() const; + void SetGridRenderingAllowedWorldwide(const bool fGridRenderingAllowed); + + bool GetFilterOnPaste() const; + void SetFilterOnPaste(const bool fFilterOnPaste); + + const WCHAR* const GetLaunchFaceName() const; + void SetLaunchFaceName(_In_ PCWSTR const LaunchFaceName, const size_t cchLength); + + UINT GetCodePage() const; + void SetCodePage(const UINT uCodePage); + + UINT GetScrollScale() const; + void SetScrollScale(const UINT uScrollScale); + + bool GetTrimLeadingZeros() const; + void SetTrimLeadingZeros(const bool fTrimLeadingZeros); + + bool GetEnableColorSelection() const; + void SetEnableColorSelection(const bool fEnableColorSelection); + + bool GetLineSelection() const; + void SetLineSelection(const bool bLineSelection); + + bool GetWrapText () const; + void SetWrapText (const bool bWrapText ); + + bool GetCtrlKeyShortcutsDisabled () const; + void SetCtrlKeyShortcutsDisabled (const bool fCtrlKeyShortcutsDisabled ); + + BYTE GetWindowAlpha() const; + void SetWindowAlpha(const BYTE bWindowAlpha); + + DWORD GetHotKey() const; + void SetHotKey(const DWORD dwHotKey); + + bool IsStartupTitleIsLinkNameSet() const; + + DWORD GetStartupFlags() const; + void SetStartupFlags(const DWORD dwStartupFlags); + void UnsetStartupFlag(const DWORD dwFlagToUnset); + + WORD GetFillAttribute() const; + void SetFillAttribute(const WORD wFillAttribute); + + WORD GetPopupFillAttribute() const; + void SetPopupFillAttribute(const WORD wPopupFillAttribute); + + WORD GetShowWindow() const; + void SetShowWindow(const WORD wShowWindow); + + WORD GetReserved() const; + void SetReserved(const WORD wReserved); + + COORD GetScreenBufferSize() const; + void SetScreenBufferSize(const COORD dwScreenBufferSize); + + COORD GetWindowSize() const; + void SetWindowSize(const COORD dwWindowSize); + + bool IsWindowSizePixelsValid() const; + COORD GetWindowSizePixels() const; + void SetWindowSizePixels(const COORD dwWindowSizePixels); + + COORD GetWindowOrigin() const; + void SetWindowOrigin(const COORD dwWindowOrigin); + + DWORD GetFont() const; + void SetFont(const DWORD dwFont); + + COORD GetFontSize() const; + void SetFontSize(const COORD dwFontSize); + + UINT GetFontFamily() const; + void SetFontFamily(const UINT uFontFamily); + + UINT GetFontWeight() const; + void SetFontWeight(const UINT uFontWeight); + + const WCHAR* const GetFaceName() const; + bool IsFaceNameSet() const; + void SetFaceName(_In_ PCWSTR const pcszFaceName, const size_t cchLength); + + UINT GetCursorSize() const; + void SetCursorSize(const UINT uCursorSize); + + bool GetFullScreen() const; + void SetFullScreen(const bool fFullScreen); + + bool GetQuickEdit() const; + void SetQuickEdit(const bool fQuickEdit); + + bool GetInsertMode() const; + void SetInsertMode(const bool fInsertMode); + + bool GetAutoPosition() const; + void SetAutoPosition(const bool fAutoPosition); + + UINT GetHistoryBufferSize() const; + void SetHistoryBufferSize(const UINT uHistoryBufferSize); + + UINT GetNumberOfHistoryBuffers() const; + void SetNumberOfHistoryBuffers(const UINT uNumberOfHistoryBuffers); + + bool GetHistoryNoDup() const; + void SetHistoryNoDup(const bool fHistoryNoDup); + + const COLORREF* const GetColorTable() const; + const size_t GetColorTableSize() const; + void SetColorTable(_In_reads_(cSize) const COLORREF* const pColorTable, const size_t cSize); + void SetColorTableEntry(const size_t index, const COLORREF ColorValue); + COLORREF GetColorTableEntry(const size_t index) const; + + COLORREF GetCursorColor() const noexcept; + CursorType GetCursorType() const noexcept; + + void SetCursorColor(const COLORREF CursorColor) noexcept; + void SetCursorType(const CursorType cursorType) noexcept; + + bool GetInterceptCopyPaste() const noexcept; + void SetInterceptCopyPaste(const bool interceptCopyPaste) noexcept; + + COLORREF GetDefaultForegroundColor() const noexcept; + void SetDefaultForegroundColor(const COLORREF defaultForeground) noexcept; + + COLORREF GetDefaultBackgroundColor() const noexcept; + void SetDefaultBackgroundColor(const COLORREF defaultBackground) noexcept; + + TextAttribute GetDefaultAttributes() const noexcept; + + bool IsTerminalScrolling() const noexcept; + void SetTerminalScrolling(const bool terminalScrollingEnabled) noexcept; + + bool GetUseDx() const noexcept; + bool GetCopyColor() const noexcept; + + COLORREF CalculateDefaultForeground() const noexcept; + COLORREF CalculateDefaultBackground() const noexcept; + COLORREF LookupForegroundColor(const TextAttribute& attr) const noexcept; + COLORREF LookupBackgroundColor(const TextAttribute& attr) const noexcept; + +private: + DWORD _dwHotKey; + DWORD _dwStartupFlags; + WORD _wFillAttribute; + WORD _wPopupFillAttribute; + WORD _wShowWindow; // used when window is created + WORD _wReserved; + // START - This section filled via memcpy from shortcut properties. Do not rearrange/change. + COORD _dwScreenBufferSize; + COORD _dwWindowSize; // this is in characters. + COORD _dwWindowOrigin; // used when window is created + DWORD _nFont; + COORD _dwFontSize; + UINT _uFontFamily; + UINT _uFontWeight; + WCHAR _FaceName[LF_FACESIZE]; + UINT _uCursorSize; + BOOL _bFullScreen; // deprecated + BOOL _bQuickEdit; + BOOL _bInsertMode; // used by command line editing + BOOL _bAutoPosition; + UINT _uHistoryBufferSize; + UINT _uNumberOfHistoryBuffers; + BOOL _bHistoryNoDup; + COLORREF _ColorTable[COLOR_TABLE_SIZE]; + // END - memcpy + UINT _uCodePage; + UINT _uScrollScale; + bool _fTrimLeadingZeros; + bool _fEnableColorSelection; + bool _bLineSelection; + bool _bWrapText; // whether to use text wrapping when resizing the window + bool _fCtrlKeyShortcutsDisabled; // disables Ctrl+ key intercepts + BYTE _bWindowAlpha; // describes the opacity of the window + + bool _fFilterOnPaste; // should we filter text when the user pastes? (e.g. remove ) + WCHAR _LaunchFaceName[LF_FACESIZE]; + bool _fAllowAltF4Close; + DWORD _dwVirtTermLevel; + bool _fAutoReturnOnNewline; + bool _fRenderGridWorldwide; + bool _fUseDx; + bool _fCopyColor; + + COLORREF _XtermColorTable[XTERM_COLOR_TABLE_SIZE]; + + // this is used for the special STARTF_USESIZE mode. + bool _fUseWindowSizePixels; + COORD _dwWindowSizePixels; + + // Technically a COLORREF, but using INVALID_COLOR as "Invert Colors" + unsigned int _CursorColor; + CursorType _CursorType; + + bool _fInterceptCopyPaste; + + COLORREF _DefaultForeground; + COLORREF _DefaultBackground; + bool _TerminalScrolling; + friend class RegistrySerialization; + +public: + + WORD GenerateLegacyAttributes(const TextAttribute attributes) const; + WORD FindNearestTableIndex(const COLORREF Color) const; + +}; diff --git a/src/host/sources.inc b/src/host/sources.inc new file mode 100644 index 000000000..c4465675f --- /dev/null +++ b/src/host/sources.inc @@ -0,0 +1,243 @@ +!include ..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Host +# ------------------------------------- + +# The console host is the application that services all requests from a Win32 +# console mode application. Since a console application has no visual representation +# on its own (it simply uses STDIN and STDOUT to process a text stream) +# the console host window provides a visual representation (output) and a means +# of capturing user-interaction (input) on behalf of the hosted application. + + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DFE_IME + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGET_DESTINATION = retail + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + ..\selection.cpp \ + ..\selectionInput.cpp \ + ..\selectionState.cpp \ + ..\scrolling.cpp \ + ..\cmdline.cpp \ + ..\CursorBlinker.cpp \ + ..\popup.cpp \ + ..\alias.cpp \ + ..\history.cpp \ + ..\VtIo.cpp \ + ..\VtInputThread.cpp \ + ..\PtySignalInputThread.cpp \ + ..\consoleInformation.cpp \ + ..\search.cpp \ + ..\directio.cpp \ + ..\getset.cpp \ + ..\globals.cpp \ + ..\handle.cpp \ + ..\init.cpp \ + ..\input.cpp \ + ..\inputBuffer.cpp \ + ..\inputKeyInfo.cpp \ + ..\inputReadHandleData.cpp \ + ..\misc.cpp \ + ..\output.cpp \ + ..\srvinit.cpp \ + ..\outputStream.cpp \ + ..\stream.cpp \ + ..\dbcs.cpp \ + ..\convarea.cpp \ + ..\screenInfo.cpp \ + ..\ScreenBufferRenderTarget.cpp \ + ..\_output.cpp \ + ..\_stream.cpp \ + ..\utils.cpp \ + ..\telemetry.cpp \ + ..\tracing.cpp \ + ..\registry.cpp \ + ..\settings.cpp \ + ..\ntprivapi.cpp \ + ..\readData.cpp \ + ..\readDataCooked.cpp \ + ..\readDataDirect.cpp \ + ..\readDataRaw.cpp \ + ..\writeData.cpp \ + ..\renderData.cpp \ + ..\renderFontDefaults.cpp \ + ..\utf8ToWideCharParser.cpp \ + ..\conareainfo.cpp \ + ..\conimeinfo.cpp \ + ..\conattrs.cpp \ + ..\ConsoleArguments.cpp \ + ..\CommandNumberPopup.cpp \ + ..\CommandListPopup.cpp \ + ..\CopyFromCharPopup.cpp \ + ..\CopyToCharPopup.cpp \ + + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h +PRECOMPILED_PCH = precomp.pch +PRECOMPILED_OBJ = precomp.obj + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\inc; \ + ..\..\buffer\out; \ + ..\..\propsheet; \ + ..\..\propslib; \ + ..\..\terminal\parser; \ + ..\..\terminal\adapter; \ + ..\..\types; \ + ..\..\renderer\inc; \ + ..\..\renderer\gdi; \ + ..\..\renderer\vt; \ + ..\..\renderer\base; \ + $(ONECOREBASE_PRIVATE_WIL_INC_PATH_L); \ + $(SHELL_INC_PATH); \ + $(INTERNAL_SDK_INC_PATH); \ + $(ONECORE_INTERNAL_SDK_INC_PATH); \ + +# Anything defined here will be defined BEFORE the CRT items the build system adds. +CRTLIBS = \ + $(CRTLIBS) \ + +# The LIB linking order is $(CRTLIBS) $(TARGETLIBS) + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_INTERNAL_SDK_LIB_PATH)\onecoreuuid.lib \ + $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + $(SDK_LIB_PATH)\d2d1.lib \ + $(SDK_LIB_PATH)\dwrite.lib \ + $(SDK_LIB_PATH)\dxgi.lib \ + $(SDK_LIB_PATH)\d3d11.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-mm-playsound-l1.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-dwmapi-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-edputil-policy-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-font-l1.lib \ + $(ONECOREWINDOWS_INTERNAL_LIB_PATH_L)\ext-ms-win-gdi-internal-desktop-l1-1-0.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-caret-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-dialogbox-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-gui-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-menu-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-message-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-misc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-mouse-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-rectangle-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-server-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-sysparams-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-window-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-object-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-rgn-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-cursor-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-dc-access-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-rawinput-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-sysparams-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-window-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-shell-shell32-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uxtheme-themes-l1.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uiacore-l1.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\host\lib\$(O)\conhostv2.lib \ + $(WINCORE_OBJ_PATH)\console\conint\$(O)\conint.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\buffer\out\lib\$(O)\conbufferout.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\tsf\$(O)\contsf.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\propslib\$(O)\conprops.lib \ + $(CONSOLE_OBJ_PATH)\terminal\input\lib\$(O)\ConTermInput.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\adapter\lib\$(O)\ConTermAdapter.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\parser\lib\$(O)\ConTermParser.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\base\lib\$(O)\ConRenderBase.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\dx\lib\$(O)\ConRenderDx.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\gdi\lib\$(O)\ConRenderGdi.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\vt\lib\$(O)\ConRenderVt.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\wddmcon\lib\$(O)\ConRenderWddmCon.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\server\lib\$(O)\ConServer.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\base\lib\$(O)\ConInteractivityBaseLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\win32\lib\$(O)\ConInteractivityWin32Lib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\onecore\lib\$(O)\ConInteractivityOneCoreLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\types\lib\$(O)\ConTypes.lib \ + +DELAYLOAD = \ + PROPSYS.dll; \ + D2D1.dll; \ + DWrite.dll; \ + DXGI.dll; \ + D3D11.dll; \ + OLEAUT32.dll; \ + api-ms-win-mm-playsound-l1.dll; \ + api-ms-win-shcore-scaling-l1.dll; \ + api-ms-win-shell-namespace-l1.dll; \ + ext-ms-win-dwmapi-ext-l1.dll; \ + ext-ms-win-edputil-policy-l1.dll; \ + ext-ms-win-gdi-dc-l1.dll; \ + ext-ms-win-gdi-dc-create-l1.dll; \ + ext-ms-win-gdi-draw-l1.dll; \ + ext-ms-win-gdi-font-l1.dll; \ + ext-ms-win-gdi-internal-desktop-l1.dll; \ + ext-ms-win-ntuser-caret-l1.dll; \ + ext-ms-win-ntuser-dialogbox-l1.dll; \ + ext-ms-win-ntuser-draw-l1.dll; \ + ext-ms-win-ntuser-keyboard-l1.dll; \ + ext-ms-win-ntuser-gui-l1.dll; \ + ext-ms-win-ntuser-menu-l1.dll; \ + ext-ms-win-ntuser-message-l1.dll; \ + ext-ms-win-ntuser-misc-l1.dll; \ + ext-ms-win-ntuser-mouse-l1.dll; \ + ext-ms-win-ntuser-rectangle-ext-l1.dll; \ + ext-ms-win-ntuser-server-l1.dll; \ + ext-ms-win-ntuser-sysparams-ext-l1.dll; \ + ext-ms-win-ntuser-window-l1.dll; \ + ext-ms-win-rtcore-gdi-object-l1.dll; \ + ext-ms-win-rtcore-gdi-rgn-l1.dll; \ + ext-ms-win-rtcore-ntuser-cursor-l1.dll; \ + ext-ms-win-rtcore-ntuser-dc-access-l1.dll; \ + ext-ms-win-rtcore-ntuser-rawinput-l1.dll; \ + ext-ms-win-rtcore-ntuser-sysparams-l1.dll; \ + ext-ms-win-rtcore-ntuser-window-ext-l1.dll; \ + ext-ms-win-shell-shell32-l1.dll; \ + ext-ms-win-uiacore-l1.dll; \ + ext-ms-win-uxtheme-themes-l1.dll; \ + +DLOAD_ERROR_HANDLER = kernelbase diff --git a/src/host/sources.test.inc b/src/host/sources.test.inc new file mode 100644 index 000000000..7d49665c3 --- /dev/null +++ b/src/host/sources.test.inc @@ -0,0 +1,35 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGET_DESTINATION = UnitTests + +UNIVERSAL_TEST = 1 +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +INCLUDES = \ + $(INCLUDES); \ + ..\..\inc\test; \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + +# prepend the ConRenderVt.Unittest.lib, so that it's linked before the non-ut version. + +TARGETLIBS = \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\vt\ut_lib\$(O)\ConRenderVt.Unittest.lib \ + $(TARGETLIBS) \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ + diff --git a/src/host/srvinit.cpp b/src/host/srvinit.cpp new file mode 100644 index 000000000..92591e408 --- /dev/null +++ b/src/host/srvinit.cpp @@ -0,0 +1,678 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "srvinit.h" + +#include "dbcs.h" +#include "handle.h" +#include "registry.hpp" +#include "renderFontDefaults.hpp" + +#include "ApiRoutines.h" + +#include "../types/inc/GlyphWidth.hpp" + +#include "..\server\Entrypoints.h" +#include "..\server\IoSorter.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "..\interactivity\base\ApiDetector.hpp" + +#include "renderData.hpp" +#include "../renderer/base/renderer.hpp" + +#pragma hdrstop + +const UINT CONSOLE_EVENT_FAILURE_ID = 21790; +const UINT CONSOLE_LPC_PORT_FAILURE_ID = 21791; + +[[nodiscard]] +HRESULT ConsoleServerInitialization(_In_ HANDLE Server, const ConsoleArguments* const args) +{ + Globals& Globals = ServiceLocator::LocateGlobals(); + + try + { + Globals.pDeviceComm = new DeviceComm(Server); + + Globals.launchArgs = *args; + + Globals.uiOEMCP = GetOEMCP(); + Globals.uiWindowsCP = GetACP(); + + Globals.pFontDefaultList = new RenderFontDefaults(); + + FontInfo::s_SetFontDefaultList(Globals.pFontDefaultList); + } + CATCH_RETURN(); + + // Removed allocation of scroll buffer here. + return S_OK; +} + +static bool s_IsOnDesktop() +{ + // Persist this across calls so we don't dig it out a whole bunch of times. Once is good enough for the system. + static bool fAlreadyQueried = false; + static bool fIsDesktop = false; + + if (!fAlreadyQueried) + { + Microsoft::Console::Interactivity::ApiLevel level; + const NTSTATUS status = Microsoft::Console::Interactivity::ApiDetector::DetectNtUserWindow(&level); + LOG_IF_NTSTATUS_FAILED(status); + + if (NT_SUCCESS(status)) + { + switch (level) + { + case Microsoft::Console::Interactivity::ApiLevel::OneCore: + fIsDesktop = false; + break; + case Microsoft::Console::Interactivity::ApiLevel::Win32: + fIsDesktop = true; + break; + } + } + + fAlreadyQueried = true; + } + + return fIsDesktop; +} + +[[nodiscard]] +NTSTATUS SetUpConsole(_Inout_ Settings* pStartupSettings, + _In_ DWORD TitleLength, + _In_reads_bytes_(TitleLength) LPWSTR Title, + _In_ LPCWSTR CurDir, + _In_ LPCWSTR AppName) +{ + // We will find and locate all relevant preference settings and then create the console here. + // The precedence order for settings is: + // 0. Launch arguments passed on the commandline. + // 1. STARTUPINFO settings + // 2a. Shortcut/Link settings + // 2b. Registry specific settings + // 3. Registry default settings + // 4. Hardcoded default settings + // To establish this hierarchy, we will need to load the settings and apply them in reverse order. + + // 4. Initializing Settings will establish hardcoded defaults. + // Set to reference of global console information since that's the only place we need to hold the settings. + CONSOLE_INFORMATION& settings = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& launchArgs = ServiceLocator::LocateGlobals().launchArgs; + // 4b. On Desktop editions, we need to apply a series of Desktop-specific defaults that are better than the + // ones from the constructor (which are great for OneCore systems.) + if (s_IsOnDesktop()) + { + settings.ApplyDesktopSpecificDefaults(); + } + + // Use the launch arguments to check if we're going to be started in pseudoconsole mode. + // If we are, we don't want to load any user settings, because that could + // result in some strange rendering results in the end terminal. + // Use the launch args because the VtIo hasn't been initialized yet. + if (!launchArgs.InConptyMode()) + { + // 3. Read the default registry values. + Registry reg(&settings); + reg.LoadGlobalsFromRegistry(); + reg.LoadDefaultFromRegistry(); + + // 2. Read specific settings + + // Link is expecting the flags from the process to be in already, so apply that first + settings.SetStartupFlags(pStartupSettings->GetStartupFlags()); + + // We need to see if we were spawned from a link. If we were, we need to + // call back into the shell to try to get all the console information from the link. + ServiceLocator::LocateSystemConfigurationProvider()->GetSettingsFromLink(&settings, Title, &TitleLength, CurDir, AppName); + + // If we weren't started from a link, this will already be set. + // If LoadLinkInfo couldn't find anything, it will remove the flag so we can dig in the registry. + if (!(settings.IsStartupTitleIsLinkNameSet())) + { + reg.LoadFromRegistry(Title); + } + } + + + // 1. The settings we were passed contains STARTUPINFO structure settings to be applied last. + settings.ApplyStartupInfo(pStartupSettings); + + // 0. The settings passed in via commandline arguments. These should override anything else. + settings.ApplyCommandlineArguments(launchArgs); + + // Validate all applied settings for correctness against final rules. + settings.Validate(); + + // As of the graphics refactoring to library based, all fonts are now DPI aware. Scaling is performed at the Blt time for raster fonts. + // Note that we can only declare our DPI awareness once per process launch. + // Set the process's default dpi awareness context to PMv2 so that new top level windows + // inherit their WM_DPICHANGED* broadcast mode (and more, like dialog scaling) from the thread. + + IHighDpiApi *pHighDpiApi = ServiceLocator::LocateHighDpiApi(); + if (pHighDpiApi) + { + // N.B.: There is no high DPI support on OneCore (non-UAP) systems. + // Instead of implementing a no-op interface, just skip all high + // DPI configuration if it is not supported. All callers into the + // high DPI API are in the Win32-specific interactivity DLL. + if (!pHighDpiApi->SetProcessDpiAwarenessContext()) + { + // Fallback to per-monitor aware V1 if the API isn't available. + LOG_IF_FAILED(pHighDpiApi->SetProcessPerMonitorDpiAwareness()); + + // Allow child dialogs (i.e. Properties and Find) to scale automatically based on DPI if we're currently DPI aware. + // Note that we don't need to do this if we're PMv2. + pHighDpiApi->EnablePerMonitorDialogScaling(); + } + } + + //Save initial font name for comparison on exit. We want telemetry when the font has changed + if (settings.IsFaceNameSet()) + { + settings.SetLaunchFaceName(settings.GetFaceName(), LF_FACESIZE); + } + +// Allocate console will read the global ServiceLocator::LocateGlobals().getConsoleInformation for the settings we just set. + NTSTATUS Status = CONSOLE_INFORMATION::AllocateConsole({ Title, TitleLength / sizeof(wchar_t)}); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS RemoveConsole(_In_ ConsoleProcessHandle* ProcessData) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + NTSTATUS Status = STATUS_SUCCESS; + + CommandHistory::s_Free((HANDLE)ProcessData); + + bool const fRecomputeOwner = ProcessData->fRootProcess; + gci.ProcessHandleList.FreeProcessData(ProcessData); + + if (fRecomputeOwner) + { + IConsoleWindow* pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + pWindow->SetOwner(); + } + } + + UnlockConsole(); + + return Status; +} + +DWORD ConsoleIoThread(); + +void ConsoleCheckDebug() +{ +#ifdef DBG + wil::unique_hkey hCurrentUser; + wil::unique_hkey hConsole; + NTSTATUS status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUser, &hConsole); + + if (NT_SUCCESS(status)) + { + DWORD dwData = 0; + status = RegistrySerialization::s_QueryValue(hConsole.get(), + L"DebugLaunch", + sizeof(dwData), + REG_DWORD, + (BYTE*)&dwData, + nullptr); + + if (NT_SUCCESS(status)) + { + if (dwData != 0) + { + DebugBreak(); + } + } + } +#endif +} + +[[nodiscard]] +HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args) +{ + auto& g = ServiceLocator::LocateGlobals(); + RETURN_IF_FAILED(ConsoleServerInitialization(Server, args)); + RETURN_IF_FAILED(g.hConsoleInputInitEvent.create(wil::EventOptions::None)); + + // Set up and tell the driver about the input available event. + RETURN_IF_FAILED(g.hInputEvent.create(wil::EventOptions::ManualReset)); + + CD_IO_SERVER_INFORMATION ServerInformation; + ServerInformation.InputAvailableEvent = ServiceLocator::LocateGlobals().hInputEvent.get(); + RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation)); + + HANDLE const hThread = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)ConsoleIoThread, 0, 0, nullptr); + RETURN_HR_IF(E_HANDLE, hThread == nullptr); + LOG_IF_WIN32_BOOL_FALSE(CloseHandle(hThread)); // The thread will run on its own and close itself. Free the associated handle. + + // See MSFT:19918626 + // Make sure to always set up the signal thread if we need to. + // Do this first, because breaking the signal pipe is used by the conpty API + // to indicate that we should close. + // The conpty i/o threads need an actual client to be connected before they + // can start, so they're started below, in ConsoleAllocateConsole + auto& gci = g.getConsoleInformation(); + RETURN_IF_FAILED(gci.GetVtIo()->Initialize(args)); + RETURN_IF_FAILED(gci.GetVtIo()->CreateAndStartSignalThread()); + + return S_OK; +} + +#define SYSTEM_ROOT (L"%SystemRoot%") +#define SYSTEM_ROOT_LENGTH (sizeof(SYSTEM_ROOT) - sizeof(WCHAR)) + +// Routine Description: +// - This routine translates path characters into '_' characters because the NT registry apis do not allow the creation of keys with +// names that contain path characters. It also converts absolute paths into %SystemRoot% relative ones. As an example, if both behaviors were +// specified it would convert a title like C:\WINNT\System32\cmd.exe to %SystemRoot%_System32_cmd.exe. +// Arguments: +// - ConsoleTitle - Pointer to string to translate. +// - Unexpand - Convert absolute path to %SystemRoot% relative one. +// - Substitute - Whether string-substitution ('_' for '\') should occur. +// Return Value: +// - Pointer to translated title or nullptr. +// Note: +// - This routine allocates a buffer that must be freed. +PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, const BOOL fSubstitute) +{ + LPWSTR Tmp = nullptr; + + size_t cbConsoleTitle; + size_t cbSystemRoot; + + LPWSTR pwszSysRoot = new(std::nothrow) wchar_t[MAX_PATH]; + if (nullptr != pwszSysRoot) + { + if (0 != GetWindowsDirectoryW(pwszSysRoot, MAX_PATH)) + { + if (SUCCEEDED(StringCbLengthW(pwszConsoleTitle, STRSAFE_MAX_CCH, &cbConsoleTitle)) && + SUCCEEDED(StringCbLengthW(pwszSysRoot, MAX_PATH, &cbSystemRoot))) + { + int const cchSystemRoot = (int)(cbSystemRoot / sizeof(WCHAR)); + int const cchConsoleTitle = (int)(cbConsoleTitle / sizeof(WCHAR)); + cbConsoleTitle += sizeof(WCHAR); // account for nullptr terminator + + if (fUnexpand && + cchConsoleTitle >= cchSystemRoot && +#pragma prefast(suppress:26018, "We've guaranteed that cchSystemRoot is equal to or smaller than cchConsoleTitle in size.") + (CSTR_EQUAL == CompareStringOrdinal(pwszConsoleTitle, cchSystemRoot, pwszSysRoot, cchSystemRoot, TRUE))) + { + cbConsoleTitle -= cbSystemRoot; + pwszConsoleTitle += cchSystemRoot; + cbSystemRoot = SYSTEM_ROOT_LENGTH; + } + else + { + cbSystemRoot = 0; + } + + LPWSTR pszTranslatedConsoleTitle; + const size_t cbTranslatedConsoleTitle = cbSystemRoot + cbConsoleTitle; + Tmp = pszTranslatedConsoleTitle = (PWSTR)new BYTE[cbTranslatedConsoleTitle]; + if (pszTranslatedConsoleTitle == nullptr) + { + return nullptr; + } + + // No need to check return here -- pszTranslatedConsoleTitle is guaranteed large enough for SYSTEM_ROOT + (void)StringCbCopy(pszTranslatedConsoleTitle, cbTranslatedConsoleTitle, SYSTEM_ROOT); + pszTranslatedConsoleTitle += (cbSystemRoot / sizeof(WCHAR)); // skip by characters -- not bytes + + for (UINT i = 0; i < cbConsoleTitle; i += sizeof(WCHAR)) + { +#pragma prefast(suppress:26018, "We are reading the null portion of the buffer on purpose and will escape on reaching it below.") + if (fSubstitute && *pwszConsoleTitle == '\\') + { +#pragma prefast(suppress:26019, "Console title must contain system root if this path was followed.") + *pszTranslatedConsoleTitle++ = (WCHAR)'_'; + } + else + { + *pszTranslatedConsoleTitle++ = *pwszConsoleTitle; + if (*pwszConsoleTitle == L'\0') + { + break; + } + } + + pwszConsoleTitle++; + } + } + } + delete[] pwszSysRoot; + } + + return Tmp; +} + +[[nodiscard]] +NTSTATUS GetConsoleLangId(const UINT uiOutputCP, _Out_ LANGID * const pLangId) +{ + NTSTATUS Status = STATUS_NOT_SUPPORTED; + + // -- WARNING -- LOAD BEARING CODE -- + // Only attempt to return the Lang ID if the Windows ACP on console launch was an East Asian Code Page. + // - + // As of right now, this is a load bearing check and causes a domino effect of errors during OEM preinstallation if removed + // resulting in a crash on launch of CMD.exe + // (and consequently any scripts OEMs use to customize an image during the auditUser preinstall step inside their unattend.xml files.) + // I have no reason to believe that removing this check causes any problems on any other SKU or scenario types. + // - + // Returning STATUS_NOT_SUPPORTED will skip a call to SetThreadLocale inside the Windows loader. This has the effect of not + // setting the appropriate locale on the client end of the pipe, but also avoids the error. + // Returning STATUS_SUCCESS will trigger the call to SetThreadLocale inside the loader. + // This method is called on process launch by the loader and on every SetConsoleOutputCP call made from the client application to + // maintain the synchrony of the client's Thread Locale state. + // - + // It is important to note that a comment exists inside the loader stating that DBCS code pages (CJK languages) + // must have the SetThreadLocale synchronized with the console in order for FormatMessage to output correctly. + // I'm not sure of the full validity of that comment at this point in time (Nov 2016), but the least risky thing is to trust it and revert + // the behavior to this function until it can be otherwise proven. + // - + // See MSFT: 9808579 for the complete story on what happened here and why this must stay until the other dominos are resolved. + // - + // I would also highly advise against expanding the LANGIDs returned here or modifying them in any way until the cascading impacts + // discovered in MSFT: 9808579 are vetted against any changes. + // -- END WARNING -- + if (IsAvailableEastAsianCodePage(ServiceLocator::LocateGlobals().uiWindowsCP)) + { + if (pLangId != nullptr) + { + switch (uiOutputCP) + { + case CP_JAPANESE: + *pLangId = MAKELANGID(LANG_JAPANESE, SUBLANG_DEFAULT); + break; + case CP_KOREAN: + *pLangId = MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN); + break; + case CP_CHINESE_SIMPLIFIED: + *pLangId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); + break; + case CP_CHINESE_TRADITIONAL: + *pLangId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); + break; + default: + *pLangId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + break; + } + } + Status = STATUS_SUCCESS; + } + + return Status; +} + +[[nodiscard]] +HRESULT ApiRoutines::GetConsoleLangIdImpl(LANGID& langId) noexcept +{ + try + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // This fails a lot and it's totally expected. It only works for a few East Asian code pages. + // As such, just return it. Do NOT use a wil macro here. It is very noisy. + return HRESULT_FROM_NT(GetConsoleLangId(gci.OutputCP, &langId)); + } + CATCH_RETURN(); +} + +// Routine Description: +// - This routine reads the connection information from a 'connect' IO, validates it and stores them in an internal format. +// - N.B. The internal informat contains information not sent by clients in their connect IOs and intialized by other routines. +// Arguments: +// - Server - Supplies a handle to the console server. +// - Message - Supplies the message representing the connect IO. +// - Cac - Receives the connection information. +// Return Value: +// - NTSTATUS indicating if the connection information was successfully initialized. +[[nodiscard]] +NTSTATUS ConsoleInitializeConnectInfo(_In_ PCONSOLE_API_MSG Message, _Out_ PCONSOLE_API_CONNECTINFO Cac) +{ + CONSOLE_SERVER_MSG Data = { 0 }; + // Try to receive the data sent by the client. + NTSTATUS Status = NTSTATUS_FROM_HRESULT(Message->ReadMessageInput(0, &Data, sizeof(Data))); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + // Validate that strings are within the buffers and null-terminated. + if ((Data.ApplicationNameLength > (sizeof(Data.ApplicationName) - sizeof(WCHAR))) || + (Data.TitleLength > (sizeof(Data.Title) - sizeof(WCHAR))) || + (Data.CurrentDirectoryLength > (sizeof(Data.CurrentDirectory) - sizeof(WCHAR))) || + (Data.ApplicationName[Data.ApplicationNameLength / sizeof(WCHAR)] != UNICODE_NULL) || + (Data.Title[Data.TitleLength / sizeof(WCHAR)] != UNICODE_NULL) || (Data.CurrentDirectory[Data.CurrentDirectoryLength / sizeof(WCHAR)] != UNICODE_NULL)) + { + return STATUS_INVALID_BUFFER_SIZE; + } + + // Initialize (partially) the connect info with the received data. + FAIL_FAST_IF(!(sizeof(Cac->AppName) == sizeof(Data.ApplicationName))); + FAIL_FAST_IF(!(sizeof(Cac->Title) == sizeof(Data.Title))); + FAIL_FAST_IF(!(sizeof(Cac->CurDir) == sizeof(Data.CurrentDirectory))); + + // unused(Data.IconId) + Cac->ConsoleInfo.SetHotKey(Data.HotKey); + Cac->ConsoleInfo.SetStartupFlags(Data.StartupFlags); + Cac->ConsoleInfo.SetFillAttribute(Data.FillAttribute); + Cac->ConsoleInfo.SetShowWindow(Data.ShowWindow); + Cac->ConsoleInfo.SetScreenBufferSize(Data.ScreenBufferSize); + Cac->ConsoleInfo.SetWindowSize(Data.WindowSize); + Cac->ConsoleInfo.SetWindowOrigin(Data.WindowOrigin); + Cac->ProcessGroupId = Data.ProcessGroupId; + Cac->ConsoleApp = Data.ConsoleApp; + Cac->WindowVisible = Data.WindowVisible; + Cac->TitleLength = Data.TitleLength; + Cac->AppNameLength = Data.ApplicationNameLength; + Cac->CurDirLength = Data.CurrentDirectoryLength; + + memmove(Cac->AppName, Data.ApplicationName, sizeof(Cac->AppName)); + memmove(Cac->Title, Data.Title, sizeof(Cac->Title)); + memmove(Cac->CurDir, Data.CurrentDirectory, sizeof(Cac->CurDir)); + + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS ConsoleAllocateConsole(PCONSOLE_API_CONNECTINFO p) +{ + // AllocConsole is outside our codebase, but we should be able to mostly track the call here. + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::AllocConsole); + + Globals& g = ServiceLocator::LocateGlobals(); + + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + + NTSTATUS Status = SetUpConsole(&p->ConsoleInfo, p->TitleLength, p->Title, p->CurDir, p->AppName); + if (!NT_SUCCESS(Status)) + { + return Status; + } + + // No matter what, create a renderer. + try + { + g.pRender = nullptr; + + auto renderThread = std::make_unique(); + // stash a local pointer to the thread here - + // We're going to give ownership of the thread to the Renderer, + // but the thread also need to be told who it's renderer is, + // and we can't do that until the renderer is constructed. + auto* const localPointerToThread = renderThread.get(); + + g.pRender = new Renderer(&gci.renderData, nullptr, 0, std::move(renderThread)); + + THROW_IF_FAILED(localPointerToThread->Initialize(g.pRender)); + + // Allow the renderer to paint. + g.pRender->EnablePainting(); + + // Set up the renderer to be used to calculate the width of a glyph, + // should we be unable to figure out it's width another way. + auto pfn = std::bind(&Renderer::IsGlyphWideByFont, static_cast(g.pRender), std::placeholders::_1); + SetGlyphWidthFallback(pfn); + + } + catch (...) + { + Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + + if (NT_SUCCESS(Status) && p->WindowVisible) + { + HANDLE Thread = nullptr; + + IConsoleInputThread *pNewThread = nullptr; + LOG_IF_FAILED(ServiceLocator::CreateConsoleInputThread(&pNewThread)); + + FAIL_FAST_IF_NULL(pNewThread); + + Thread = pNewThread->Start(); + if (Thread == nullptr) + { + Status = STATUS_NO_MEMORY; + } + else + { + ServiceLocator::LocateGlobals().dwInputThreadId = pNewThread->GetThreadId(); + + // The ConsoleInputThread needs to lock the console so we must first unlock it ourselves. + UnlockConsole(); + g.hConsoleInputInitEvent.wait(); + LockConsole(); + + CloseHandle(Thread); + g.hConsoleInputInitEvent.release(); + + if (!NT_SUCCESS(g.ntstatusConsoleInputInitStatus)) + { + Status = g.ntstatusConsoleInputInitStatus; + } + else + { + Status = STATUS_SUCCESS; + } + + + // If we're not headless, we'll make a real window. + // Allow UI Access to the real window but not the little + // fake window we would make in headless mode. + if (!g.launchArgs.IsHeadless()) + { + /* + * Tell driver to allow clients with UIAccess to connect + * to this server even if the security descriptor doesn't + * allow it. + * + * N.B. This allows applications like narrator.exe to have + * access to the console. This is ok because they already + * have access to the console window anyway - this function + * is only called when a window is created. + */ + + LOG_IF_FAILED(g.pDeviceComm->AllowUIAccess()); + } + } + } + + // Potentially start the VT IO (if needed) + // Make sure to do this after the i/o buffers have been created. + // We'll need the size of the screen buffer in the vt i/o initialization + if (NT_SUCCESS(Status)) + { + HRESULT hr = gci.GetVtIo()->CreateIoHandlers(); + if (hr == S_FALSE) + { + // We're not in VT I/O mode, this is fine. + } + else if (SUCCEEDED(hr)) + { + // Actually start the VT I/O threads + hr = gci.GetVtIo()->StartIfNeeded(); + // Don't convert S_FALSE to an NTSTATUS - the equivalent NTSTATUS + // is treated as an error + if (hr != S_FALSE) + { + Status = NTSTATUS_FROM_HRESULT(hr); + } + else + { + Status = ERROR_SUCCESS; + } + } + else + { + Status = NTSTATUS_FROM_HRESULT(hr); + } + } + + return Status; +} + +// Routine Description: +// - This routine is the main one in the console server IO thread. +// - It reads IO requests submitted by clients through the driver, services and completes them in a loop. +// Arguments: +// - +// Return Value: +// - This routine never returns. The process exits when no more references or clients exist. +DWORD ConsoleIoThread() +{ + auto& globals = ServiceLocator::LocateGlobals(); + + CONSOLE_API_MSG ReceiveMsg; + ReceiveMsg._pApiRoutines = &globals.api; + ReceiveMsg._pDeviceComm = globals.pDeviceComm; + PCONSOLE_API_MSG ReplyMsg = nullptr; + + bool fShouldExit = false; + while (!fShouldExit) + { + if (ReplyMsg != nullptr) + { + LOG_IF_FAILED(ReplyMsg->ReleaseMessageBuffers()); + } + + // TODO: 9115192 correct mixed NTSTATUS/HRESULT + HRESULT hr = ServiceLocator::LocateGlobals().pDeviceComm->ReadIo(&ReplyMsg->Complete, &ReceiveMsg); + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_PIPE_NOT_CONNECTED)) + { + fShouldExit = true; + + // This will not return. Terminate immediately when disconnected. + ServiceLocator::RundownAndExit(STATUS_SUCCESS); + } + RIPMSG1(RIP_WARNING, "DeviceIoControl failed with Result 0x%x", hr); + ReplyMsg = nullptr; + continue; + } + + IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg); + } + + return 0; +} diff --git a/src/host/srvinit.h b/src/host/srvinit.h new file mode 100644 index 000000000..dd3902518 --- /dev/null +++ b/src/host/srvinit.h @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- srvinit.h + +Abstract: +- This is the main initialization file for the console Server. + +Author: +- Therese Stowell (ThereseS) 11-Nov-1990 + +Revision History: +--*/ + +#pragma once + +#include "conserv.h" + +[[nodiscard]] +NTSTATUS GetConsoleLangId(const UINT uiOutputCP, _Out_ LANGID * const pLangId); + +PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, const BOOL fSubstitute); + +[[nodiscard]] +NTSTATUS ConsoleInitializeConnectInfo(_In_ PCONSOLE_API_MSG Message, _Out_ PCONSOLE_API_CONNECTINFO Cac); +[[nodiscard]] +NTSTATUS ConsoleAllocateConsole(PCONSOLE_API_CONNECTINFO p); +[[nodiscard]] +NTSTATUS RemoveConsole(_In_ ConsoleProcessHandle* ProcessData); + +void ConsoleCheckDebug(); diff --git a/src/host/stream.cpp b/src/host/stream.cpp new file mode 100644 index 000000000..83385376c --- /dev/null +++ b/src/host/stream.cpp @@ -0,0 +1,819 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "_stream.h" +#include "stream.h" + +#include "dbcs.h" +#include "handle.h" +#include "misc.h" +#include "readDataRaw.hpp" + +#include "ApiRoutines.h" + +#include "../types/inc/GlyphWidth.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// Routine Description: +// - This routine is used in stream input. It gets input and filters it for unicode characters. +// Arguments: +// - pInputBuffer - The InputBuffer to read from +// - pwchOut - On a successful read, the char data read +// - Wait - true if a waited read should be performed +// - pCommandLineEditingKeys - if present, arrow keys will be +// returned. on output, if true, pwchOut contains virtual key code for +// arrow key. +// - pPopupKeys - if present, arrow keys will be +// returned. on output, if true, pwchOut contains virtual key code for +// arrow key. +// Return Value: +// - STATUS_SUCCESS on success or a relevant error code on failure. +[[nodiscard]] +NTSTATUS GetChar(_Inout_ InputBuffer* const pInputBuffer, + _Out_ wchar_t* const pwchOut, + const bool Wait, + _Out_opt_ bool* const pCommandLineEditingKeys, + _Out_opt_ bool* const pPopupKeys, + _Out_opt_ DWORD* const pdwKeyState) noexcept +{ + if (nullptr != pCommandLineEditingKeys) + { + *pCommandLineEditingKeys = false; + } + + if (nullptr != pPopupKeys) + { + *pPopupKeys = false; + } + + if (nullptr != pdwKeyState) + { + *pdwKeyState = 0; + } + + NTSTATUS Status; + for (;;) + { + std::unique_ptr inputEvent; + Status = pInputBuffer->Read(inputEvent, + false, // peek + Wait, + true, // unicode + true); // stream + + if (!NT_SUCCESS(Status)) + { + return Status; + } + else if (inputEvent.get() == nullptr) + { + FAIL_FAST_IF(Wait); + return STATUS_UNSUCCESSFUL; + } + + if (inputEvent->EventType() == InputEventType::KeyEvent) + { + std::unique_ptr keyEvent = std::unique_ptr(static_cast(inputEvent.release())); + + bool commandLineEditKey = false; + if (pCommandLineEditingKeys) + { + commandLineEditKey = keyEvent->IsCommandLineEditingKey(); + } + else if (pPopupKeys) + { + commandLineEditKey = keyEvent->IsPopupKey(); + } + + if (pdwKeyState) + { + *pdwKeyState = keyEvent->GetActiveModifierKeys(); + } + + if (keyEvent->GetCharData() != 0 && !commandLineEditKey) + { + // chars that are generated using alt + numpad + if (!keyEvent->IsKeyDown() && keyEvent->GetVirtualKeyCode() == VK_MENU) + { + if (keyEvent->IsAltNumpadSet()) + { + if (HIBYTE(keyEvent->GetCharData())) + { + char chT[2] = { + static_cast(HIBYTE(keyEvent->GetCharData())), + static_cast(LOBYTE(keyEvent->GetCharData())), + }; + *pwchOut = CharToWchar(chT, 2); + } + else + { + // Because USER doesn't know our codepage, + // it gives us the raw OEM char and we + // convert it to a Unicode character. + char chT = LOBYTE(keyEvent->GetCharData()); + *pwchOut = CharToWchar(&chT, 1); + } + } + else + { + *pwchOut = keyEvent->GetCharData(); + } + return STATUS_SUCCESS; + } + // Ignore Escape and Newline chars + else if (keyEvent->IsKeyDown() && + (WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_VIRTUAL_TERMINAL_INPUT) || + (keyEvent->GetVirtualKeyCode() != VK_ESCAPE && + keyEvent->GetCharData() != UNICODE_LINEFEED))) + { + *pwchOut = keyEvent->GetCharData(); + return STATUS_SUCCESS; + } + } + + if (keyEvent->IsKeyDown()) + { + if (pCommandLineEditingKeys && commandLineEditKey) + { + *pCommandLineEditingKeys = true; + *pwchOut = static_cast(keyEvent->GetVirtualKeyCode()); + return STATUS_SUCCESS; + } + else if (pPopupKeys && commandLineEditKey) + { + *pPopupKeys = true; + *pwchOut = static_cast(keyEvent->GetVirtualKeyCode()); + return STATUS_SUCCESS; + } + else + { + const short zeroVkeyData = ServiceLocator::LocateInputServices()->VkKeyScanW(0); + const byte zeroVKey = LOBYTE(zeroVkeyData); + const byte zeroControlKeyState = HIBYTE(zeroVkeyData); + + try + { + // Convert real Windows NT modifier bit into bizarre Console bits + std::unordered_set consoleModKeyState = FromVkKeyScan(zeroControlKeyState); + + if (zeroVKey == keyEvent->GetVirtualKeyCode() && + keyEvent->DoActiveModifierKeysMatch(consoleModKeyState)) + { + // This really is the character 0x0000 + *pwchOut = keyEvent->GetCharData(); + return STATUS_SUCCESS; + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } + } + } + } +} + +// Routine Description: +// - This routine returns the total number of screen spaces the characters up to the specified character take up. +size_t RetrieveTotalNumberOfSpaces(const SHORT sOriginalCursorPositionX, + _In_reads_(ulCurrentPosition) const WCHAR * const pwchBuffer, + _In_ size_t ulCurrentPosition) +{ + SHORT XPosition = sOriginalCursorPositionX; + size_t NumSpaces = 0; + + for (size_t i = 0; i < ulCurrentPosition; i++) + { + WCHAR const Char = pwchBuffer[i]; + + size_t NumSpacesForChar; + if (Char == UNICODE_TAB) + { + NumSpacesForChar = NUMBER_OF_SPACES_IN_TAB(XPosition); + } + else if (IS_CONTROL_CHAR(Char)) + { + NumSpacesForChar = 2; + } + else if (IsGlyphFullWidth(Char)) + { + NumSpacesForChar = 2; + } + else + { + NumSpacesForChar = 1; + } + XPosition = (SHORT)(XPosition + NumSpacesForChar); + NumSpaces += NumSpacesForChar; + } + + return NumSpaces; +} + +// Routine Description: +// - This routine returns the number of screen spaces the specified character takes up. +size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX, + _In_reads_(ulCurrentPosition + 1) const WCHAR * const pwchBuffer, + _In_ size_t ulCurrentPosition) +{ + WCHAR Char = pwchBuffer[ulCurrentPosition]; + if (Char == UNICODE_TAB) + { + size_t NumSpaces = 0; + SHORT XPosition = sOriginalCursorPositionX; + + for (size_t i = 0; i <= ulCurrentPosition; i++) + { + Char = pwchBuffer[i]; + if (Char == UNICODE_TAB) + { + NumSpaces = NUMBER_OF_SPACES_IN_TAB(XPosition); + } + else if (IS_CONTROL_CHAR(Char)) + { + NumSpaces = 2; + } + else if (IsGlyphFullWidth(Char)) + { + NumSpaces = 2; + } + else + { + NumSpaces = 1; + } + XPosition = (SHORT)(XPosition + NumSpaces); + } + + return NumSpaces; + } + else if (IS_CONTROL_CHAR(Char)) + { + return 2; + } + else if (IsGlyphFullWidth(Char)) + { + return 2; + } + else + { + return 1; + } +} + + +// Routine Description: +// - if we have leftover input, copy as much fits into the user's +// buffer and return. we may have multi line input, if a macro +// has been defined that contains the $T character. +// Arguments: +// - inputBuffer - Pointer to input buffer to read from. +// - buffer - buffer to place read char data into +// - bytesRead - number of bytes read and filled into the buffer +// - readHandleState - input read handle data associated with this read operation +// - unicode - true if read should be unicode, false otherwise +// Return Value: +// - STATUS_NO_MEMORY in low memory situation +// - other relevant NTSTATUS codes +[[nodiscard]] +static NTSTATUS _ReadPendingInput(InputBuffer& inputBuffer, + gsl::span buffer, + size_t& bytesRead, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool unicode) +{ + // TODO: MSFT: 18047766 - Correct this method to not play byte counting games. + BOOL fAddDbcsLead = FALSE; + size_t NumToWrite = 0; + size_t NumToBytes = 0; + wchar_t* pBuffer = reinterpret_cast(buffer.data()); + size_t bufferRemaining = buffer.size_bytes(); + bytesRead = 0; + + if (buffer.size_bytes() < sizeof(wchar_t)) + { + return STATUS_BUFFER_TOO_SMALL; + } + + const auto pending = readHandleState.GetPendingInput(); + size_t pendingBytes = pending.size() * sizeof(wchar_t); + auto Tmp = pending.cbegin(); + + if (readHandleState.IsMultilineInput()) + { + if (!unicode) + { + if (inputBuffer.IsReadPartialByteSequenceAvailable()) + { + std::unique_ptr event = inputBuffer.FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *pBuffer = static_cast(pKeyEvent->GetCharData()); + ++pBuffer; + bufferRemaining -= sizeof(wchar_t); + pendingBytes -= sizeof(wchar_t); + fAddDbcsLead = TRUE; + } + + if (pendingBytes == 0 || bufferRemaining == 0) + { + readHandleState.CompletePending(); + bytesRead = 1; + return STATUS_SUCCESS; + } + else + { + for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0; + NumToBytes < pendingBytes && + NumToBytes < bufferRemaining / sizeof(wchar_t) && + *Tmp != UNICODE_LINEFEED; + Tmp++, NumToWrite += sizeof(wchar_t)) + { + NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; + } + } + } + + + NumToWrite = 0; + Tmp = pending.cbegin(); + while (NumToWrite < pendingBytes && + *Tmp != UNICODE_LINEFEED) + { + ++Tmp; + NumToWrite += sizeof(wchar_t); + } + + NumToWrite += sizeof(wchar_t); + if (NumToWrite > bufferRemaining) + { + NumToWrite = bufferRemaining; + } + } + else + { + if (!unicode) + { + if (inputBuffer.IsReadPartialByteSequenceAvailable()) + { + std::unique_ptr event = inputBuffer.FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *pBuffer = static_cast(pKeyEvent->GetCharData()); + ++pBuffer; + bufferRemaining -= sizeof(wchar_t); + pendingBytes -= sizeof(wchar_t); + fAddDbcsLead = TRUE; + } + + if (pendingBytes == 0) + { + readHandleState.CompletePending(); + bytesRead = 1; + return STATUS_SUCCESS; + } + else + { + for (NumToWrite = 0, Tmp = pending.cbegin(), NumToBytes = 0; + NumToBytes < pendingBytes && NumToBytes < bufferRemaining / sizeof(wchar_t); + Tmp++, NumToWrite += sizeof(wchar_t)) + { + NumToBytes += IsGlyphFullWidth(*Tmp) ? 2 : 1; + } + } + } + + NumToWrite = (bufferRemaining < pendingBytes) ? bufferRemaining : pendingBytes; + } + + memmove(pBuffer, pending.data(), NumToWrite); + pendingBytes -= NumToWrite; + if (pendingBytes != 0) + { + std::wstring_view remainingPending{ pending.data() + (NumToWrite / sizeof(wchar_t)) , pendingBytes / sizeof(wchar_t) }; + readHandleState.UpdatePending(remainingPending); + } + else + { + readHandleState.CompletePending(); + } + + if (!unicode) + { + // if ansi, translate string. we allocated the capture buffer + // large enough to handle the translated string. + std::unique_ptr tempBuffer = std::make_unique(NumToBytes); + std::unique_ptr partialEvent; + + NumToWrite = TranslateUnicodeToOem(pBuffer, + gsl::narrow(NumToWrite / sizeof(wchar_t)), + tempBuffer.get(), + gsl::narrow(NumToBytes), + partialEvent); + if (partialEvent.get()) + { + inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent)); + } + +#pragma prefast(suppress:__WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine but prefast can't follow it, evidently") + memmove(pBuffer, tempBuffer.get(), NumToWrite); + + if (fAddDbcsLead) + { + NumToWrite++; + } + } + + bytesRead = NumToWrite; + return STATUS_SUCCESS; +} + +// Routine Description: +// - read in characters until the buffer is full or return is read. +// since we may wait inside this loop, store all important variables +// in the read data structure. if we do wait, a read data structure +// will be allocated from the heap and its pointer will be stored +// in the wait block. the CookedReadData will be copied into the +// structure. the data is freed when the read is completed. +// Arguments: +// - inputBuffer - input buffer to read data from +// - processData - process handle of process making read request +// - buffer - buffer to place read char data +// - bytesRead - on output, the number of bytes read into pwchBuffer +// - controlKeyState - set by a cooked read +// - initialData - text of initial data found in the read message +// - ctrlWakeupMask - used by COOKED_READ_DATA +// - readHandleState - input read handle data associated with this read operation +// - exeName - name of the exe requesting the read +// - unicode - true if read should be unicode, false otherwise +// - waiter - If a wait is necessary this will contain the wait +// object on output +// Return Value: +// - STATUS_UNSUCCESSFUL if not able to access current screen buffer +// - STATUS_NO_MEMORY in low memory situation +// - other relevant HRESULT codes +[[nodiscard]] +static HRESULT _ReadLineInput(InputBuffer& inputBuffer, + const HANDLE processData, + gsl::span buffer, + size_t& bytesRead, + DWORD& controlKeyState, + const std::string_view initialData, + const DWORD ctrlWakeupMask, + INPUT_READ_HANDLE_DATA& readHandleState, + const std::wstring_view exeName, + const bool unicode, + std::unique_ptr& waiter) noexcept +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + RETURN_HR_IF(E_FAIL, !gci.HasActiveOutputBuffer()); + + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + CommandHistory* const pCommandHistory = CommandHistory::s_Find(processData); + + try + { + auto cookedReadData = std::make_unique(&inputBuffer, // pInputBuffer + &readHandleState, // pInputReadHandleData + screenInfo, // pScreenInfo + buffer.size_bytes(), // UserBufferSize + reinterpret_cast(buffer.data()), // UserBuffer + ctrlWakeupMask, // CtrlWakeupMask + pCommandHistory, // CommandHistory + exeName, // exe name + initialData); + + gci.SetCookedReadData(cookedReadData.get()); + bytesRead = buffer.size_bytes(); // This parameter on the way in is the size to read, on the way out, it will be updated to what is actually read. + if (CONSOLE_STATUS_WAIT == cookedReadData->Read(unicode, bytesRead, controlKeyState)) + { + // memory will be cleaned up by wait queue + waiter.reset(cookedReadData.release()); + } + else + { + gci.SetCookedReadData(nullptr); + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Character (raw) mode. Read at least one character in. After one +// character has been read, get any more available characters and +// return. The first call to GetChar may block. If we do wait, a read +// data structure will be allocated from the heap and its pointer will +// be stored in the wait block. The RawReadData will be copied into +// the structure. The data is freed when the read is completed. +// Arguments: +// - inputBuffer - input buffer to read data from +// - buffer - on output, the amount of data read, in bytes +// - bytesRead - number of bytes read and placed into buffer +// - readHandleState - input read handle data associated with this read operation +// - unicode - true if read should be unicode, false otherwise +// - waiter - if a wait is necessary, on output this will contain +// the associated wait object +// Return Value: +// - CONSOLE_STATUS_WAIT if a wait is necessary. ppWaiter will be +// populated. +// - STATUS_SUCCESS on success +// - Other NTSTATUS codes as necessary +[[nodiscard]] +static NTSTATUS _ReadCharacterInput(InputBuffer& inputBuffer, + gsl::span buffer, + size_t& bytesRead, + INPUT_READ_HANDLE_DATA& readHandleState, + const bool unicode, + std::unique_ptr& waiter) +{ + + size_t NumToWrite = 0; + bool addDbcsLead = false; + NTSTATUS Status = STATUS_SUCCESS; + wchar_t* pBuffer = reinterpret_cast(buffer.data()); + size_t bufferRemaining = buffer.size_bytes(); + bytesRead = 0; + + if (buffer.size() < 1) + { + return STATUS_BUFFER_TOO_SMALL; + } + + if (bytesRead < bufferRemaining) + { + wchar_t* pwchBufferTmp = pBuffer; + + NumToWrite = 0; + + if (!unicode && inputBuffer.IsReadPartialByteSequenceAvailable()) + { + std::unique_ptr event = inputBuffer.FetchReadPartialByteSequence(false); + const KeyEvent* const pKeyEvent = static_cast(event.get()); + *pBuffer = static_cast(pKeyEvent->GetCharData()); + ++pBuffer; + bufferRemaining -= sizeof(wchar_t); + addDbcsLead = true; + + if (bufferRemaining == 0) + { + bytesRead = 1; + return STATUS_SUCCESS; + } + } + else + { + Status = GetChar(&inputBuffer, + pBuffer, + true, + nullptr, + nullptr, + nullptr); + } + + if (Status == CONSOLE_STATUS_WAIT) + { + waiter = std::make_unique(&inputBuffer, + &readHandleState, + gsl::narrow(buffer.size_bytes()), + reinterpret_cast(buffer.data())); + } + + if (!NT_SUCCESS(Status)) + { + bytesRead = 0; + return Status; + } + + if (!addDbcsLead) + { + bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1; + NumToWrite += sizeof(wchar_t); + pBuffer++; + } + + while (NumToWrite < static_cast(bufferRemaining)) + { + Status = GetChar(&inputBuffer, + pBuffer, + false, + nullptr, + nullptr, + nullptr); + if (!NT_SUCCESS(Status)) + { + break; + } + bytesRead += IsGlyphFullWidth(*pBuffer) ? 2 : 1; + NumToWrite += sizeof(wchar_t); + pBuffer++; + } + + // if ansi, translate string. we allocated the capture buffer large enough to handle the translated string. + if (!unicode) + { + std::unique_ptr tempBuffer; + try + { + tempBuffer = std::make_unique(bytesRead); + } + catch (...) + { + return STATUS_NO_MEMORY; + } + + pBuffer = pwchBufferTmp; + std::unique_ptr partialEvent; + + bytesRead = TranslateUnicodeToOem(pBuffer, + gsl::narrow(NumToWrite / sizeof(wchar_t)), + tempBuffer.get(), + gsl::narrow(bytesRead), + partialEvent); + + if (partialEvent.get()) + { + inputBuffer.StoreReadPartialByteSequence(std::move(partialEvent)); + } + +#pragma prefast(suppress:26053 26015, "PREfast claims read overflow. *pReadByteCount is the exact size of tempBuffer as allocated above.") + memmove(pBuffer, tempBuffer.get(), bytesRead); + + if (addDbcsLead) + { + ++bytesRead; + } + } + else + { + // We always return the byte count for A & W modes, so in + // the Unicode case where we didn't translate back, set + // the return to the byte count that we assembled while + // pulling characters from the internal buffers. + bytesRead = NumToWrite; + } + } + return STATUS_SUCCESS; +} + +// Routine Description: +// - This routine reads in characters for stream input and does the +// required processing based on the input mode (line, char, echo). +// - This routine returns UNICODE characters. +// Arguments: +// - inputBuffer - Pointer to input buffer to read from. +// - processData - process handle of process making read request +// - buffer - buffer to place read char data into +// - bytesRead - the length of data placed in buffer. Measured in bytes. +// - controlKeyState - set by a cooked read +// - initialData - text of initial data found in the read message +// - ctrlWakeupMask - used by COOKED_READ_DATA +// - readHandleState - read handle data associated with this read +// - exeName- name of the exe requesting the read +// - unicode - true for a unicode read, false for ascii +// - waiter - If a wait is necessary this will contain the wait +// object on output +// Return Value: +// - STATUS_BUFFER_TOO_SMALL if pdwNumBytes is too small to store char +// data. +// - CONSOLE_STATUS_WAIT if a wait is necessary. ppWaiter will be +// populated. +// - STATUS_SUCCESS on success +// - Other NSTATUS codes as necessary +[[nodiscard]] +NTSTATUS DoReadConsole(InputBuffer& inputBuffer, + const HANDLE processData, + gsl::span buffer, + size_t& bytesRead, + ULONG& controlKeyState, + const std::string_view initialData, + const DWORD ctrlWakeupMask, + INPUT_READ_HANDLE_DATA& readHandleState, + const std::wstring_view exeName, + const bool unicode, + std::unique_ptr& waiter) noexcept +{ + try + { + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + waiter.reset(); + + bytesRead = 0; + + if (buffer.size() < 1) + { + return STATUS_BUFFER_TOO_SMALL; + } + + const size_t OutputBufferSize = buffer.size_bytes(); + + if (readHandleState.IsInputPending()) + { + return _ReadPendingInput(inputBuffer, + buffer, + bytesRead, + readHandleState, + unicode); + } + else if (WI_IsFlagSet(inputBuffer.InputMode, ENABLE_LINE_INPUT)) + { + return NTSTATUS_FROM_HRESULT(_ReadLineInput(inputBuffer, + processData, + buffer, + bytesRead, + controlKeyState, + initialData, + ctrlWakeupMask, + readHandleState, + exeName, + unicode, + waiter)); + } + else + { + return _ReadCharacterInput(inputBuffer, + buffer, + bytesRead, + readHandleState, + unicode, + waiter); + } + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleAImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept +{ + try + { + return HRESULT_FROM_NT(DoReadConsole(context, + clientHandle, + buffer, + written, + controlKeyState, + initialData, + controlWakeupMask, + readHandleState, + exeName, + false, + waiter)); + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT ApiRoutines::ReadConsoleWImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept +{ + try + { + return HRESULT_FROM_NT(DoReadConsole(context, + clientHandle, + buffer, + written, + controlKeyState, + initialData, + controlWakeupMask, + readHandleState, + exeName, + true, + waiter)); + } + CATCH_RETURN(); +} + +void UnblockWriteConsole(const DWORD dwReason) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.Flags &= ~dwReason; + + if (WI_AreAllFlagsClear(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING))) + { + // There is no longer any reason to suspend output, so unblock it. + gci.OutputQueue.NotifyWaiters(true); + } +} diff --git a/src/host/stream.h b/src/host/stream.h new file mode 100644 index 000000000..306c8fe05 --- /dev/null +++ b/src/host/stream.h @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation + +Licensed under the MIT license. +Module Name: +- stream.h + +Abstract: +- This file implements the NT console server stream API + +Author: +- Therese Stowell (ThereseS) 6-Nov-1990 + +Revision History: +--*/ + +#pragma once + +#include "cmdline.h" +#include "..\server\IWaitRoutine.h" +#include "readData.hpp" + +#define IS_CONTROL_CHAR(wch) ((wch) < L' ') + +[[nodiscard]] +NTSTATUS GetChar(_Inout_ InputBuffer* const pInputBuffer, + _Out_ wchar_t* const pwchOut, + const bool Wait, + _Out_opt_ bool* const pCommandLineEditingKeys, + _Out_opt_ bool* const pPopupKeys, + _Out_opt_ DWORD* const pdwKeyState) noexcept; + +// Routine Description: +// - This routine returns the total number of screen spaces the characters up to the specified character take up. +size_t RetrieveTotalNumberOfSpaces(const SHORT sOriginalCursorPositionX, + _In_reads_(ulCurrentPosition) const WCHAR * const pwchBuffer, + const size_t ulCurrentPosition); + +// Routine Description: +// - This routine returns the number of screen spaces the specified character takes up. +size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX, + _In_reads_(ulCurrentPosition + 1) const WCHAR * const pwchBuffer, + _In_ size_t ulCurrentPosition); + +VOID UnblockWriteConsole(const DWORD dwReason); diff --git a/src/host/telemetry.cpp b/src/host/telemetry.cpp new file mode 100644 index 000000000..90226cbac --- /dev/null +++ b/src/host/telemetry.cpp @@ -0,0 +1,576 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include +#include "Shlwapi.h" +#include "telemetry.hpp" +#include + +#include "history.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +TRACELOGGING_DEFINE_PROVIDER(g_hConhostV2EventTraceProvider, + "Microsoft.Windows.Console.Host", + // {fe1ff234-1f09-50a8-d38d-c44fab43e818} + (0xfe1ff234, 0x1f09, 0x50a8, 0xd3, 0x8d, 0xc4, 0x4f, 0xab, 0x43, 0xe8, 0x18), + TraceLoggingOptionMicrosoftTelemetry()); +#pragma warning(push) +// Disable 4351 so we can initialize the arrays to 0 without a warning. +#pragma warning(disable:4351) +Telemetry::Telemetry() + : _fpFindStringLengthAverage(0), + _fpDirectionDownAverage(0), + _fpMatchCaseAverage(0), + _uiFindNextClickedTotal(0), + _uiColorSelectionUsed(0), + _tStartedAt(0), + _wchProcessFileNames(), + // Start at position 1, since the first 2 bytes contain the number of strings. + _iProcessFileNamesNext(1), + _iProcessConnectedCurrently(SIZE_MAX), + _rgiProccessFileNameIndex(), + _rguiProcessFileNamesCount(), + _rgiAlphabeticalIndex(), + _rguiProcessFileNamesCodesCount(), + _rguiProcessFileNamesFailedCodesCount(), + _rguiProcessFileNamesFailedOutsideCodesCount(), + _rguiTimesApiUsed(), + _rguiTimesApiUsedAnsi(), + _uiNumberProcessFileNames(0), + _fBashUsed(false), + _fKeyboardTextEditingUsed(false), + _fKeyboardTextSelectionUsed(false), + _fUserInteractiveForTelemetry(false), + _fCtrlPgUpPgDnUsed(false), + _uiCtrlShiftCProcUsed(0), + _uiCtrlShiftCRawUsed(0), + _uiCtrlShiftVProcUsed(0), + _uiCtrlShiftVRawUsed(0), + _uiQuickEditCopyProcUsed(0), + _uiQuickEditCopyRawUsed(0), + _uiQuickEditPasteProcUsed(0), + _uiQuickEditPasteRawUsed(0) +{ + time(&_tStartedAt); + TraceLoggingRegister(g_hConhostV2EventTraceProvider); + TraceLoggingWriteStart(_activity, "ActivityStart"); + // initialize wil tracelogging + wil::SetResultLoggingCallback(&Tracing::TraceFailure); +} +#pragma warning(pop) + +Telemetry::~Telemetry() +{ + TraceLoggingWriteStop(_activity, "ActivityStop"); + TraceLoggingUnregister(g_hConhostV2EventTraceProvider); +} + +void Telemetry::SetUserInteractive() +{ + _fUserInteractiveForTelemetry = true; +} + +void Telemetry::SetCtrlPgUpPgDnUsed() +{ + _fCtrlPgUpPgDnUsed = true; + SetUserInteractive(); +} + +void Telemetry::LogCtrlShiftCProcUsed() +{ + _uiCtrlShiftCProcUsed++; + SetUserInteractive(); +} + +void Telemetry::LogCtrlShiftCRawUsed() +{ + _uiCtrlShiftCRawUsed++; + SetUserInteractive(); +} + +void Telemetry::LogCtrlShiftVProcUsed() +{ + _uiCtrlShiftVProcUsed++; + SetUserInteractive(); +} + +void Telemetry::LogCtrlShiftVRawUsed() +{ + _uiCtrlShiftVRawUsed++; + SetUserInteractive(); +} + +void Telemetry::LogQuickEditCopyProcUsed() +{ + _uiQuickEditCopyProcUsed++; + SetUserInteractive(); +} + +void Telemetry::LogQuickEditCopyRawUsed() +{ + _uiQuickEditCopyRawUsed++; + SetUserInteractive(); +} + +void Telemetry::LogQuickEditPasteProcUsed() +{ + _uiQuickEditPasteProcUsed++; + SetUserInteractive(); +} + +void Telemetry::LogQuickEditPasteRawUsed() +{ + _uiQuickEditPasteRawUsed++; + SetUserInteractive(); +} + +// Log usage of the Color Selection option. +void Telemetry::LogColorSelectionUsed() +{ + _uiColorSelectionUsed++; + SetUserInteractive(); +} + +void Telemetry::SetWindowSizeChanged() +{ + SetUserInteractive(); +} + +void Telemetry::SetContextMenuUsed() +{ + SetUserInteractive(); +} + +void Telemetry::SetKeyboardTextSelectionUsed() +{ + _fKeyboardTextSelectionUsed = true; + SetUserInteractive(); +} + +void Telemetry::SetKeyboardTextEditingUsed() +{ + _fKeyboardTextEditingUsed = true; + SetUserInteractive(); +} + +// Log an API call was used. +void Telemetry::LogApiCall(const ApiCall api, const BOOLEAN fUnicode) +{ + // Initially we thought about passing over a string (ex. "XYZ") 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. + if (fUnicode) + { + _rguiTimesApiUsed[api]++; + } + else + { + _rguiTimesApiUsedAnsi[api]++; + } +} + +// Log an API call was used. +void Telemetry::LogApiCall(const ApiCall api) +{ + _rguiTimesApiUsed[api]++; +} + +// Log usage of the Find Dialog. +void Telemetry::LogFindDialogNextClicked(const unsigned int uiStringLength, const bool fDirectionDown, const bool fMatchCase) +{ + // Don't send telemetry for every time it's used, as this will help reduce the load on our servers. + // Instead just create a running average of the string length, the direction down radio + // button, and match case checkbox. + _fpFindStringLengthAverage = ((_fpFindStringLengthAverage * _uiFindNextClickedTotal + uiStringLength) / (_uiFindNextClickedTotal + 1)); + _fpDirectionDownAverage = ((_fpDirectionDownAverage * _uiFindNextClickedTotal + (fDirectionDown ? 1 : 0)) / (_uiFindNextClickedTotal + 1)); + _fpMatchCaseAverage = ((_fpMatchCaseAverage * _uiFindNextClickedTotal + (fMatchCase ? 1 : 0)) / (_uiFindNextClickedTotal + 1)); + _uiFindNextClickedTotal++; +} + +// Find dialog was closed, now send out the telemetry. +void Telemetry::FindDialogClosed() +{ +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "FindDialogUsed", + TraceLoggingValue(_fpFindStringLengthAverage, "StringLengthAverage"), + TraceLoggingValue(_fpDirectionDownAverage, "DirectionDownAverage"), + TraceLoggingValue(_fpMatchCaseAverage, "MatchCaseAverage"), + TraceLoggingValue(_uiFindNextClickedTotal, "FindNextButtonClickedTotal"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + // Get ready for the next time the dialog is used. + _fpFindStringLengthAverage = 0; + _fpDirectionDownAverage = 0; + _fpMatchCaseAverage = 0; + _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() +{ + // Get the values even if we aren't recording the previously connected process, since we want to reset them to 0. + unsigned int _uiTimesUsedCurrent = TermTelemetry::Instance().GetAndResetTimesUsedCurrent(); + unsigned int _uiTimesFailedCurrent = TermTelemetry::Instance().GetAndResetTimesFailedCurrent(); + unsigned int _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 +// us from having an additional search through the array, and improves performance. +bool Telemetry::FindProcessName(const WCHAR* pszProcessName, _Out_ size_t *iPosition) const +{ + int iMin = 0; + int iMid = 0; + int iMax = _uiNumberProcessFileNames - 1; + int result = 0; + + while (iMin <= iMax) + { + iMid = (iMax + iMin) / 2; + // Use a case-insensitive comparison. We do support running Linux binaries now, but we haven't seen them connect + // as processes, and even if they did, we don't care about the difference in running emacs vs. Emacs. + result = _wcsnicmp(pszProcessName, _wchProcessFileNames + _rgiProccessFileNameIndex[_rgiAlphabeticalIndex[iMid]], MAX_PATH); + if (result < 0) + { + iMax = iMid - 1; + } + else if (result > 0) + { + iMin = iMid + 1; + } + else + { + // Found the string. + *iPosition = iMid; + return true; + } + } + + // Let them know which position to insert the string at. + *iPosition = (result > 0) ? iMid + 1 : iMid; + return false; +} + +// Log a process name and number of times it has connected to the console in preparation to send through telemetry. +// We were considering sending out a log of telemetry when each process connects, but then the telemetry can get +// complicated and spammy, especially since command line utilities like help.exe and where.exe are considered processes. +// Don't send telemetry for every time a process connects, as this will help reduce the load on our servers. +// Just save the name and count, and send the telemetry before the console exits. +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]; + DWORD dwSize = ARRAYSIZE(wszFilePathAndName); + if (QueryFullProcessImageName(hProcess, 0, wszFilePathAndName, &dwSize)) + { + // Stripping out the path also helps with PII issues in case they launched the program + // from a path containing their username. + PWSTR pwszFileName = PathFindFileName(wszFilePathAndName); + + size_t iFileName; + if (FindProcessName(pwszFileName, &iFileName)) + { + // We already logged this process name, so just increment the count. + _iProcessConnectedCurrently = _rgiAlphabeticalIndex[iFileName]; + _rguiProcessFileNamesCount[_iProcessConnectedCurrently]++; + } + else if ((_uiNumberProcessFileNames < ARRAYSIZE(_rguiProcessFileNamesCount)) && + (_iProcessFileNamesNext < ARRAYSIZE(_wchProcessFileNames) - 10)) + { + // Check if the MS released bash was used. MS bash is installed under windows\system32, and it's possible somebody else + // could be installing their bash into that directory, but not likely. If the user first runs a non-MS bash, + // and then runs MS bash, we won't detect the MS bash as running, but it's an acceptable compromise. + if (!_fBashUsed && !_wcsnicmp(c_pwszBashExeName, pwszFileName, MAX_PATH)) + { + // We could have gotten the system directory once when this class starts, but we'd have to hold the memory for it + // plus we're not sure we'd ever need it, so just get it when we know we're running bash.exe. + WCHAR wszSystemDirectory[MAX_PATH] = L""; + if (GetSystemDirectory(wszSystemDirectory, ARRAYSIZE(wszSystemDirectory))) + { + _fBashUsed = (PathIsSameRoot(wszFilePathAndName, wszSystemDirectory) == TRUE); + } + } + + // In order to send out a dynamic array of strings through telemetry, we have to pack the strings into a single WCHAR array. + // There currently aren't any helper functions for this, and we have to pack it manually. + // To understand the format of the single string, consult the documentation in the traceloggingprovider.h file. + if (SUCCEEDED(StringCchCopyW(_wchProcessFileNames + _iProcessFileNamesNext, ARRAYSIZE(_wchProcessFileNames) - _iProcessFileNamesNext - 1, pwszFileName))) + { + // As each FileName comes in, it's appended to the end. However to improve searching speed, we have an array of indexes + // that is alphabetically sorted. We could call qsort, but that would be a waste in performance since we're just adding one string + // at a time and we always keep the array sorted, so just shift everything over one. + for (size_t n = _uiNumberProcessFileNames; n > iFileName; n--) + { + _rgiAlphabeticalIndex[n] = _rgiAlphabeticalIndex[n - 1]; + } + + // Now point to the string, and set the count to 1. + _rgiAlphabeticalIndex[iFileName] = _uiNumberProcessFileNames; + _rgiProccessFileNameIndex[_uiNumberProcessFileNames] = _iProcessFileNamesNext; + _rguiProcessFileNamesCount[_uiNumberProcessFileNames] = 1; + _iProcessFileNamesNext += wcslen(pwszFileName) + 1; + _iProcessConnectedCurrently = _uiNumberProcessFileNames++; + + // Packed arrays start with a UINT16 value indicating the number of elements in the array. + BYTE *pbFileNames = reinterpret_cast(_wchProcessFileNames); + pbFileNames[0] = (BYTE)_uiNumberProcessFileNames; + pbFileNames[1] = (BYTE)(_uiNumberProcessFileNames >> 8); + } + } + } + } +} + +// This Function sends final Trace log before session closes. +// We're primarily sending this telemetry once at the end, and only when the user interacted with the console +// so we don't overwhelm our servers by sending a constant stream of telemetry while the console is being used. +void Telemetry::WriteFinalTraceLog() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // 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. + TermTelemetry::Instance().SetActivityId(_activity.Id()); + 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); +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "SessionEnding", + TraceLoggingBool(_fBashUsed, "BashUsed"), + TraceLoggingBool(_fCtrlPgUpPgDnUsed, "CtrlPgUpPgDnUsed"), + TraceLoggingBool(_fKeyboardTextEditingUsed, "KeyboardTextEditingUsed"), + TraceLoggingBool(_fKeyboardTextSelectionUsed, "KeyboardTextSelectionUsed"), + TraceLoggingUInt32(_uiCtrlShiftCProcUsed, "CtrlShiftCProcUsed"), + TraceLoggingUInt32(_uiCtrlShiftCRawUsed, "CtrlShiftCRawUsed"), + TraceLoggingUInt32(_uiCtrlShiftVProcUsed, "CtrlShiftVProcUsed"), + TraceLoggingUInt32(_uiCtrlShiftVRawUsed, "CtrlShiftVRawUsed"), + TraceLoggingUInt32(_uiQuickEditCopyProcUsed, "QuickEditCopyProcUsed"), + TraceLoggingUInt32(_uiQuickEditCopyRawUsed, "QuickEditCopyRawUsed"), + TraceLoggingUInt32(_uiQuickEditPasteProcUsed, "QuickEditPasteProcUsed"), + TraceLoggingUInt32(_uiQuickEditPasteRawUsed, "QuickEditPasteRawUsed"), + TraceLoggingBool(gci.GetLinkTitle().length() == 0, "LaunchedFromShortcut"), + // Normally we would send out a single array containing the name and count, + // but that's difficult to do with our telemetry system, so send out two separate arrays. + // 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"), + TraceLoggingInt32(static_cast(tEndedAt), "EndedUsingAtSeconds"), + TraceLoggingUInt32(_uiColorSelectionUsed, "ColorSelectionUsed"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + // Always send this back. We could only send this back when they click "OK" in the settings dialog, but sending it + // back every time should give us a good idea of their current, final settings, and not just only when they change a setting. +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "Settings", + TraceLoggingBool(gci.GetAutoPosition(), "AutoPosition"), + TraceLoggingBool(gci.GetHistoryNoDup(), "HistoryNoDuplicates"), + TraceLoggingBool(gci.GetInsertMode(), "InsertMode"), + TraceLoggingBool(gci.GetLineSelection(), "LineSelection"), + TraceLoggingBool(gci.GetQuickEdit(), "QuickEdit"), + TraceLoggingValue(gci.GetWindowAlpha(), "WindowAlpha"), + TraceLoggingBool(gci.GetWrapText(), "WrapText"), + TraceLoggingUInt32Array((UINT32 const*)gci.GetColorTable(), (UINT16)gci.GetColorTableSize(), "ColorTable"), + TraceLoggingValue(gci.CP, "CodePageInput"), + TraceLoggingValue(gci.OutputCP, "CodePageOutput"), + TraceLoggingValue(gci.GetFontSize().X, "FontSizeX"), + TraceLoggingValue(gci.GetFontSize().Y, "FontSizeY"), + TraceLoggingValue(gci.GetHotKey(), "HotKey"), + TraceLoggingValue(gci.GetScreenBufferSize().X, "ScreenBufferSizeX"), + TraceLoggingValue(gci.GetScreenBufferSize().Y, "ScreenBufferSizeY"), + TraceLoggingValue(gci.GetStartupFlags(), "StartupFlags"), + TraceLoggingValue(gci.GetVirtTermLevel(), "VirtualTerminalLevel"), + TraceLoggingValue(gci.GetWindowSize().X, "WindowSizeX"), + TraceLoggingValue(gci.GetWindowSize().Y, "WindowSizeY"), + TraceLoggingValue(gci.GetWindowOrigin().X, "WindowOriginX"), + TraceLoggingValue(gci.GetWindowOrigin().Y, "WindowOriginY"), + TraceLoggingValue(gci.GetFaceName(), "FontName"), + TraceLoggingBool(gci.IsAltF4CloseAllowed(), "AllowAltF4Close"), + TraceLoggingBool(gci.GetCtrlKeyShortcutsDisabled(), "ControlKeyShortcutsDisabled"), + TraceLoggingBool(gci.GetEnableColorSelection(), "EnabledColorSelection"), + TraceLoggingBool(gci.GetFilterOnPaste(), "FilterOnPaste"), + TraceLoggingBool(gci.GetTrimLeadingZeros(), "TrimLeadingZeros"), + TraceLoggingValue(gci.GetLaunchFaceName(), "LaunchFontName"), + TraceLoggingValue(CommandHistory::s_CountOfHistories(), "CommandHistoriesNumber"), + TraceLoggingValue(gci.GetCodePage(), "CodePage"), + TraceLoggingValue(gci.GetCursorSize(), "CursorSize"), + TraceLoggingValue(gci.GetFontFamily(), "FontFamily"), + TraceLoggingValue(gci.GetFontWeight(), "FontWeight"), + TraceLoggingValue(gci.GetHistoryBufferSize(), "HistoryBufferSize"), + TraceLoggingValue(gci.GetNumberOfHistoryBuffers(), "HistoryBuffersNumber"), + TraceLoggingValue(gci.GetScrollScale(), "ScrollScale"), + TraceLoggingValue(gci.GetFillAttribute(), "FillAttribute"), + TraceLoggingValue(gci.GetPopupFillAttribute(), "PopupFillAttribute"), + TraceLoggingValue(gci.GetShowWindow(), "ShowWindow"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + static_assert(sizeof(UINT32) == sizeof(*gci.GetColorTable()), "gci.GetColorTable()"); + + // 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. +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "ApiUsed", + TraceLoggingUInt32(_rguiTimesApiUsed[AddConsoleAlias], "AddConsoleAlias"), + TraceLoggingUInt32(_rguiTimesApiUsed[AllocConsole], "AllocConsole"), + TraceLoggingUInt32(_rguiTimesApiUsed[AttachConsole], "AttachConsole"), + TraceLoggingUInt32(_rguiTimesApiUsed[CreateConsoleScreenBuffer], "CreateConsoleScreenBuffer"), + TraceLoggingUInt32(_rguiTimesApiUsed[GenerateConsoleCtrlEvent], "GenerateConsoleCtrlEvent"), + TraceLoggingUInt32(_rguiTimesApiUsed[FillConsoleOutputAttribute], "FillConsoleOutputAttribute"), + TraceLoggingUInt32(_rguiTimesApiUsed[FillConsoleOutputCharacter], "FillConsoleOutputCharacter"), + TraceLoggingUInt32(_rguiTimesApiUsed[FlushConsoleInputBuffer], "FlushConsoleInputBuffer"), + TraceLoggingUInt32(_rguiTimesApiUsed[FreeConsole], "FreeConsole"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleAlias], "GetConsoleAlias"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleAliases], "GetConsoleAliases"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleAliasExesLength], "GetConsoleAliasExesLength"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleAliasesLength], "GetConsoleAliasesLength"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleAliasExes], "GetConsoleAliasExes"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleCP], "GetConsoleCP"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleCursorInfo], "GetConsoleCursorInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleDisplayMode], "GetConsoleDisplayMode"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleFontSize], "GetConsoleFontSize"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleHistoryInfo], "GetConsoleHistoryInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleLangId], "GetConsoleLangId"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleMode], "GetConsoleMode"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleOriginalTitle], "GetConsoleOriginalTitle"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleOutputCP], "GetConsoleOutputCP"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleProcessList], "GetConsoleProcessList"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleScreenBufferInfoEx], "GetConsoleScreenBufferInfoEx"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleSelectionInfo], "GetConsoleSelectionInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleTitle], "GetConsoleTitle"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetConsoleWindow], "GetConsoleWindow"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetCurrentConsoleFontEx], "GetCurrentConsoleFontEx"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetLargestConsoleWindowSize], "GetLargestConsoleWindowSize"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetNumberOfConsoleInputEvents], "GetNumberOfConsoleInputEvents"), + TraceLoggingUInt32(_rguiTimesApiUsed[GetNumberOfConsoleMouseButtons], "GetNumberOfConsoleMouseButtons"), + TraceLoggingUInt32(_rguiTimesApiUsed[PeekConsoleInput], "PeekConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsed[ReadConsole], "ReadConsole"), + TraceLoggingUInt32(_rguiTimesApiUsed[ReadConsoleInput], "ReadConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsed[ReadConsoleOutput], "ReadConsoleOutput"), + TraceLoggingUInt32(_rguiTimesApiUsed[ReadConsoleOutputAttribute], "ReadConsoleOutputAttribute"), + TraceLoggingUInt32(_rguiTimesApiUsed[ReadConsoleOutputCharacter], "ReadConsoleOutputCharacter"), + TraceLoggingUInt32(_rguiTimesApiUsed[ScrollConsoleScreenBuffer], "ScrollConsoleScreenBuffer"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleActiveScreenBuffer], "SetConsoleActiveScreenBuffer"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleCP], "SetConsoleCP"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleCursorInfo], "SetConsoleCursorInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleCursorPosition], "SetConsoleCursorPosition"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleDisplayMode], "SetConsoleDisplayMode"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleHistoryInfo], "SetConsoleHistoryInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleMode], "SetConsoleMode"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleOutputCP], "SetConsoleOutputCP"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleScreenBufferInfoEx], "SetConsoleScreenBufferInfoEx"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleScreenBufferSize], "SetConsoleScreenBufferSize"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleTextAttribute], "SetConsoleTextAttribute"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleTitle], "SetConsoleTitle"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetConsoleWindowInfo], "SetConsoleWindowInfo"), + TraceLoggingUInt32(_rguiTimesApiUsed[SetCurrentConsoleFontEx], "SetCurrentConsoleFontEx"), + TraceLoggingUInt32(_rguiTimesApiUsed[WriteConsole], "WriteConsole"), + TraceLoggingUInt32(_rguiTimesApiUsed[WriteConsoleInput], "WriteConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsed[WriteConsoleOutput], "WriteConsoleOutput"), + TraceLoggingUInt32(_rguiTimesApiUsed[WriteConsoleOutputAttribute], "WriteConsoleOutputAttribute"), + TraceLoggingUInt32(_rguiTimesApiUsed[WriteConsoleOutputCharacter], "WriteConsoleOutputCharacter"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + for (int n = 0; n < ARRAYSIZE(_rguiTimesApiUsedAnsi); n++) + { + if (_rguiTimesApiUsedAnsi[n]) + { + // Ansi specific API's are used less, so check if we have anything to send back. + // Also breaking it up into a separate TraceLoggingWriteTagged fixes a compilation warning that + // the heap is too small. +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "ApiAnsiUsed", + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[AddConsoleAlias], "AddConsoleAlias"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[FillConsoleOutputCharacter], "FillConsoleOutputCharacter"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleAlias], "GetConsoleAlias"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleAliases], "GetConsoleAliases"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleAliasesLength], "GetConsoleAliasesLength"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleAliasExes], "GetConsoleAliasExes"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleAliasExesLength], "GetConsoleAliasExesLength"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleOriginalTitle], "GetConsoleOriginalTitle"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[GetConsoleTitle], "GetConsoleTitle"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[PeekConsoleInput], "PeekConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[ReadConsole], "ReadConsole"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[ReadConsoleInput], "ReadConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[ReadConsoleOutput], "ReadConsoleOutput"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[ReadConsoleOutputCharacter], "ReadConsoleOutputCharacter"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[SetConsoleTitle], "SetConsoleTitle"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[WriteConsole], "WriteConsole"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[WriteConsoleInput], "WriteConsoleInput"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[WriteConsoleOutput], "WriteConsoleOutput"), + TraceLoggingUInt32(_rguiTimesApiUsedAnsi[WriteConsoleOutputCharacter], "WriteConsoleOutputCharacter"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + break; + } + } + } + } +} + +// These are legacy error messages with limited value, so don't send them back as telemetry. +void Telemetry::LogRipMessage(_In_z_ const char* pszMessage, ...) const +{ + // Code needed for passing variable parameters to the vsprintf function. + va_list args; + va_start(args, pszMessage); + char szMessageEvaluated[200] = ""; + int cCharsWritten = vsprintf_s(szMessageEvaluated, ARRAYSIZE(szMessageEvaluated), pszMessage, args); + va_end(args); + +#if DBG + OutputDebugStringA(szMessageEvaluated); +#endif + + if (cCharsWritten > 0) + { +#pragma prefast(suppress:__WARNING_NONCONST_LOCAL, "Activity can't be const, since it's set to a random value on startup.") + TraceLoggingWriteTagged(_activity, + "RipMessage", + TraceLoggingString(szMessageEvaluated, "Message")); + } +} diff --git a/src/host/telemetry.hpp b/src/host/telemetry.hpp new file mode 100644 index 000000000..cb9b3af58 --- /dev/null +++ b/src/host/telemetry.hpp @@ -0,0 +1,197 @@ +/*++ +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 + +Author(s): +- Evan Wirt (EvanWi) 09-Jul-2014 +- Kourosh Mehrain (KMehrain) 09-Jul-2014 +- Stephen Somuah (StSomuah) 09-Jul-2014 +- Anup Manandhar (AnupM) 09-Jul-2014 +--*/ +#pragma once + +#include + +class Telemetry +{ +public: + // Implement this as a singleton class. + static Telemetry& Instance() + { + static Telemetry s_Instance; + return s_Instance; + } + + void SetUserInteractive(); + void SetWindowSizeChanged(); + void SetContextMenuUsed(); + void SetKeyboardTextSelectionUsed(); + void SetKeyboardTextEditingUsed(); + void SetCtrlPgUpPgDnUsed(); + void LogCtrlShiftCProcUsed(); + void LogCtrlShiftCRawUsed(); + void LogCtrlShiftVProcUsed(); + void LogCtrlShiftVRawUsed(); + void LogQuickEditCopyProcUsed(); + void LogQuickEditCopyRawUsed(); + void LogQuickEditPasteProcUsed(); + void LogQuickEditPasteRawUsed(); + void LogColorSelectionUsed(); + + void LogFindDialogNextClicked(const unsigned int iStringLength, const bool fDirectionDown, const bool fMatchCase); + void LogProcessConnected(const HANDLE hProcess); + void FindDialogClosed(); + void WriteFinalTraceLog(); + + void LogRipMessage(_In_z_ const char* pszMessage, ...) const; + + // Names are from the external API call names. Note that some names can be different + // than the internal API calls. + // Don't worry about the following APIs, because they are external to our conhost codebase and hard to track through + // telemetry: GetStdHandle, SetConsoleCtrlHandler, SetStdHandle + // We can't differentiate between these apis, so just log the "-Ex" versions: GetConsoleScreenBufferInfo / GetConsoleScreenBufferInfoEx, + // GetCurrentConsoleFontEx / GetCurrentConsoleFont + enum ApiCall + { + AddConsoleAlias = 0, + AllocConsole, + AttachConsole, + CreateConsoleScreenBuffer, + FillConsoleOutputAttribute, + FillConsoleOutputCharacter, + FlushConsoleInputBuffer, + FreeConsole, + GenerateConsoleCtrlEvent, + GetConsoleAlias, + GetConsoleAliases, + GetConsoleAliasesLength, + GetConsoleAliasExes, + GetConsoleAliasExesLength, + GetConsoleCP, + GetConsoleCursorInfo, + GetConsoleDisplayMode, + GetConsoleFontSize, + GetConsoleHistoryInfo, + GetConsoleMode, + GetConsoleLangId, + GetConsoleOriginalTitle, + GetConsoleOutputCP, + GetConsoleProcessList, + GetConsoleScreenBufferInfoEx, + GetConsoleSelectionInfo, + GetConsoleTitle, + GetConsoleWindow, + GetCurrentConsoleFontEx, + GetLargestConsoleWindowSize, + GetNumberOfConsoleInputEvents, + GetNumberOfConsoleMouseButtons, + PeekConsoleInput, + ReadConsole, + ReadConsoleInput, + ReadConsoleOutput, + ReadConsoleOutputAttribute, + ReadConsoleOutputCharacter, + ScrollConsoleScreenBuffer, + SetConsoleActiveScreenBuffer, + SetConsoleCP, + SetConsoleCursorInfo, + SetConsoleCursorPosition, + SetConsoleDisplayMode, + SetConsoleHistoryInfo, + SetConsoleMode, + SetConsoleOutputCP, + SetConsoleScreenBufferInfoEx, + SetConsoleScreenBufferSize, + SetConsoleTextAttribute, + SetConsoleTitle, + SetConsoleWindowInfo, + SetCurrentConsoleFontEx, + WriteConsole, + WriteConsoleInput, + WriteConsoleOutput, + WriteConsoleOutputAttribute, + WriteConsoleOutputCharacter, + // Only use this last enum as a count of the number of api enums. + NUMBER_OF_APIS + }; + void LogApiCall(const ApiCall api); + void LogApiCall(const ApiCall api, const BOOLEAN fUnicode); + +private: + // Used to prevent multiple instances + Telemetry(); + ~Telemetry(); + Telemetry(Telemetry const&); + void operator=(Telemetry const&); + + bool FindProcessName(const WCHAR* pszProcessName, _Out_ size_t *iPosition) const; + void TotalCodesForPreviousProcess(); + + static const int c_iMaxProcessesConnected = 100; + + TraceLoggingActivity _activity; + + float _fpFindStringLengthAverage; + float _fpDirectionDownAverage; + float _fpMatchCaseAverage; + unsigned int _uiFindNextClickedTotal; + unsigned int _uiColorSelectionUsed; + time_t _tStartedAt; + WCHAR const * const c_pwszBashExeName = L"bash.exe"; + + // The current recommendation is to keep telemetry events 4KB or less, so let's keep our array at less than 2KB (1000 * 2 bytes). + WCHAR _wchProcessFileNames[1000]; + // Index into our specially packed string, where to insert the next string. + size_t _iProcessFileNamesNext; + // Index for the currently connected process. + size_t _iProcessConnectedCurrently; + // An array of indexes into the _wchProcessFileNames array, which point to the individual process names. + size_t _rgiProccessFileNameIndex[c_iMaxProcessesConnected]; + // Number of times each process has connected to the console. + 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]; + // Total number of file names we've added. + UINT16 _uiNumberProcessFileNames; + + bool _fBashUsed; + bool _fKeyboardTextEditingUsed; + bool _fKeyboardTextSelectionUsed; + bool _fUserInteractiveForTelemetry; + bool _fCtrlPgUpPgDnUsed; + + // Linux copy and paste keyboard shortcut telemetry + unsigned int _uiCtrlShiftCProcUsed; + unsigned int _uiCtrlShiftCRawUsed; + unsigned int _uiCtrlShiftVProcUsed; + unsigned int _uiCtrlShiftVRawUsed; + + // Quick edit copy and paste usage telemetry + unsigned int _uiQuickEditCopyProcUsed; + unsigned int _uiQuickEditCopyRawUsed; + unsigned int _uiQuickEditPasteProcUsed; + unsigned int _uiQuickEditPasteRawUsed; +}; + +// Log the RIPMSG through telemetry, and also through a normal OutputDebugStringW call. +// These are drop-in substitutes for the RIPMSG0-4 macros from \windows\Core\ntcon2\conhost\consrv.h +#define RIPMSG0(flags, msg) Telemetry::Instance().LogRipMessage(msg); +#define RIPMSG1(flags, msg, a) Telemetry::Instance().LogRipMessage(msg, a); +#define RIPMSG2(flags, msg, a, b) Telemetry::Instance().LogRipMessage(msg, a, b); +#define RIPMSG3(flags, msg, a, b, c) Telemetry::Instance().LogRipMessage(msg, a, b, c); +#define RIPMSG4(flags, msg, a, b, c, d) Telemetry::Instance().LogRipMessage(msg, a, b, c, d); diff --git a/src/host/tracing.cpp b/src/host/tracing.cpp new file mode 100644 index 000000000..1f3ec9686 --- /dev/null +++ b/src/host/tracing.cpp @@ -0,0 +1,1149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "tracing.hpp" +#include "../interactivity/win32/UiaTextRange.hpp" +#include "../interactivity/win32/screenInfoUiaProvider.hpp" +#include "../interactivity/win32/windowUiaProvider.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +enum TraceKeywords +{ + //Font = 0x001, // _DBGFONTS + //Font2 = 0x002, // _DBGFONTS2 + Chars = 0x004, // _DBGCHARS + Output = 0x008, // _DBGOUTPUT + General = 0x100, + Input = 0x200, + API = 0x400, + UIA = 0x800, + All = 0xFFF +}; +DEFINE_ENUM_FLAG_OPERATORS(TraceKeywords); + +// Routine Description: +// - Creates a tracing object to assist with automatically firing a stop event +// when this object goes out of scope. +// - Give it back to the caller and they will release it when the event period is over. +// Arguments: +// - onExit - Function to process when the object is destroyed (on exit) +Tracing::Tracing(std::function onExit) : + _onExit(onExit) +{ + +} + +// Routine Description: +// - Destructs a tracing object, running any on exit routine, if necessary. +Tracing::~Tracing() +{ + if (_onExit) + { + _onExit(); + } +} + +// Routine Description: +// - Provides generic tracing for all API call types in the form of +// start/stop period events for timing and region-of-interest purposes +// while doing performance analysis. +// Arguments: +// - result - Reference to the area where the result code from the Api call +// will be stored for use in the stop event. +// - traceName - The name of the API call to list in the trace details +// Return Value: +// - An object for the caller to hold until the API call is complete. +// Then destroy it to signal that the call is over so the stop trace can be written. +Tracing Tracing::s_TraceApiCall(const NTSTATUS& result, PCSTR traceName) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "ApiCall", + TraceLoggingString(traceName, "ApiName"), + TraceLoggingOpcode(WINEVENT_OPCODE_START), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API)); + + return Tracing([traceName, &result] { + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "ApiCall", + TraceLoggingString(traceName, "ApiName"), + TraceLoggingHResult(result, "Result"), + TraceLoggingOpcode(WINEVENT_OPCODE_STOP), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API)); + }); +} + +ULONG Tracing::s_ulDebugFlag = 0x0; + +void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetLargestWindowSize", + TraceLoggingHexInt32(status, "ResultCode"), + TraceLoggingInt32(a->Size.X, "MaxWindowWidthInChars"), + TraceLoggingInt32(a->Size.Y, "MaxWindowHeightInChars"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet) +{ + // Duplicate copies required by TraceLogging documentation ("don't get cute" examples) + // Using logic inside these macros can make problems. Do all logic outside macros. + + if (fSet) + { + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_SetConsoleScreenBufferInfo", + TraceLoggingHexInt32(status, "ResultCode"), + TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), + TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), + TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"), + TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"), + TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"), + TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); + } + else + { + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetConsoleScreenBufferInfo", + TraceLoggingHexInt32(status, "ResultCode"), + TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), + TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), + TraceLoggingInt32(a->CurrentWindowSize.X, "WindowWidthInChars"), + TraceLoggingInt32(a->CurrentWindowSize.Y, "WindowHeightInChars"), + TraceLoggingInt32(a->MaximumWindowSize.X, "MaxWindowWidthInChars"), + TraceLoggingInt32(a->MaximumWindowSize.Y, "MaxWindowHeightInChars"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); + } +} + +void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_SetConsoleScreenBufferSize", + TraceLoggingHexInt32(status, "ResultCode"), + TraceLoggingInt32(a->Size.X, "BufferWidthInChars"), + TraceLoggingInt32(a->Size.Y, "BufferHeightInChars"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_SetConsoleWindowInfo", + TraceLoggingHexInt32(status, "ResultCode"), + TraceLoggingBool(a->Absolute, "IsWindowRectAbsolute"), + TraceLoggingInt32(a->Window.Left, "WindowRectLeft"), + TraceLoggingInt32(a->Window.Right, "WindowRectRight"), + TraceLoggingInt32(a->Window.Top, "WindowRectTop"), + TraceLoggingInt32(a->Window.Bottom, "WindowRectBottom"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a) +{ + if (a->Unicode) + { + const wchar_t* const buf = static_cast(buffer); + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_WriteConsole", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingUInt32(a->NumBytes, "NumBytes"), + TraceLoggingCountedWideString(buf, static_cast(a->NumBytes / sizeof(wchar_t)), "input buffer"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); + } + else + { + const char* const buf = static_cast(buffer); + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_WriteConsole", + TraceLoggingBoolean(a->Unicode, "Unicode"), + TraceLoggingUInt32(a->NumBytes, "NumBytes"), + TraceLoggingCountedString(buf, static_cast(a->NumBytes / sizeof(char)), "input buffer"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); + } +} + +void Tracing::s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetConsoleScreenBufferInfo", + TraceLoggingInt16(a->Size.X, "Size.X"), + TraceLoggingInt16(a->Size.Y, "Size.Y"), + TraceLoggingInt16(a->CursorPosition.X, "CursorPosition.X"), + TraceLoggingInt16(a->CursorPosition.Y, "CursorPosition.Y"), + TraceLoggingInt16(a->ScrollPosition.X, "ScrollPosition.X"), + TraceLoggingInt16(a->ScrollPosition.Y, "ScrollPosition.Y"), + TraceLoggingHexUInt16(a->Attributes, "Attributes"), + TraceLoggingInt16(a->CurrentWindowSize.X, "CurrentWindowSize.X"), + TraceLoggingInt16(a->CurrentWindowSize.Y, "CurrentWindowSize.Y"), + TraceLoggingInt16(a->MaximumWindowSize.X, "MaximumWindowSize.X"), + TraceLoggingInt16(a->MaximumWindowSize.Y, "MaximumWindowSize.Y"), + TraceLoggingHexUInt16(a->PopupAttributes, "PopupAttributes"), + TraceLoggingBoolean(a->FullscreenSupported, "FullscreenSupported"), + TraceLoggingHexUInt32FixedArray((UINT32 const*)a->ColorTable, _countof(a->ColorTable), "ColorTable"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); + static_assert(sizeof(UINT32) == sizeof(*a->ColorTable), "a->ColorTable"); +} + +void Tracing::s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring& handleType) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetConsoleMode", + TraceLoggingHexUInt32(a->Mode, "Mode"), + TraceLoggingWideString(handleType.c_str(), "Handle type"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_SetConsoleTextAttribute", + TraceLoggingHexUInt16(a->Attributes, "Attributes"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_WriteConsoleOutput", + TraceLoggingInt16(a->WriteCoord.X, "WriteCoord.X"), + TraceLoggingInt16(a->WriteCoord.Y, "WriteCoord.Y"), + TraceLoggingHexUInt32(a->StringType, "StringType"), + TraceLoggingUInt32(a->NumRecords, "NumRecords"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::API) + ); +} + +void Tracing::s_TraceWindowViewport(const Microsoft::Console::Types::Viewport& viewport) +{ + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "WindowViewport", + TraceLoggingInt32(viewport.Height(), "ViewHeight"), + TraceLoggingInt32(viewport.Width(), "ViewWidth"), + TraceLoggingInt32(viewport.Top(), "OriginTop"), + TraceLoggingInt32(viewport.Left(), "OriginLeft"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::General) + ); +} + +void Tracing::s_TraceChars(_In_z_ const char* pszMessage, ...) +{ + va_list args; + va_start(args, pszMessage); + char szBuffer[256] = ""; + vsprintf_s(szBuffer, ARRAYSIZE(szBuffer), pszMessage, args); + va_end(args); + + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "CharsTrace", + TraceLoggingString(szBuffer), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Chars) + ); + + if (s_ulDebugFlag & TraceKeywords::Chars) + { + OutputDebugStringA(szBuffer); + } +} + +void Tracing::s_TraceOutput(_In_z_ const char* pszMessage, ...) +{ + va_list args; + va_start(args, pszMessage); + char szBuffer[256] = ""; + vsprintf_s(szBuffer, ARRAYSIZE(szBuffer), pszMessage, args); + va_end(args); + + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "OutputTrace", + TraceLoggingString(szBuffer), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Output) + ); + + if (s_ulDebugFlag & TraceKeywords::Output) + { + OutputDebugStringA(szBuffer); + } +} + +void Tracing::s_TraceWindowMessage(const MSG& msg) +{ + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Window Message", + TraceLoggingHexUInt32(msg.message, "message"), + TraceLoggingHexUInt64(msg.wParam, "wParam"), + TraceLoggingHexUInt64(msg.lParam, "lParam"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); +} + +void Tracing::s_TraceInputRecord(const INPUT_RECORD& inputRecord) +{ + switch (inputRecord.EventType) + { + case KEY_EVENT: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Key Event Input Record", + TraceLoggingBool(inputRecord.Event.KeyEvent.bKeyDown, "bKeyDown"), + TraceLoggingUInt16(inputRecord.Event.KeyEvent.wRepeatCount, "wRepeatCount"), + TraceLoggingHexUInt16(inputRecord.Event.KeyEvent.wVirtualKeyCode, "wVirtualKeyCode"), + TraceLoggingHexUInt16(inputRecord.Event.KeyEvent.wVirtualScanCode, "wVirtualScanCode"), + TraceLoggingWChar(inputRecord.Event.KeyEvent.uChar.UnicodeChar, "UnicodeChar"), + TraceLoggingWChar(inputRecord.Event.KeyEvent.uChar.AsciiChar, "AsciiChar"), + TraceLoggingHexUInt16(inputRecord.Event.KeyEvent.uChar.UnicodeChar, "Hex UnicodeChar"), + TraceLoggingHexUInt8(inputRecord.Event.KeyEvent.uChar.AsciiChar, "Hex AsciiChar"), + TraceLoggingHexUInt32(inputRecord.Event.KeyEvent.dwControlKeyState, "dwControlKeyState"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + case MOUSE_EVENT: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Mouse Event Input Record", + TraceLoggingInt16(inputRecord.Event.MouseEvent.dwMousePosition.X, "dwMousePosition.X"), + TraceLoggingInt16(inputRecord.Event.MouseEvent.dwMousePosition.Y, "dwMousePosition.Y"), + TraceLoggingHexUInt32(inputRecord.Event.MouseEvent.dwButtonState, "dwButtonState"), + TraceLoggingHexUInt32(inputRecord.Event.MouseEvent.dwControlKeyState, "dwControlKeyState"), + TraceLoggingHexUInt32(inputRecord.Event.MouseEvent.dwEventFlags, "dwEventFlags"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + case WINDOW_BUFFER_SIZE_EVENT: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Window Buffer Size Event Input Record", + TraceLoggingInt16(inputRecord.Event.WindowBufferSizeEvent.dwSize.X, "dwSize.X"), + TraceLoggingInt16(inputRecord.Event.WindowBufferSizeEvent.dwSize.Y, "dwSize.Y"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + case MENU_EVENT: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Menu Event Input Record", + TraceLoggingHexUInt64(inputRecord.Event.MenuEvent.dwCommandId, "dwCommandId"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + case FOCUS_EVENT: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Focus Event Input Record", + TraceLoggingBool(inputRecord.Event.FocusEvent.bSetFocus, "bSetFocus"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + default: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Unknown Input Record", + TraceLoggingHexUInt16(inputRecord.EventType, "EventType"), + TraceLoggingLevel(WINEVENT_LEVEL_ERROR), + TraceLoggingKeyword(TraceKeywords::Input)); + break; + } +} + +void __stdcall Tracing::TraceFailure(const wil::FailureInfo& failure) noexcept +{ + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "Failure", + TraceLoggingHexUInt32(failure.hr, "HResult"), + TraceLoggingString(failure.pszFile, "File"), + TraceLoggingUInt32(failure.uLineNumber, "LineNumber"), + TraceLoggingString(failure.pszFunction, "Function"), + TraceLoggingWideString(failure.pszMessage, "Message"), + TraceLoggingString(failure.pszCallContext, "CallingContext"), + TraceLoggingString(failure.pszModule, "Module"), + TraceLoggingPointer(failure.returnAddress, "Site"), + TraceLoggingString(failure.pszCode, "Code"), + TraceLoggingLevel(WINEVENT_LEVEL_ERROR)); +} + +void Tracing::s_TraceUia(const UiaTextRange* const range, + const UiaTextRangeTracing::ApiCall apiCall, + const UiaTextRangeTracing::IApiMsg* const apiMsg) +{ + unsigned long long id = 0u; + bool degenerate = true; + Endpoint start = 0u; + Endpoint end = 0u; + if (range) + { + id = range->GetId(); + degenerate = range->IsDegenerate(); + start = range->GetStart(); + end = range->GetEnd(); + } + + switch (apiCall) + { + case UiaTextRangeTracing::ApiCall::Constructor: + { + id = static_cast(apiMsg)->Id; + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Constructor", + TraceLoggingValue(id, "_id"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::AddRef: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::AddRef", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::Release: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Release", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::QueryInterface: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::QueryInterface", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::Clone: + { + if (apiMsg == nullptr) + { + return; + } + auto cloneId = reinterpret_cast(apiMsg)->CloneId; + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Clone", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(cloneId, "clone's _id"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::Compare: + { + const UiaTextRangeTracing::ApiMsgCompare* const msg = static_cast(apiMsg); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Compare", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->OtherId, "Other's Id"), + TraceLoggingValue(msg->Equal, "Equal"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::CompareEndpoints: + { + const UiaTextRangeTracing::ApiMsgCompareEndpoints* const msg = static_cast(apiMsg); + const wchar_t* const pEndpoint = _textPatternRangeEndpointToString(msg->Endpoint); + const wchar_t* const pTargetEndpoint = _textPatternRangeEndpointToString(msg->TargetEndpoint); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::CompareEndpoints", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->OtherId, "Other's Id"), + TraceLoggingValue(pEndpoint, "endpoint"), + TraceLoggingValue(pTargetEndpoint, "targetEndpoint"), + TraceLoggingValue(msg->Result, "Result"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::ExpandToEnclosingUnit: + { + const UiaTextRangeTracing::ApiMsgExpandToEnclosingUnit* const msg = static_cast(apiMsg); + const wchar_t* const pUnitName = _textUnitToString(msg->Unit); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::ExpandToEnclosingUnit", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(pUnitName, "Unit"), + TraceLoggingValue(msg->OriginalStart, "Original Start"), + TraceLoggingValue(msg->OriginalEnd, "Original End"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::FindAttribute: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::FindAttribute", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::FindText: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::FindText", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::GetAttributeValue: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::GetAttributeValue", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::GetBoundingRectangles: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::GetBoundingRectangles", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::GetEnclosingElement: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::GetEnclosingElement", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::GetText: + { + const UiaTextRangeTracing::ApiMsgGetText* const msg = static_cast(apiMsg); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::GetText", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->Text, "Text"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::Move: + { + const UiaTextRangeTracing::ApiMsgMove* const msg = static_cast(apiMsg); + const wchar_t* const unitStr = _textUnitToString(msg->Unit); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Move", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->OriginalStart, "Original Start"), + TraceLoggingValue(msg->OriginalEnd, "Original End"), + TraceLoggingValue(unitStr, "unit"), + TraceLoggingValue(msg->RequestedCount, "Requested Count"), + TraceLoggingValue(msg->MovedCount, "Moved Count"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::MoveEndpointByUnit: + { + const UiaTextRangeTracing::ApiMsgMoveEndpointByUnit* const msg = static_cast(apiMsg); + const wchar_t* const pEndpoint = _textPatternRangeEndpointToString(msg->Endpoint); + const wchar_t* const unitStr = _textUnitToString(msg->Unit); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::MoveEndpointByUnit", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->OriginalStart, "Original Start"), + TraceLoggingValue(msg->OriginalEnd, "Original End"), + TraceLoggingValue(pEndpoint, "endpoint"), + TraceLoggingValue(unitStr, "unit"), + TraceLoggingValue(msg->RequestedCount, "Requested Count"), + TraceLoggingValue(msg->MovedCount, "Moved Count"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::MoveEndpointByRange: + { + const UiaTextRangeTracing::ApiMsgMoveEndpointByRange* const msg = static_cast(apiMsg); + const wchar_t* const pEndpoint = _textPatternRangeEndpointToString(msg->Endpoint); + const wchar_t* const pTargetEndpoint = _textPatternRangeEndpointToString(msg->TargetEndpoint); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::MoveEndpointByRange", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->OriginalStart, "Original Start"), + TraceLoggingValue(msg->OriginalEnd, "Original End"), + TraceLoggingValue(pEndpoint, "endpoint"), + TraceLoggingValue(pTargetEndpoint, "targetEndpoint"), + TraceLoggingValue(msg->OtherId, "Other's _id"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::Select: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::Select", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::AddToSelection: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::AddToSelection", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::RemoveFromSelection: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::RemoveFromSelection", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case UiaTextRangeTracing::ApiCall::ScrollIntoView: + { + const UiaTextRangeTracing::ApiMsgScrollIntoView* const msg = static_cast(apiMsg); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::ScrollIntoView", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingValue(msg->AlignToTop, "alignToTop"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case UiaTextRangeTracing::ApiCall::GetChildren: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "UiaTextRange::GetChildren", + TraceLoggingValue(id, "_id"), + TraceLoggingValue(start, "_start"), + TraceLoggingValue(end, "_end"), + TraceLoggingValue(degenerate, "_degenerate"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + default: + break; + } +} + +void Tracing::s_TraceUia(const ScreenInfoUiaProvider* const /*pProvider*/, + const ScreenInfoUiaProviderTracing::ApiCall apiCall, + const ScreenInfoUiaProviderTracing::IApiMsg* const apiMsg) +{ + switch (apiCall) + { + case ScreenInfoUiaProviderTracing::ApiCall::Constructor: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::Constructor", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::Signal: + { + const ScreenInfoUiaProviderTracing::ApiMsgSignal* const msg = static_cast(apiMsg); + const wchar_t* const signalName = _eventIdToString(msg->Signal); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::Signal", + TraceLoggingValue(msg->Signal), + TraceLoggingValue(signalName, "Event Name"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case ScreenInfoUiaProviderTracing::ApiCall::AddRef: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::AddRef", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::Release: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::Release", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::QueryInterface: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::QueryInterface", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetProviderOptions: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetProviderOptions", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetPatternProvider: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetPatternProvider", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetPropertyValue: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetPropertyValue", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetHostRawElementProvider: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetHostRawElementProvider", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::Navigate: + { + const ScreenInfoUiaProviderTracing::ApiMsgNavigate* const msg = static_cast(apiMsg); + const wchar_t* const direction = _directionToString(msg->Direction); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::Navigate", + TraceLoggingValue(direction, "direction"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case ScreenInfoUiaProviderTracing::ApiCall::GetRuntimeId: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetRuntimeId", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetBoundingRectangle: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetBoundingRectangles", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetEmbeddedFragmentRoots: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetEmbeddedFragmentRoots", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::SetFocus: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::SetFocus", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetFragmentRoot: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetFragmentRoot", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetSelection: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetSelection", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetVisibleRanges: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetVisibleRanges", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::RangeFromChild: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::RangeFromChild", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::RangeFromPoint: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::RangeFromPoint", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetDocumentRange: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetDocumentRange", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case ScreenInfoUiaProviderTracing::ApiCall::GetSupportedTextSelection: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "ScreenInfoUiaProvider::GetSupportedTextSelection", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + default: + break; + } +} + +void Tracing::s_TraceUia(const WindowUiaProvider* const /*pProvider*/, + const WindowUiaProviderTracing::ApiCall apiCall, + const WindowUiaProviderTracing::IApiMsg* const apiMsg) +{ + switch (apiCall) + { + case WindowUiaProviderTracing::ApiCall::Create: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::Create", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::Signal: + { + const WindowUiaProviderTracing::ApiMessageSignal* const msg = static_cast(apiMsg); + const wchar_t* const eventName = _eventIdToString(msg->Signal); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::Signal", + TraceLoggingValue(msg->Signal, "Signal"), + TraceLoggingValue(eventName, "Signal Name"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case WindowUiaProviderTracing::ApiCall::AddRef: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::AddRef", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::Release: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::Release", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::QueryInterface: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::QueryInterface", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetProviderOptions: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetProviderOptions", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetPatternProvider: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetPatternProvider", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetPropertyValue: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetPropertyValue", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetHostRawElementProvider: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetHostRawElementProvider", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::Navigate: + { + const WindowUiaProviderTracing::ApiMsgNavigate* const msg = static_cast(apiMsg); + const wchar_t* const direction = _directionToString(msg->Direction); + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::Navigate", + TraceLoggingValue(direction, "direction"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + } + case WindowUiaProviderTracing::ApiCall::GetRuntimeId: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetRuntimeId", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetBoundingRectangle: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetBoundingRectangle", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetEmbeddedFragmentRoots: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetEmbeddedFragmentRoots", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::SetFocus: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::SetFocus", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetFragmentRoot: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetFragmentRoot", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::ElementProviderFromPoint: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::ElementProviderFromPoint", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + case WindowUiaProviderTracing::ApiCall::GetFocus: + TraceLoggingWrite( + g_hConhostV2EventTraceProvider, + "WindowUiaProvider::GetFocus", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TraceKeywords::UIA) + ); + break; + default: + break; + } +} + +const wchar_t* const Tracing::_textPatternRangeEndpointToString(int endpoint) +{ + switch (endpoint) + { + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start: + return L"Start"; + case TextPatternRangeEndpoint::TextPatternRangeEndpoint_End: + return L"End"; + default: + return L"Unknown"; + } +} + +const wchar_t* const Tracing::_textUnitToString(int unit) +{ + switch (unit) + { + case TextUnit::TextUnit_Character: + return L"TextUnit_Character"; + case TextUnit::TextUnit_Format: + return L"TextUnit_Format"; + case TextUnit::TextUnit_Word: + return L"TextUnit_Word"; + case TextUnit::TextUnit_Line: + return L"TextUnit_Line"; + case TextUnit::TextUnit_Paragraph: + return L"TextUnit_Paragraph"; + case TextUnit::TextUnit_Page: + return L"TextUnit_Page"; + case TextUnit::TextUnit_Document: + return L"TextUnit_Document"; + default: + return L"Unknown"; + } +} + +const wchar_t* const Tracing::_eventIdToString(long eventId) +{ + switch (eventId) + { + case UIA_AutomationFocusChangedEventId: + return L"UIA_AutomationFocusChangedEventId"; + case UIA_Text_TextChangedEventId: + return L"UIA_Text_TextChangedEventId"; + case UIA_Text_TextSelectionChangedEventId: + return L"UIA_Text_TextSelectionChangedEventId"; + default: + return L"Unknown"; + } +} + +const wchar_t* const Tracing::_directionToString(int direction) +{ + switch (direction) + { + case NavigateDirection::NavigateDirection_FirstChild: + return L"NavigateDirection_FirstChild"; + case NavigateDirection::NavigateDirection_LastChild: + return L"NavigateDirection_LastChild"; + case NavigateDirection::NavigateDirection_NextSibling: + return L"NavigateDirection_NextSibling"; + case NavigateDirection::NavigateDirection_Parent: + return L"NavigateDirection_Parent"; + case NavigateDirection::NavigateDirection_PreviousSibling: + return L"NavigateDirection_PreviousSibling"; + default: + return L"Unknown"; + } +} diff --git a/src/host/tracing.hpp b/src/host/tracing.hpp new file mode 100644 index 000000000..cb3f2b66d --- /dev/null +++ b/src/host/tracing.hpp @@ -0,0 +1,109 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- tracing.hpp + +Abstract: +- This module is used for recording tracing/debugging information to the telemetry ETW channel +- The data is not automatically broadcast to telemetry backends as it does not set the TELEMETRY keyword. +- NOTE: Many functions in this file appear to be copy/pastes. This is because the TraceLog documentation warns + to not be "cute" in trying to reduce its macro usages with variables as it can cause unexpected behavior. + +Author(s): +- Michael Niksa (miniksa) 25-Nov-2014 +--*/ + +#pragma once + +#include "../types/inc/Viewport.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class UiaTextRange; + + namespace UiaTextRangeTracing + { + enum class ApiCall; + struct IApiMsg; + } + + class ScreenInfoUiaProvider; + + namespace ScreenInfoUiaProviderTracing + { + enum class ApiCall; + struct IApiMsg; + } + + class WindowUiaProvider; + + namespace WindowUiaProviderTracing + { + enum class ApiCall; + struct IApiMsg; + } +} + +#if DBG +#define DBGCHARS(_params_) { Tracing::s_TraceChars _params_ ; } +#define DBGOUTPUT(_params_) { Tracing::s_TraceOutput _params_ ; } +#else +#define DBGCHARS(_params_) +#define DBGOUTPUT(_params_) +#endif + +class Tracing +{ +public: + ~Tracing(); + + static Tracing s_TraceApiCall(const NTSTATUS& result, PCSTR traceName); + + static void s_TraceApi(const NTSTATUS status, const CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a); + static void s_TraceApi(const NTSTATUS status, const CONSOLE_SCREENBUFFERINFO_MSG* const a, const bool fSet); + static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETSCREENBUFFERSIZE_MSG* const a); + static void s_TraceApi(const NTSTATUS status, const CONSOLE_SETWINDOWINFO_MSG* const a); + + static void s_TraceApi(_In_ const void* const buffer, const CONSOLE_WRITECONSOLE_MSG* const a); + + static void s_TraceApi(const CONSOLE_SCREENBUFFERINFO_MSG* const a); + static void s_TraceApi(const CONSOLE_MODE_MSG* const a, const std::wstring& handleType); + static void s_TraceApi(const CONSOLE_SETTEXTATTRIBUTE_MSG* const a); + static void s_TraceApi(const CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG* const a); + + static void s_TraceWindowViewport(const Microsoft::Console::Types::Viewport& viewport); + + static void s_TraceChars(_In_z_ const char* pszMessage, ...); + static void s_TraceOutput(_In_z_ const char* pszMessage, ...); + + static void s_TraceWindowMessage(const MSG& msg); + static void s_TraceInputRecord(const INPUT_RECORD& inputRecord); + + static void __stdcall TraceFailure(const wil::FailureInfo& failure) noexcept; + + static void s_TraceUia(const Microsoft::Console::Interactivity::Win32::UiaTextRange* const range, + const Microsoft::Console::Interactivity::Win32::UiaTextRangeTracing::ApiCall apiCall, + const Microsoft::Console::Interactivity::Win32::UiaTextRangeTracing::IApiMsg* const apiMsg); + + static void s_TraceUia(const Microsoft::Console::Interactivity::Win32::ScreenInfoUiaProvider* const pProvider, + const Microsoft::Console::Interactivity::Win32::ScreenInfoUiaProviderTracing::ApiCall apiCall, + const Microsoft::Console::Interactivity::Win32::ScreenInfoUiaProviderTracing::IApiMsg* const apiMsg); + + static void s_TraceUia(const Microsoft::Console::Interactivity::Win32::WindowUiaProvider* const pProvider, + const Microsoft::Console::Interactivity::Win32::WindowUiaProviderTracing::ApiCall apiCall, + const Microsoft::Console::Interactivity::Win32::WindowUiaProviderTracing::IApiMsg* const apiMsg); + +private: + static ULONG s_ulDebugFlag; + + Tracing(std::function onExit); + + std::function _onExit; + + static const wchar_t* const _textPatternRangeEndpointToString(int endpoint); + static const wchar_t* const _textUnitToString(int unit); + static const wchar_t* const _eventIdToString(long eventId); + static const wchar_t* const _directionToString(int direction); +}; diff --git a/src/host/ut_host/AliasTests.cpp b/src/host/ut_host/AliasTests.cpp new file mode 100644 index 000000000..c6f07480d --- /dev/null +++ b/src/host/ut_host/AliasTests.cpp @@ -0,0 +1,642 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "alias.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class AliasTests +{ + TEST_CLASS(AliasTests); + + TEST_CLASS_SETUP(ClassSetup) + { + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + // Don't let aliases spill across test functions. + Alias::s_TestClearAliases(); + return true; + } + + DWORD _ReplacePercentWithCRLF(std::wstring& string) + { + DWORD linesExpected = 0; + + auto pos = string.find(L'%'); + + while (pos != std::wstring::npos) + { + PCWSTR newline = L"\r\n"; + string = string.erase(pos, 1); + string = string.insert(pos, newline); + linesExpected++; // we expect one "line" per newline character returned. + pos = string.find(L'%'); + } + + return linesExpected; + } + + void _RetrieveTargetExpectedPair(std::wstring& target, + std::wstring& expected) + { + // Get test parameters + String targetExpectedPair; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"targetExpectedPair", targetExpectedPair)); + + // Convert WEX strings into the wstrings + int sepIndex = targetExpectedPair.Find(L'='); + target = targetExpectedPair.Left(sepIndex); + expected = targetExpectedPair.Mid(sepIndex + 1); + } + + TEST_METHOD(TestMatchAndCopy) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:exeName", L"{test.exe}") + TEST_METHOD_PROPERTY(L"Data:aliasName", L"{foo}") + TEST_METHOD_PROPERTY(L"Data:originalString", L"{ foo one two three four five six seven eight nine ten eleven twelve }") + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" // Each of these is a human-generated test of macro before and after. + L"bar=bar%," // The character % will be turned into an \r\n + L"bar $1=bar one%," + L"bar $2=bar two%," + L"bar $3=bar three%," + L"bar $4=bar four%," + L"bar $5=bar five%," + L"bar $6=bar six%," + L"bar $7=bar seven%," + L"bar $8=bar eight%," + L"bar $9=bar nine%," + L"bar $3 $1 $4 $1 $5 $9=bar three one four one five nine%," // assorted mixed order parameters with a repeat + L"bar $*=bar one two three four five six seven eight nine ten eleven twelve%," + L"longer=longer%," // replace with a target longer than the original alias + L"redirect $1$goutput $2=redirect one>output two%," // doing these without spaces between some commands + L"REDIRECT $1$GOUTPUT $2=REDIRECT one>OUTPUT two%," // also notice we're checking both upper and lowercase + L"append $1$g$goutput $2=append one>>output two%," + L"APPEND $1$G$GOUTPUT $2=APPEND one>>OUTPUT two%," + L"redirect $1$linputfile.$2=redirect onefun one | test nine < two.txt%all$$the$$things one two three four five six seven eight nine ten eleven twelve%at>>once.log%" + L"}") + END_TEST_METHOD_PROPERTIES() + + // Get test parameters + String exeName; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"exeName", exeName)); + + String aliasName; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"aliasName", aliasName)); + + String originalString; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"originalString", originalString)); + + // Prepare internal alias structures + + // Convert WEX strings into the wstrings we will use to feed into the Alias structures + // and match to our expected values. + std::wstring alias(aliasName); + std::wstring exe(exeName); + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + DWORD linesExpected = _ReplacePercentWithCRLF(expected); + + std::wstring original(originalString); + + Alias::s_TestAddAlias(exe, alias, target); + + // Fill classic wchar_t[] buffer for interfacing with the MatchAndCopyAlias function + const USHORT bufferSize = 160ui16; + auto buffer = std::make_unique(bufferSize); + wcscpy_s(buffer.get(), bufferSize, original.data()); + + const size_t cbBuffer = bufferSize * sizeof(wchar_t); + size_t bufferUsed = 0; + DWORD linesActual = 0; + + // Run the match and copy function. + Alias::s_MatchAndCopyAliasLegacy(buffer.get(), + wcslen(buffer.get()) * sizeof(wchar_t), + buffer.get(), + cbBuffer, + bufferUsed, + exe, + linesActual); + + // Null terminate buffer for comparison + buffer[bufferUsed / sizeof(wchar_t)] = L'\0'; + + Log::Comment(String().Format(L"Expected: '%s'", expected.data())); + Log::Comment(String().Format(L"Actual : '%s'", buffer.get())); + + VERIFY_ARE_EQUAL(WEX::Common::String(expected.data()), WEX::Common::String(buffer.get())); + + VERIFY_ARE_EQUAL(linesExpected, linesActual); + } + + TEST_METHOD(TestMatchAndCopyTrailingCRLF) + { + PWSTR pwszSource = L"SourceWithoutCRLF\r\n"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 60; + auto rgwchTarget = std::make_unique(cchTarget); + const size_t cbTarget = cchTarget * sizeof(wchar_t); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtesttesttesttest"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + + size_t cbTargetUsed = 0; + auto const cbTargetUsedBefore = cbTargetUsed; + + DWORD dwLines = 0; + auto const dwLinesBefore = dwLines; + + // Register the wrong alias name before we try. + std::wstring exe(L"exe.exe"); + std::wstring sourceWithoutCRLF(L"SourceWithoutCRLF"); + std::wstring target(L"someTarget"); + Alias::s_TestAddAlias(exe, sourceWithoutCRLF, target); + + const auto targetExpected = target + L"\r\n"; + const size_t cbTargetExpected = targetExpected.size() * sizeof(wchar_t); // +2 for \r\n that will be added on replace. + + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + cbTarget, + cbTargetUsed, + exe, + dwLines); + + // Terminate target buffer with \0 for comparison + rgwchTarget[cbTargetUsed] = L'\0'; + + VERIFY_ARE_EQUAL(cbTargetExpected, cbTargetUsed, L"Target bytes should be filled with target size."); + VERIFY_ARE_EQUAL(String(targetExpected.data()), String(rgwchTarget.get(), gsl::narrow(cbTargetUsed / sizeof(wchar_t))), L"Target string should be filled with target data."); + VERIFY_ARE_EQUAL(1u, dwLines, L"Line count should be 1."); + } + + TEST_METHOD(TestMatchAndCopyInvalidExeName) + { + PWSTR pwszSource = L"Source"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 12; + auto rgwchTarget = std::make_unique(cchTarget); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + + const size_t cbTarget = cchTarget * sizeof(wchar_t); + size_t cbTargetUsed = cbTarget; + + DWORD dwLines = 0; + auto const dwLinesBefore = dwLines; + + std::wstring exeName; + + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + cbTarget, + cbTargetUsed, + exeName, + dwLines); + + VERIFY_ARE_EQUAL(cbTarget, cbTargetUsed, L"Byte count shouldn't have changed with failure."); + VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); + VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + } + + TEST_METHOD(TestMatchAndCopyExeNotFound) + { + PWSTR pwszSource = L"Source"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 12; + auto rgwchTarget = std::make_unique(cchTarget); + const size_t cbTarget = cchTarget * sizeof(wchar_t); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + size_t cbTargetUsed = 0; + auto const cbTargetUsedBefore = cbTargetUsed; + + std::wstring exeName(L"exe.exe"); + + DWORD dwLines = 0; + auto const dwLinesBefore = dwLines; + + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + cbTarget, + cbTargetUsed, + exeName, // we didn't pre-set-up the exe name + dwLines); + + VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should have been written."); + VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); + VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); + } + + TEST_METHOD(TestMatchAndCopyAliasNotFound) + { + PWSTR pwszSource = L"Source"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 12; + auto rgwchTarget = std::make_unique(cchTarget); + const size_t cbTarget = cchTarget * sizeof(wchar_t); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + + size_t cbTargetUsed = 0; + auto const cbTargetUsedBefore = cbTargetUsed; + + DWORD dwLines = 0; + auto const dwLinesBefore = dwLines; + + // Register the wrong alias name before we try. + std::wstring exe(L"exe.exe"); + std::wstring badSource(L"wrongSource"); + std::wstring target(L"someTarget"); + Alias::s_TestAddAlias(exe, badSource, target); + + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + cbTarget, + cbTargetUsed, + exe, + dwLines); + + VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should be used if nothing was found."); + VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); + VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); + } + + TEST_METHOD(TestMatchAndCopyTargetTooSmall) + { + PWSTR pwszSource = L"Source"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 12; + auto rgwchTarget = std::make_unique(cchTarget); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + + const size_t cbTarget = cchTarget * sizeof(wchar_t); + size_t cbTargetUsed = 0; + auto const cbTargetUsedBefore = cbTargetUsed; + + DWORD dwLines = 0; + auto const dwLinesBefore = dwLines; + + // Register the correct alias name before we try. + std::wstring exe(L"exe.exe"); + std::wstring source(pwszSource); + std::wstring target(L"someTarget"); + Alias::s_TestAddAlias(exe, source, target); + + + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + 1, // Make the target size too small + cbTargetUsed, + exe, + dwLines); + + VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"Byte count shouldn't have changed with failure."); + VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); + VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + } + + TEST_METHOD(TestMatchAndCopyLeadingSpaces) + { + PWSTR pwszSource = L" Source"; + const size_t cbSource = wcslen(pwszSource) * sizeof(wchar_t); + + const size_t cchTarget = 12; + auto rgwchTarget = std::make_unique(cchTarget); + const size_t cbTarget = cchTarget * sizeof(wchar_t); + wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); + auto rgwchTargetBefore = std::make_unique(cchTarget); + wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); + size_t cbTargetUsed = 0; + auto const cbTargetUsedExpected = cbTarget; + + DWORD dwLines = 0; + auto const dwLinesExpected = dwLines + 1; + + // Register the correct alias name before we try. + std::wstring exe(L"exe.exe"); + std::wstring source(L"Source"); + std::wstring target(L"someTarget"); + Alias::s_TestAddAlias(exe, source, target); + + std::wstring targetExpected = target + L"\r\n"; + + // We should be able to match through the leading spaces. They should be stripped. + Alias::s_MatchAndCopyAliasLegacy(pwszSource, + cbSource, + rgwchTarget.get(), + cbTarget, + cbTargetUsed, + exe, + dwLines); + + VERIFY_ARE_EQUAL(cbTargetUsedExpected, cbTargetUsed, L"No target bytes should be used."); + VERIFY_ARE_EQUAL(String(targetExpected.data(), gsl::narrow(targetExpected.size())), String(rgwchTarget.get(), cchTarget), L"Target string should match expected."); + VERIFY_ARE_EQUAL(dwLinesExpected, dwLines, L"Line count be updated to 1."); + } + + TEST_METHOD(TrimTrailing) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"bar%=bar," // The character % will be turned into an \r\n + L"bar=bar" + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + // Substitute %s from metadata into \r\n (since metadata can't hold \r\n) + _ReplacePercentWithCRLF(target); + _ReplacePercentWithCRLF(expected); + + Alias::s_TrimTrailingCrLf(target); + + VERIFY_ARE_EQUAL(String(expected.data()), String(target.data())); + } + + TEST_METHOD(Tokenize) + { + std::wstring tokenStr(L"one two three"); + std::deque tokensExpected; + tokensExpected.emplace_back(L"one"); + tokensExpected.emplace_back(L"two"); + tokensExpected.emplace_back(L"three"); + + auto tokensActual = Alias::s_Tokenize(tokenStr); + + VERIFY_ARE_EQUAL(tokensExpected.size(), tokensActual.size()); + + for (size_t i = 0; i < tokensExpected.size(); i++) + { + VERIFY_ARE_EQUAL(String(tokensExpected[i].data()), String(tokensActual[i].data())); + } + } + + TEST_METHOD(TokenizeNothing) + { + std::wstring tokenStr(L"alias"); + std::deque tokensExpected; + tokensExpected.emplace_back(tokenStr); + + auto tokensActual = Alias::s_Tokenize(tokenStr); + + VERIFY_ARE_EQUAL(tokensExpected.size(), tokensActual.size()); + + for (size_t i = 0; i < tokensExpected.size(); i++) + { + VERIFY_ARE_EQUAL(String(tokensExpected[i].data()), String(tokensActual[i].data())); + } + } + + TEST_METHOD(GetArgString) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"alias arg1 arg2 arg3=arg1 arg2 arg3," + L"aliasOnly=" + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + std::wstring actual = Alias::s_GetArgString(target); + + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(NumberedArgMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"1=one," + L"2=two," + L"3=three," + L"4=four," + L"5=five," + L"6=six," + L"7=seven," + L"8=eight," + L"9=nine," + L"A=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + std::deque tokens; + tokens.emplace_back(L"alias"); + tokens.emplace_back(L"one"); + tokens.emplace_back(L"two"); + tokens.emplace_back(L"three"); + tokens.emplace_back(L"four"); + tokens.emplace_back(L"five"); + tokens.emplace_back(L"six"); + tokens.emplace_back(L"seven"); + tokens.emplace_back(L"eight"); + tokens.emplace_back(L"nine"); + tokens.emplace_back(L"ten"); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + const bool returnActual = Alias::s_TryReplaceNumberedArgMacro(target[0], actual, tokens); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(WildcardArgMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"*=one two three," + L"A=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + std::wstring fullArgString(L"one two three"); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + const bool returnActual = Alias::s_TryReplaceWildcardArgMacro(target[0], actual, fullArgString); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(InputRedirMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"L=<," + L"l=<," + L"A=," + L"a=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + const bool returnActual = Alias::s_TryReplaceInputRedirMacro(target[0], actual); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(OutputRedirMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"G=>," + L"g=>," + L"A=," + L"a=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + const bool returnActual = Alias::s_TryReplaceOutputRedirMacro(target[0], actual); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(PipeRedirMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"B=|," + L"b=|," + L"A=," + L"a=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + const bool returnActual = Alias::s_TryReplacePipeRedirMacro(target[0], actual); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + } + + TEST_METHOD(NextCommandMacro) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", L"{" + L"T=%," + L"t=%," + L"A=," + L"a=," + L"0=," + L"}") + END_TEST_METHOD_PROPERTIES() + + std::wstring target; + std::wstring expected; + _RetrieveTargetExpectedPair(target, expected); + + _ReplacePercentWithCRLF(expected); + + // if we expect non-empty results, then we should get a bool back saying it was processed + const bool returnExpected = !expected.empty(); + + std::wstring actual; + size_t lineCountActual = 0; + + const auto lineCountExpected = lineCountActual + (returnExpected ? 1 : 0); + + const bool returnActual = Alias::s_TryReplaceNextCommandMacro(target[0], actual, lineCountActual); + + VERIFY_ARE_EQUAL(returnExpected, returnActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + VERIFY_ARE_EQUAL(lineCountExpected, lineCountActual); + } + + TEST_METHOD(AppendCrLf) + { + std::wstring actual; + size_t lineCountActual = 0; + + const std::wstring expected(L"\r\n"); + const auto lineCountExpected = lineCountActual + 1; + + Alias::s_AppendCrLf(actual, lineCountActual); + VERIFY_ARE_EQUAL(String(expected.data()), String(actual.data())); + VERIFY_ARE_EQUAL(lineCountExpected, lineCountActual); + } +}; diff --git a/src/host/ut_host/ApiRoutinesTests.cpp b/src/host/ut_host/ApiRoutinesTests.cpp new file mode 100644 index 000000000..c12fba0c8 --- /dev/null +++ b/src/host/ut_host/ApiRoutinesTests.cpp @@ -0,0 +1,857 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "ApiRoutines.h" +#include "getset.h" +#include "dbcs.h" +#include "misc.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Types; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class ApiRoutinesTests +{ + TEST_CLASS(ApiRoutinesTests); + + std::unique_ptr m_state; + + ApiRoutines _Routines; + IApiRoutines* _pApiRoutines = &_Routines; + + TEST_METHOD_SETUP(MethodSetup) + { + m_state = std::make_unique(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + m_state->PrepareGlobalInputBuffer(); + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupGlobalInputBuffer(); + + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + m_state.reset(nullptr); + + return true; + } + + BOOL _fPrevInsertMode; + void PrepVerifySetConsoleInputModeImpl(const ULONG ulOriginalInputMode) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.Flags = 0; + gci.pInputBuffer->InputMode = ulOriginalInputMode & ~(ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS); + gci.SetInsertMode(WI_IsFlagSet(ulOriginalInputMode, ENABLE_INSERT_MODE)); + WI_UpdateFlag(gci.Flags, CONSOLE_QUICK_EDIT_MODE, WI_IsFlagSet(ulOriginalInputMode, ENABLE_QUICK_EDIT_MODE)); + WI_UpdateFlag(gci.Flags, CONSOLE_AUTO_POSITION, WI_IsFlagSet(ulOriginalInputMode, ENABLE_AUTO_POSITION)); + + // Set cursor DB to on so we can verify that it turned off when the Insert Mode changes. + gci.GetActiveOutputBuffer().SetCursorDBMode(true); + + // Record the insert mode at this time to see if it changed. + _fPrevInsertMode = gci.GetInsertMode(); + } + + void VerifySetConsoleInputModeImpl(const HRESULT hrExpected, + const ULONG ulNewMode) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + InputBuffer* const pii = gci.pInputBuffer; + + // The expected mode set in the buffer is the mode given minus the flags that are stored in different fields. + ULONG ulModeExpected = ulNewMode; + WI_ClearAllFlags(ulModeExpected, (ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION | ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS)); + bool const fQuickEditExpected = WI_IsFlagSet(ulNewMode, ENABLE_QUICK_EDIT_MODE); + bool const fAutoPositionExpected = WI_IsFlagSet(ulNewMode, ENABLE_AUTO_POSITION); + bool const fInsertModeExpected = WI_IsFlagSet(ulNewMode, ENABLE_INSERT_MODE); + + // If the insert mode changed, we expect the cursor to have turned off. + bool const fCursorDBModeExpected = ((!!_fPrevInsertMode) == fInsertModeExpected); + + // Call the API + HRESULT const hrActual = _pApiRoutines->SetConsoleInputModeImpl(*pii, ulNewMode); + + // Now do verifications of final state. + VERIFY_ARE_EQUAL(hrExpected, hrActual); + VERIFY_ARE_EQUAL(ulModeExpected, pii->InputMode); + VERIFY_ARE_EQUAL(fQuickEditExpected, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE)); + VERIFY_ARE_EQUAL(fAutoPositionExpected, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION)); + VERIFY_ARE_EQUAL(!!fInsertModeExpected, !!gci.GetInsertMode()); + VERIFY_ARE_EQUAL(fCursorDBModeExpected, gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().IsDouble()); + } + + TEST_METHOD(ApiSetConsoleInputModeImplValidNonExtended) + { + Log::Comment(L"Set some perfectly valid, non-extended flags."); + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Success code should result from setting valid flags."); + Log::Comment(L"Flags should be set exactly as given."); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT); + } + + TEST_METHOD(ApiSetConsoleInputModeImplValidExtended) + { + Log::Comment(L"Set some perfectly valid, extended flags."); + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Success code should result from setting valid flags."); + Log::Comment(L"Flags should be set exactly as given."); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION); + } + + TEST_METHOD(ApiSetConsoleInputModeImplExtendedTurnOff) + { + Log::Comment(L"Try to turn off extended flags."); + PrepVerifySetConsoleInputModeImpl(ENABLE_EXTENDED_FLAGS | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION); + Log::Comment(L"Success code should result from setting valid flags."); + Log::Comment(L"Flags should be set exactly as given."); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS); + } + + TEST_METHOD(ApiSetConsoleInputModeImplInvalid) + { + Log::Comment(L"Set some invalid flags."); + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Should get invalid argument code because we set invalid flags."); + Log::Comment(L"Flags should be set anyway despite invalid code."); + VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x8000000); + } + + TEST_METHOD(ApiSetConsoleInputModeImplInsertNoCookedRead) + { + Log::Comment(L"Turn on insert mode without cooked read data."); + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Success code should result from setting valid flags."); + Log::Comment(L"Flags should be set exactly as given."); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE); + Log::Comment(L"Turn back off and verify."); + PrepVerifySetConsoleInputModeImpl(0); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS); + } + + TEST_METHOD(ApiSetConsoleInputModeImplInsertCookedRead) + { + Log::Comment(L"Turn on insert mode with cooked read data."); + m_state->PrepareReadHandle(); + auto cleanupReadHandle = wil::scope_exit([&](){ m_state->CleanupReadHandle(); }); + m_state->PrepareCookedReadData(); + auto cleanupCookedRead = wil::scope_exit([&](){ m_state->CleanupCookedReadData(); }); + + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Success code should result from setting valid flags."); + Log::Comment(L"Flags should be set exactly as given."); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS | ENABLE_INSERT_MODE); + Log::Comment(L"Turn back off and verify."); + PrepVerifySetConsoleInputModeImpl(0); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_EXTENDED_FLAGS); + } + + TEST_METHOD(ApiSetConsoleInputModeImplEchoOnLineOff) + { + Log::Comment(L"Set ECHO on with LINE off. It's invalid, but it should get set anyway and return an error code."); + PrepVerifySetConsoleInputModeImpl(0); + Log::Comment(L"Setting ECHO without LINE should return an invalid argument code."); + Log::Comment(L"Input mode should be set anyway despite FAILED return code."); + VerifySetConsoleInputModeImpl(E_INVALIDARG, ENABLE_ECHO_INPUT); + } + + TEST_METHOD(ApiSetConsoleInputModeExtendedFlagBehaviors) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + Log::Comment(L"Verify that we can set various extended flags even without the ENABLE_EXTENDED_FLAGS flag."); + PrepVerifySetConsoleInputModeImpl(0); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_INSERT_MODE); + PrepVerifySetConsoleInputModeImpl(0); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_QUICK_EDIT_MODE); + PrepVerifySetConsoleInputModeImpl(0); + VerifySetConsoleInputModeImpl(S_OK, ENABLE_AUTO_POSITION); + + Log::Comment(L"Verify that we cannot unset various extended flags without the ENABLE_EXTENDED_FLAGS flag."); + PrepVerifySetConsoleInputModeImpl(ENABLE_INSERT_MODE | ENABLE_QUICK_EDIT_MODE | ENABLE_AUTO_POSITION); + InputBuffer* const pii = gci.pInputBuffer; + HRESULT const hr = _pApiRoutines->SetConsoleInputModeImpl(*pii, 0); + + VERIFY_ARE_EQUAL(S_OK, hr); + VERIFY_ARE_EQUAL(true, !!gci.GetInsertMode()); + VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_QUICK_EDIT_MODE)); + VERIFY_ARE_EQUAL(true, WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION)); + } + + TEST_METHOD(ApiSetConsoleInputModeImplPSReadlineScenario) + { + Log::Comment(L"Set Powershell PSReadline expected modes."); + PrepVerifySetConsoleInputModeImpl(0x1F7); + Log::Comment(L"Should return an invalid argument code because ECHO is set without LINE."); + Log::Comment(L"Input mode should be set anyway despite FAILED return code."); + VerifySetConsoleInputModeImpl(E_INVALIDARG, 0x1E4); + } + + TEST_METHOD(ApiGetConsoleTitleA) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetTitle(L"Test window title."); + + int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP, + 0, + gci.GetTitle().c_str(), + -1, + nullptr, + 0, + nullptr, + nullptr); + + wistd::unique_ptr pszExpected = wil::make_unique_nothrow(iBytesNeeded); + VERIFY_IS_NOT_NULL(pszExpected); + + VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP, + 0, + gci.GetTitle().c_str(), + -1, + pszExpected.get(), + iBytesNeeded, + nullptr, + nullptr)); + + char pszTitle[MAX_PATH]; // most applications use MAX_PATH + size_t cchWritten = 0; + size_t cchNeeded = 0; + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleAImpl(gsl::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); + + VERIFY_ARE_NOT_EQUAL(0u, cchWritten); + // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). + VERIFY_ARE_EQUAL(gci.GetTitle().length() + 1, cchWritten); + VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchNeeded); + VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle)); + } + + TEST_METHOD(ApiGetConsoleTitleW) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetTitle(L"Test window title."); + + wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH + size_t cchWritten = 0; + size_t cchNeeded = 0; + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleTitleWImpl(gsl::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); + + VERIFY_ARE_NOT_EQUAL(0u, cchWritten); + // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). + VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchWritten); + VERIFY_ARE_EQUAL(gci.GetTitle().length(), cchNeeded); + VERIFY_ARE_EQUAL(WEX::Common::String(gci.GetTitle().c_str()), WEX::Common::String(pwszTitle)); + } + + TEST_METHOD(ApiGetConsoleOriginalTitleA) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetOriginalTitle(L"Test original window title."); + + int const iBytesNeeded = WideCharToMultiByte(gci.OutputCP, + 0, + gci.GetOriginalTitle().c_str(), + -1, + nullptr, + 0, + nullptr, + nullptr); + + wistd::unique_ptr pszExpected = wil::make_unique_nothrow(iBytesNeeded); + VERIFY_IS_NOT_NULL(pszExpected); + + VERIFY_WIN32_BOOL_SUCCEEDED(WideCharToMultiByte(gci.OutputCP, + 0, + gci.GetOriginalTitle().c_str(), + -1, + pszExpected.get(), + iBytesNeeded, + nullptr, + nullptr)); + + char pszTitle[MAX_PATH]; // most applications use MAX_PATH + size_t cchWritten = 0; + size_t cchNeeded = 0; + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleAImpl(gsl::span(pszTitle, ARRAYSIZE(pszTitle)), cchWritten, cchNeeded)); + + VERIFY_ARE_NOT_EQUAL(0u, cchWritten); + // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). + VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length() + 1, cchWritten); + VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchNeeded); + VERIFY_ARE_EQUAL(WEX::Common::String(pszExpected.get()), WEX::Common::String(pszTitle)); + } + + TEST_METHOD(ApiGetConsoleOriginalTitleW) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetOriginalTitle(L"Test original window title."); + + wchar_t pwszTitle[MAX_PATH]; // most applications use MAX_PATH + size_t cchWritten = 0; + size_t cchNeeded = 0; + VERIFY_SUCCEEDED(_pApiRoutines->GetConsoleOriginalTitleWImpl(gsl::span(pwszTitle, ARRAYSIZE(pwszTitle)), cchWritten, cchNeeded)); + + VERIFY_ARE_NOT_EQUAL(0u, cchWritten); + // NOTE: W version of API returns string length. A version of API returns buffer length (string + null). + VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchWritten); + VERIFY_ARE_EQUAL(gci.GetOriginalTitle().length(), cchNeeded); + VERIFY_ARE_EQUAL(WEX::Common::String(gci.GetOriginalTitle().c_str()), WEX::Common::String(pwszTitle)); + } + + static void s_AdjustOutputWait(const bool fShouldBlock) + { + WI_SetFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, fShouldBlock); + WI_ClearFlagIf(ServiceLocator::LocateGlobals().getConsoleInformation().Flags, CONSOLE_SELECTING, !fShouldBlock); + } + + TEST_METHOD(ApiWriteConsoleA) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:dwCodePage", L"{437, 932, 65001}") + TEST_METHOD_PROPERTY(L"Data:dwIncrement", L"{0, 1, 2}") + END_TEST_METHOD_PROPERTIES(); + + bool fInduceWait; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state."); + + DWORD dwCodePage; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwCodePage", dwCodePage), L"Get the codepage for the test. Check a single byte, a double byte, and UTF-8."); + + DWORD dwIncrement; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dwIncrement", dwIncrement), + L"Get how many chars we should feed in at a time. This validates lead bytes and bytes held across calls."); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + // Ensure global state is updated for our codepage. + gci.OutputCP = dwCodePage; + SetConsoleCPInfo(TRUE); + + PCSTR pszTestText; + switch (dwCodePage) + { + case CP_USA: // US English ANSI + pszTestText = "Test Text"; + break; + case CP_JAPANESE: // Japanese Shift-JIS + pszTestText = "J\x82\xa0\x82\xa2"; + break; + case CP_UTF8: + pszTestText = "Test \xe3\x82\xab Text"; + break; + default: + VERIFY_FAIL(L"Test is not ready for this codepage."); + return; + } + size_t cchTestText = strlen(pszTestText); + + // Set our increment value for the loop. + // 0 represents the special case of feeding the whole string in at at time. + // Otherwise, we try different segment sizes to ensure preservation across calls + // for appropriate handling of DBCS and UTF-8 sequences. + const size_t cchIncrement = dwIncrement == 0 ? cchTestText : dwIncrement; + + for (size_t i = 0; i < cchTestText; i += cchIncrement) + { + Log::Comment(WEX::Common::String().Format(L"Iteration %d of loop with increment %d", i, cchIncrement)); + if (fInduceWait) + { + Log::Comment(L"Blocking global output state to induce waits."); + s_AdjustOutputWait(true); + } + + size_t cchRead = 0; + std::unique_ptr waiter; + + // The increment is either the specified length or the remaining text in the string (if that is smaller). + const size_t cchWriteLength = std::min(cchIncrement, cchTestText - i); + + // Run the test method + const HRESULT hr = _pApiRoutines->WriteConsoleAImpl(si, { pszTestText + i, cchWriteLength }, cchRead, waiter); + + VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing."); + if (!fInduceWait) + { + VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case."); + VERIFY_ARE_EQUAL(cchWriteLength, cchRead, L"We should have the same character count back as 'written' that we gave in."); + } + else + { + VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case."); + // The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait. + + Log::Comment(L"Unblocking global output state so the wait can be serviced."); + s_AdjustOutputWait(false); + Log::Comment(L"Dispatching the wait."); + NTSTATUS Status = STATUS_SUCCESS; + size_t dwNumBytes = 0; + DWORD dwControlKeyState = 0; // unused but matches the pattern for read. + void* pOutputData = nullptr; // unused for writes but used for read. + const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, FALSE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData); + + VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful."); + VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller."); + + const size_t dwBytesExpected = cchWriteLength; + VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value."); + } + } + } + + TEST_METHOD(ApiWriteConsoleW) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:fInduceWait", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + + bool fInduceWait; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"fInduceWait", fInduceWait), L"Get whether or not we should exercise this function off a wait state."); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + const std::wstring testText(L"Test text"); + + if (fInduceWait) + { + Log::Comment(L"Blocking global output state to induce waits."); + s_AdjustOutputWait(true); + } + + size_t cchRead = 0; + std::unique_ptr waiter; + const HRESULT hr = _pApiRoutines->WriteConsoleWImpl(si, testText, cchRead, waiter); + + VERIFY_ARE_EQUAL(S_OK, hr, L"Successful result code from writing."); + if (!fInduceWait) + { + VERIFY_IS_NULL(waiter.get(), L"We should have no waiter for this case."); + VERIFY_ARE_EQUAL(testText.size(), cchRead, L"We should have the same character count back as 'written' that we gave in."); + } + else + { + VERIFY_IS_NOT_NULL(waiter.get(), L"We should have a waiter for this case."); + // The cchRead is irrelevant at this point as it's not going to be returned until we're off the wait. + + Log::Comment(L"Unblocking global output state so the wait can be serviced."); + s_AdjustOutputWait(false); + Log::Comment(L"Dispatching the wait."); + NTSTATUS Status = STATUS_SUCCESS; + size_t dwNumBytes = 0; + DWORD dwControlKeyState = 0; // unused but matches the pattern for read. + void* pOutputData = nullptr; // unused for writes but used for read. + const BOOL bNotifyResult = waiter->Notify(WaitTerminationReason::NoReason, TRUE, &Status, &dwNumBytes, &dwControlKeyState, &pOutputData); + + VERIFY_IS_TRUE(!!bNotifyResult, L"Wait completion on notify should be successful."); + VERIFY_ARE_EQUAL(STATUS_SUCCESS, Status, L"We should have a successful return code to pass to the caller."); + + const size_t dwBytesExpected = testText.size() * sizeof(wchar_t); + VERIFY_ARE_EQUAL(dwBytesExpected, dwNumBytes, L"We should have the byte length of the string we put in as the returned value."); + } + } + + void ValidateScreen(SCREEN_INFORMATION& si, + const CHAR_INFO background, + const CHAR_INFO fill, + const COORD delta, + const std::optional clip) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& activeSi = si.GetActiveBuffer(); + auto bufferSize = activeSi.GetBufferSize(); + + // Find the background area viewport by taking the size, translating it by the delta, then cropping it back to the buffer size. + Viewport backgroundArea = Viewport::Offset(bufferSize, delta); + bufferSize.Clamp(backgroundArea); + + auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner. + + while (it) + { + if (backgroundArea.IsInBounds(it._pos) || + (clip.has_value() && !clip.value().IsInBounds(it._pos))) + { + auto cellInfo = gci.AsCharInfo(*it); + VERIFY_ARE_EQUAL(background, cellInfo); + } + else + { + VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it)); + } + it++; + } + } + + void ValidateComplexScreen(SCREEN_INFORMATION& si, + const CHAR_INFO background, + const CHAR_INFO fill, + const CHAR_INFO scroll, + const Viewport scrollArea, + const COORD destPoint, + const std::optional clip) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& activeSi = si.GetActiveBuffer(); + auto bufferSize = activeSi.GetBufferSize(); + + // Find the delta by comparing the scroll area to the destination point + COORD delta; + delta.X = destPoint.X - scrollArea.Left(); + delta.Y = destPoint.Y - scrollArea.Top(); + + // Find the area where the scroll text should have gone by taking the scrolled area by the delta + Viewport scrolledDestination = Viewport::Offset(scrollArea, delta); + bufferSize.Clamp(scrolledDestination); + + auto it = activeSi.GetCellDataAt({ 0, 0 }); // We're going to walk the whole thing. Start in the top left corner. + + while (it) + { + // If there's no clip rectangle... + if (!clip.has_value()) + { + // Three states. + // 1. We filled the background with something (background CHAR_INFO) + // 2. We filled another smaller area with a different something (scroll CHAR_INFO) + // 3. We moved #2 by delta and the uncovered area was filled with a third something (fill CHAR_INFO) + + // If it's in the scrolled destination, it's the value that just got moved. + if (scrolledDestination.IsInBounds(it._pos)) + { + VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it)); + } + // Otherwise, if it's not in the destination but it was in the source, assume it got filled in. + else if (scrollArea.IsInBounds(it._pos)) + { + VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it)); + } + // Lastly if it's not in either spot, it should have our background CHAR_INFO + else + { + VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it)); + } + } + // If there is a clip rectangle... + else + { + const auto unboxedClip = clip.value(); + + if (unboxedClip.IsInBounds(it._pos)) + { + if (scrolledDestination.IsInBounds(it._pos)) + { + VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it)); + } + else if (scrollArea.IsInBounds(it._pos)) + { + VERIFY_ARE_EQUAL(fill, gci.AsCharInfo(*it)); + } + else + { + VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it)); + } + } + else + { + if (scrollArea.IsInBounds(it._pos)) + { + VERIFY_ARE_EQUAL(scroll, gci.AsCharInfo(*it)); + } + else + { + VERIFY_ARE_EQUAL(background, gci.AsCharInfo(*it)); + } + } + } + + // Move to next iterator position and check. + it++; + } + } + + TEST_METHOD(ApiScrollConsoleScreenBufferW) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"data:checkClipped", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + + bool checkClipped; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"checkClipped", checkClipped), L"Get whether or not we should check all the options using a clipping rectangle."); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional({ 5, 5 }), L"Make the buffer small so this doesn't take forever."); + + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + CHAR_INFO fill; + fill.Char.UnicodeChar = L'A'; + fill.Attributes = FOREGROUND_RED; + + // By default, we're going to use a nullopt clip rectangle. + // If this instance of the test is checking clipping, we'll assign a clip value + // prior to each call variation. + std::optional clipRectangle = std::nullopt; + std::optional clipViewport = std::nullopt; + const auto bufferSize = si.GetBufferSize(); + + SMALL_RECT scroll = bufferSize.ToInclusive(); + COORD destination{ 0, -2 }; // scroll up. + + Log::Comment(L"Fill screen with green Zs. Scroll all up by two, backfilling with red As. Confirm every cell."); + si.GetActiveBuffer().ClearTextData(); // Clean out screen + + CHAR_INFO background; + background.Char.UnicodeChar = L'Z'; + background.Attributes = FOREGROUND_GREEN; + + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + if (checkClipped) + { + // for scrolling up and down, we're going to clip to only modify the left half of the buffer + COORD clipRectDimensions = bufferSize.Dimensions(); + clipRectDimensions.X /= 2; + + clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); + clipRectangle = clipViewport.value().ToInclusive(); + } + + // Scroll everything up and backfill with red As. + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Fill screen with green Zs. Scroll all down by two, backfilling with red As. Confirm every cell."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything down and backfill with red As. + destination = { 0, 2 }; + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + if (checkClipped) + { + // for scrolling left and right, we're going to clip to only modify the top half of the buffer + COORD clipRectDimensions = bufferSize.Dimensions(); + clipRectDimensions.Y /= 2; + + clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); + clipRectangle = clipViewport.value().ToInclusive(); + } + + Log::Comment(L"Fill screen with green Zs. Scroll all left by two, backfilling with red As. Confirm every cell."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything left and backfill with red As. + destination = { -2, 0 }; + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Fill screen with green Zs. Scroll all right by two, backfilling with red As. Confirm every cell."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything right and backfill with red As. + destination = { 2, 0 }; + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Fill screen with green Zs. Move everything down and right by two, backfilling with red As. Confirm every cell."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything down and right and backfill with red As. + destination = { 2, 2 }; + if (checkClipped) + { + // Clip out the left most and top most column. + clipViewport = Viewport::FromDimensions({ 1, 1 }, { 4, 4 }); + clipRectangle = clipViewport.value().ToInclusive(); + } + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Fill screen with green Zs. Move everything up and left by two, backfilling with red As. Confirm every cell."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything up and left and backfill with red As. + destination = { -2, -2 }; + if (checkClipped) + { + // Clip out the bottom most and right most column + clipViewport = Viewport::FromDimensions({ 0, 0 }, { 4, 4 }); + clipRectangle = clipViewport.value().ToInclusive(); + } + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Scroll everything completely off the screen."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Scroll everything way off the screen. + destination = { 0, -10 }; + if (checkClipped) + { + // for scrolling up and down, we're going to clip to only modify the left half of the buffer + COORD clipRectDimensions = bufferSize.Dimensions(); + clipRectDimensions.X /= 2; + + clipViewport = Viewport::FromDimensions({ 0, 0 }, clipRectDimensions); + clipRectangle = clipViewport.value().ToInclusive(); + } + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + ValidateScreen(si, background, fill, destination, clipViewport); + + Log::Comment(L"Scroll everything completely off the screen but use a null fill and confirm it is replaced with default attribute spaces."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + // Scroll everything way off the screen. + destination = { -10, -10 }; + + CHAR_INFO nullFill = { 0 }; + + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, nullFill.Char.UnicodeChar, nullFill.Attributes)); + + CHAR_INFO fillExpected; + fillExpected.Char.UnicodeChar = UNICODE_SPACE; + fillExpected.Attributes = si.GetAttributes().GetLegacyAttributes(); + ValidateScreen(si, background, fillExpected, destination, clipViewport); + + if (checkClipped) + { + // If we're doing clipping here, we're going to clip the scrolled area (after Bs are filled onto field of Zs) + // to only the 3rd and 4th columns of the pattern. + clipViewport = Viewport::FromDimensions({ 2, 0 }, { 2, 5 }); + clipRectangle = clipViewport.value().ToInclusive(); + } + + Log::Comment(L"Scroll a small portion of the screen in an overlapping fashion."); + scroll.Top = 1; + scroll.Bottom = 2; + scroll.Left = 1; + scroll.Right = 2; + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Screen now looks like: + // ZZZZZ + // ZZZZZ + // ZZZZZ + // ZZZZZ + // ZZZZZ + + // Fill the scroll rectangle with Blue Bs. + CHAR_INFO scrollRect; + scrollRect.Char.UnicodeChar = L'B'; + scrollRect.Attributes = FOREGROUND_BLUE; + si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll)); + + // Screen now looks like: + // ZZZZZ + // ZBBZZ + // ZBBZZ + // ZZZZZ + // ZZZZZ + + // We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right just one. + destination = { scroll.Left + 1, scroll.Top + 1 }; + + // Move rectangle and backfill with red As. + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + + // Screen should now look like either: + // (with no clip rectangle): + // ZZZZZ + // ZAAZZ + // ZABBZ + // ZZBBZ + // ZZZZZ + // or with clip rectangle (of 3rd and 4th columns only, defined above) + // ZZZZZ + // ZBAZZ + // ZBBBZ + // ZZBBZ + // ZZZZZ + + ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport); + + Log::Comment(L"Scroll a small portion of the screen in a non-overlapping fashion."); + + si.GetActiveBuffer().ClearTextData(); // Clean out screen + si.GetActiveBuffer().Write(OutputCellIterator(background), { 0, 0 }); // Fill entire screen with green Zs. + + // Screen now looks like: + // ZZZZZ + // ZZZZZ + // ZZZZZ + // ZZZZZ + // ZZZZZ + + // Fill the scroll rectangle with Blue Bs. + si.GetActiveBuffer().WriteRect(OutputCellIterator(scrollRect), Viewport::FromInclusive(scroll)); + + // Screen now looks like: + // ZZZZZ + // ZBBZZ + // ZBBZZ + // ZZZZZ + // ZZZZZ + + // We're going to move our little embedded rectangle of Blue Bs inside the field of Green Zs down and to the right by two. + destination = { scroll.Left + 2, scroll.Top + 2 }; + + // Move rectangle and backfill with red As. + VERIFY_SUCCEEDED(_pApiRoutines->ScrollConsoleScreenBufferWImpl(si, scroll, destination, clipRectangle, fill.Char.UnicodeChar, fill.Attributes)); + + // Screen should now look like either: + // (with no clip rectangle): + // ZZZZZ + // ZAAZZ + // ZAAZZ + // ZZZBB + // ZZZBB + // or with clip rectangle (of 3rd and 4th columns only, defined above) + // ZZZZZ + // ZBAZZ + // ZBAZZ + // ZZZBZ + // ZZZBZ + + ValidateComplexScreen(si, background, fill, scrollRect, Viewport::FromInclusive(scroll), destination, clipViewport); + } +}; diff --git a/src/host/ut_host/AttrRowTests.cpp b/src/host/ut_host/AttrRowTests.cpp new file mode 100644 index 000000000..7a1881efe --- /dev/null +++ b/src/host/ut_host/AttrRowTests.cpp @@ -0,0 +1,607 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" +#include "../buffer/out/textBuffer.hpp" + +#include "input.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace WEX { + namespace TestExecution { + template<> + class VerifyOutputTraits + { + public: + static WEX::Common::NoThrowString ToString(const TextAttributeRun& tar) + { + return WEX::Common::NoThrowString().Format( + L"Length:%d, attr:%s", + tar.GetLength(), + VerifyOutputTraits::ToString(tar.GetAttributes()).GetBuffer() + ); + } + }; + + template<> + class VerifyCompareTraits + { + public: + static bool AreEqual(const TextAttributeRun& expected, const TextAttributeRun& actual) + { + return expected.GetAttributes() == actual.GetAttributes() && + expected.GetLength() == actual.GetLength(); + } + + static bool AreSame(const TextAttributeRun& expected, const TextAttributeRun& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const TextAttributeRun&, const TextAttributeRun&) = delete; + + static bool IsGreaterThan(const TextAttributeRun&, const TextAttributeRun&) = delete; + + static bool IsNull(const TextAttributeRun& object) + { + return object.GetAttributes().IsLegacy() && object.GetAttributes().GetLegacyAttributes() == 0 && + object.GetLength() == 0; + } + }; + } +} + +class AttrRowTests +{ + ATTR_ROW* pSingle; + ATTR_ROW* pChain; + + short _sDefaultLength = 80; + short _sDefaultChainLength = 6; + + short sChainSegLength; + short sChainLeftover; + short sChainSegmentsNeeded; + + WORD __wDefaultAttr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + WORD __wDefaultChainAttr = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY; + TextAttribute _DefaultAttr = TextAttribute(__wDefaultAttr); + TextAttribute _DefaultChainAttr = TextAttribute(__wDefaultChainAttr); + + TEST_CLASS(AttrRowTests); + + TEST_METHOD_SETUP(MethodSetup) + { + pSingle = new ATTR_ROW(_sDefaultLength, _DefaultAttr); + + // Segment length is the expected length divided by the row length + // E.g. row of 80, 4 segments, 20 segment length each + sChainSegLength = _sDefaultLength / _sDefaultChainLength; + + // Leftover is spaces that don't fit evenly + // E.g. row of 81, 4 segments, 1 leftover length segment + sChainLeftover = _sDefaultLength % _sDefaultChainLength; + + // Start with the number of segments we expect + sChainSegmentsNeeded = _sDefaultChainLength; + + // If we had a remainder, add one more segment + if (sChainLeftover) + { + sChainSegmentsNeeded++; + } + + // Create the chain + pChain = new ATTR_ROW(_sDefaultLength, _DefaultAttr); + pChain->_list.resize(sChainSegmentsNeeded); + + // Attach all chain segments that are even multiples of the row length + for (short iChain = 0; iChain < _sDefaultChainLength; iChain++) + { + TextAttributeRun* pRun = &pChain->_list[iChain]; + + pRun->SetAttributesFromLegacy(iChain); // Just use the chain position as the value + pRun->SetLength(sChainSegLength); + } + + if (sChainLeftover > 0) + { + // If we had a leftover, then this chain is one longer than we expected (the default length) + // So use it as the index (because indicies start at 0) + TextAttributeRun* pRun = &pChain->_list[_sDefaultChainLength]; + + pRun->SetAttributes(_DefaultChainAttr); + pRun->SetLength(sChainLeftover); + } + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + delete pSingle; + + delete pChain; + + return true; + } + + TEST_METHOD(TestInitialize) + { + // Properties needed for test + const WORD wAttr = FOREGROUND_RED | BACKGROUND_BLUE; + TextAttribute attr = TextAttribute(wAttr); + // Cases to test + ATTR_ROW* pTestItems[]{ pSingle, pChain }; + + // Loop cases + for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) + { + ATTR_ROW* pUnderTest = pTestItems[iIndex]; + + pUnderTest->Reset(attr); + + VERIFY_ARE_EQUAL(pUnderTest->_list.size(), 1u); + VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetAttributes(), attr); + VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetLength(), (unsigned int)_sDefaultLength); + } + } + + // Routine Description: + // - Packs an array of words representing attributes into the more compact storage form used by the row. + // Arguments: + // - rgAttrs - Array of words representing the attribute associated with each character position in the row. + // - cRowLength - Length of preceeding array. + // - outAttrRun - reference to unique_ptr that will contain packed attr run on success. + // Return Value: + // - Success if success. Buffer too small if row length is incorrect. + HRESULT PackAttrs(_In_reads_(cRowLength) const TextAttribute* const rgAttrs, + const size_t cRowLength, + _Inout_ std::unique_ptr& outAttrRun, + _Out_ size_t* const cOutAttrRun) + { + NTSTATUS status = STATUS_SUCCESS; + + if (cRowLength == 0) + { + status = STATUS_BUFFER_TOO_SMALL; + } + + if (NT_SUCCESS(status)) + { + // first count up the deltas in the array + size_t cDeltas = 1; + + const TextAttribute* pPrevAttr = &rgAttrs[0]; + + for (size_t i = 1; i < cRowLength; i++) + { + const TextAttribute* pCurAttr = &rgAttrs[i]; + + if (*pCurAttr != *pPrevAttr) + { + cDeltas++; + } + + pPrevAttr = pCurAttr; + } + + // This whole situation was too complicated with a one off holder for one row run + // new method: + // delete the old buffer + // make a new buffer, one run + one run for each change + // set the values for each run one run index at a time + + std::unique_ptr attrRun = std::make_unique(cDeltas); + status = NT_TESTNULL(attrRun.get()); + if (NT_SUCCESS(status)) + { + TextAttributeRun* pCurrentRun = attrRun.get(); + pCurrentRun->SetAttributes(rgAttrs[0]); + pCurrentRun->SetLength(1); + for (size_t i = 1; i < cRowLength; i++) + { + if (pCurrentRun->GetAttributes() == rgAttrs[i]) + { + pCurrentRun->SetLength(pCurrentRun->GetLength() + 1); + } + else + { + pCurrentRun++; + pCurrentRun->SetAttributes(rgAttrs[i]); + pCurrentRun->SetLength(1); + } + } + attrRun.swap(outAttrRun); + *cOutAttrRun = cDeltas; + } + } + + return HRESULT_FROM_NT(status); + } + + NoThrowString LogRunElement(_In_ TextAttributeRun& run) + { + return NoThrowString().Format(L"%wc%d", run.GetAttributes().GetLegacyAttributes(), run.GetLength()); + } + + void LogChain(_In_ PCWSTR pwszPrefix, + std::vector& chain) + { + NoThrowString str(pwszPrefix); + + if (chain.size() > 0) + { + str.Append(LogRunElement(chain[0])); + + for (size_t i = 1; i < chain.size(); i++) + { + str.AppendFormat(L"->%s", (const wchar_t*)(LogRunElement(chain[i]))); + } + } + + Log::Comment(str); + } + + void DoTestInsertAttrRuns(UINT& uiStartPos, WORD& ch1, UINT& uiChar1Length, WORD& ch2, UINT& uiChar2Length) + { + Log::Comment(String().Format(L"StartPos: %d, Char1: %wc, Char1Length: %d, Char2: %wc, Char2Length: %d", + uiStartPos, + ch1, + uiChar1Length, + ch2, + uiChar2Length)); + + bool const fUseStr2 = (ch2 != L'0'); + + // Set up our "original row" that we are going to try to insert into. + // This will represent a 10 column run of R3->B5->G2 that we will use for all tests. + ATTR_ROW originalRow{ static_cast(_sDefaultLength), _DefaultAttr }; + originalRow._list.resize(3); + originalRow._cchRowWidth = 10; + originalRow._list[0].SetAttributesFromLegacy('R'); + originalRow._list[0].SetLength(3); + originalRow._list[1].SetAttributesFromLegacy('B'); + originalRow._list[1].SetLength(5); + originalRow._list[2].SetAttributesFromLegacy('G'); + originalRow._list[2].SetLength(2); + LogChain(L"Original: ", originalRow._list); + + // Set up our "insertion run" + size_t cInsertRow = 1; + if (fUseStr2) + { + cInsertRow++; + } + + std::vector insertRow; + insertRow.resize(cInsertRow); + insertRow[0].SetAttributesFromLegacy(ch1); + insertRow[0].SetLength(uiChar1Length); + if (fUseStr2) + { + insertRow[1].SetAttributesFromLegacy(ch2); + insertRow[1].SetLength(uiChar2Length); + } + + + LogChain(L"Insert: ", insertRow); + Log::Comment(NoThrowString().Format(L"At Index: %d", uiStartPos)); + + UINT uiTotalLength = uiChar1Length; + if (fUseStr2) + { + uiTotalLength += uiChar2Length; + } + + VERIFY_IS_TRUE((uiStartPos + uiTotalLength) >= 1); // assert we won't underflow. + UINT const uiEndPos = uiStartPos + uiTotalLength - 1; + + // Calculate our expected final/result run by unpacking original, laying our insertion on it at the index + // then using our pack function to repack it. + // This method is easy to understand and very reliable, but its performance is bad. + // The InsertAttrRuns method we test against below is hard to understand but very high performance in production. + + // - 1. Unpack + std::vector unpackedOriginal = { originalRow.begin(), originalRow.end() }; + + // - 2. Overlay insertion + UINT uiInsertedCount = 0; + UINT uiInsertIndex = 0; + + // --- Walk through the unpacked run from start to end.... + for (UINT uiUnpackedIndex = uiStartPos; uiUnpackedIndex <= uiEndPos; uiUnpackedIndex++) + { + // Pull the item from the insert run to analyze. + TextAttributeRun run = insertRow[uiInsertIndex]; + + // Copy the attribute from the run into the unpacked array + unpackedOriginal[uiUnpackedIndex] = run.GetAttributes(); + + // Increment how many times we've copied this particular portion of the run + uiInsertedCount++; + + // If we've now inserted enough of them to match the length, advance the insert index and reset the counter. + if (uiInsertedCount >= run.GetLength()) + { + uiInsertIndex++; + uiInsertedCount = 0; + } + } + + // - 3. Pack. + std::unique_ptr packedRun; + size_t cPackedRun = 0; + VERIFY_SUCCEEDED(PackAttrs(unpackedOriginal.data(), originalRow._cchRowWidth, packedRun, &cPackedRun)); + + // Now send parameters into InsertAttrRuns and get its opinion on the subject. + VERIFY_SUCCEEDED(originalRow.InsertAttrRuns({ insertRow.data(), insertRow.size() }, uiStartPos, uiEndPos, (UINT)originalRow._cchRowWidth)); + + // Compare and ensure that the expected and actual match. + VERIFY_ARE_EQUAL(cPackedRun, originalRow._list.size(), L"Ensure that number of array elements required for RLE are the same."); + + std::vector packedRunExpected; + std::copy_n(packedRun.get(), cPackedRun, std::back_inserter(packedRunExpected)); + + LogChain(L"Expected: ", packedRunExpected); + LogChain(L"Actual: ", originalRow._list); + + for (size_t testIndex = 0; testIndex < cPackedRun; testIndex++) + { + VERIFY_ARE_EQUAL(packedRun[testIndex], originalRow._list[testIndex]); + } + } + + TEST_METHOD(TestInsertAttrRunsSingle) + { + UINT const uiTestRunLength = 10; + + UINT uiStartPos = 0; + WORD ch1 = L'0'; + UINT uiChar1Length = 0; + WORD ch2 = L'0'; + UINT uiChar2Length = 0; + + Log::Comment(L"Test inserting a single item of a variable length into the run."); + WORD rgch1Options[] = { L'X', L'R', L'G', L'B' }; + for (size_t iCh1Option = 0; iCh1Option < ARRAYSIZE(rgch1Options); iCh1Option++) + { + ch1 = rgch1Options[iCh1Option]; + for (UINT iCh1Length = 1; iCh1Length <= uiTestRunLength; iCh1Length++) + { + uiChar1Length = iCh1Length; + + // We can't try to insert a run that's longer than would fit. + // If the run is of length 10 and we're trying to insert a length of 10, + // we can only insert at position 0. + // For the run length of 10 and an insert length of 9, we can try positions 0 and 1. + // And so on... + UINT const uiMaxPos = uiTestRunLength - uiChar1Length; + + for (UINT iStartPos = 0; iStartPos < uiMaxPos; iStartPos++) + { + uiStartPos = iStartPos; + + DoTestInsertAttrRuns(uiStartPos, ch1, uiChar1Length, ch2, uiChar2Length); + } + } + } + } + + TEST_METHOD(TestInsertAttrRunsMultiple) + { + UINT const uiTestRunLength = 10; + + UINT uiStartPos = 0; + WORD ch1 = L'0'; + UINT uiChar1Length = 0; + WORD ch2 = L'0'; + UINT uiChar2Length = 0; + + Log::Comment(L"Test inserting a multiple item run with each piece having variable length into the existing run."); + WORD rgch1Options[] = { L'X', L'R', L'G', L'B' }; + for (size_t iCh1Option = 0; iCh1Option < ARRAYSIZE(rgch1Options); iCh1Option++) + { + ch1 = rgch1Options[iCh1Option]; + + UINT const uiMaxCh1Length = uiTestRunLength - 1; // leave at least 1 space for the second piece of the inser trun. + for (UINT iCh1Length = 1; iCh1Length <= uiMaxCh1Length; iCh1Length++) + { + uiChar1Length = iCh1Length; + + WORD rgch2Options[] = { L'Y' }; + for (size_t iCh2Option = 0; iCh2Option < ARRAYSIZE(rgch2Options); iCh2Option++) + { + ch2 = rgch2Options[iCh2Option]; + + // When choosing the length of the second item, it can't be bigger than the remaining space in the run + // when accounting for the length of the first piece chosen. + // For example if the total run length is 10 and the first piece chosen was 8 long, + // the second piece can only be 1 or 2 long. + UINT const uiMaxCh2Length = uiTestRunLength - uiMaxCh1Length; + for (UINT iCh2Length = 1; iCh2Length <= uiMaxCh2Length; iCh2Length++) + { + uiChar2Length = iCh2Length; + + // We can't try to insert a run that's longer than would fit. + // If the run is of length 10 and we're trying to insert a total length of 10, + // we can only insert at position 0. + // For the run length of 10 and an insert length of 9, we can try positions 0 and 1. + // And so on... + UINT const uiMaxPos = uiTestRunLength - (uiChar1Length + uiChar2Length); + + for (UINT iStartPos = 0; iStartPos <= uiMaxPos; iStartPos++) + { + uiStartPos = iStartPos; + + DoTestInsertAttrRuns(uiStartPos, ch1, uiChar1Length, ch2, uiChar2Length); + } + } + } + } + } + } + + TEST_METHOD(TestUnpackAttrs) + { + + Log::Comment(L"Checking unpack of a single color for the entire length"); + { + const std::vector attrs{ pSingle->begin(), pSingle->end() }; + + for (auto& attr : attrs) + { + VERIFY_ARE_EQUAL(attr, _DefaultAttr); + } + } + + Log::Comment(L"Checking unpack of the multiple color chain"); + + const std::vector attrs{ pChain->begin(), pChain->end() }; + + short cChainRun = 0; // how long we've been looking at the current piece of the chain + short iChainSegIndex = 0; // which piece of the chain we should be on right now + + for (auto& attr : attrs) + { + // by default the chain was assembled above to have the chain segment index be the attribute + TextAttribute MatchingAttr = TextAttribute(iChainSegIndex); + + // However, if the index is greater than the expected chain length, a remainder piece was made with a default attribute + if (iChainSegIndex >= _sDefaultChainLength) + { + MatchingAttr = _DefaultChainAttr; + } + + VERIFY_ARE_EQUAL(attr, MatchingAttr); + + // Add to the chain run + cChainRun++; + + // If the chain run is greater than the length the segments were specified to be + if (cChainRun >= sChainSegLength) + { + // reset to 0 + cChainRun = 0; + + // move to the next chain segment down the line + iChainSegIndex++; + } + } + } + + TEST_METHOD(TestSetAttrToEnd) + { + const WORD wTestAttr = FOREGROUND_BLUE | BACKGROUND_GREEN; + TextAttribute TestAttr = TextAttribute(wTestAttr); + + Log::Comment(L"FIRST: Set index to > 0 to test making/modifying chains"); + const short iTestIndex = 50; + VERIFY_IS_TRUE(iTestIndex >= 0 && iTestIndex < _sDefaultLength); + + Log::Comment(L"SetAttrToEnd for single color applied to whole string."); + pSingle->SetAttrToEnd(iTestIndex, TestAttr); + + // Was 1 (single), should now have 2 segments + VERIFY_ARE_EQUAL(pSingle->_list.size(), 2u); + + VERIFY_ARE_EQUAL(pSingle->_list[0].GetAttributes(), _DefaultAttr); + VERIFY_ARE_EQUAL(pSingle->_list[0].GetLength(), (unsigned int)(_sDefaultLength - (_sDefaultLength - iTestIndex))); + + VERIFY_ARE_EQUAL(pSingle->_list[1].GetAttributes(), TestAttr); + VERIFY_ARE_EQUAL(pSingle->_list[1].GetLength(), (unsigned int)(_sDefaultLength - iTestIndex)); + + Log::Comment(L"SetAttrToEnd for existing chain of multiple colors."); + pChain->SetAttrToEnd(iTestIndex, TestAttr); + + // From 7 segments down to 5. + VERIFY_ARE_EQUAL(pChain->_list.size(), 5u); + + // Verify chain colors and lengths + VERIFY_ARE_EQUAL(TextAttribute(0), pChain->_list[0].GetAttributes()); + VERIFY_ARE_EQUAL(pChain->_list[0].GetLength(), (unsigned int)13); + + VERIFY_ARE_EQUAL(TextAttribute(1), pChain->_list[1].GetAttributes()); + VERIFY_ARE_EQUAL(pChain->_list[1].GetLength(), (unsigned int)13); + + VERIFY_ARE_EQUAL(TextAttribute(2), pChain->_list[2].GetAttributes()); + VERIFY_ARE_EQUAL(pChain->_list[2].GetLength(), (unsigned int)13); + + VERIFY_ARE_EQUAL(TextAttribute(3), pChain->_list[3].GetAttributes()); + VERIFY_ARE_EQUAL(pChain->_list[3].GetLength(), (unsigned int)11); + + VERIFY_ARE_EQUAL(TestAttr, pChain->_list[4].GetAttributes()); + VERIFY_ARE_EQUAL(pChain->_list[4].GetLength(), (unsigned int)30); + + Log::Comment(L"SECOND: Set index to 0 to test replacing anything with a single"); + + ATTR_ROW* pTestItems[]{ pSingle, pChain }; + + for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) + { + ATTR_ROW* pUnderTest = pTestItems[iIndex]; + + pUnderTest->SetAttrToEnd(0, TestAttr); + + // should be down to 1 attribute set from beginning to end of string + VERIFY_ARE_EQUAL(pUnderTest->_list.size(), 1u); + + // singular pair should contain the color + VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetAttributes(), TestAttr); + + // and its length should be the length of the whole string + VERIFY_ARE_EQUAL(pUnderTest->_list[0].GetLength(), (unsigned int)_sDefaultLength); + } + } + + TEST_METHOD(TestTotalLength) + { + ATTR_ROW* pTestItems[]{ pSingle, pChain }; + + for (UINT iIndex = 0; iIndex < ARRAYSIZE(pTestItems); iIndex++) + { + ATTR_ROW* pUnderTest = pTestItems[iIndex]; + + const size_t Result = pUnderTest->_cchRowWidth; + + VERIFY_ARE_EQUAL((short)Result, _sDefaultLength); + } + } + + TEST_METHOD(TestResize) + { + CommonState state; + state.PrepareGlobalFont(); + state.PrepareGlobalScreenBuffer(); + + pSingle->Resize(240); + pChain->Resize(240); + + pSingle->Resize(255); + pChain->Resize(255); + + pSingle->Resize(255); + pChain->Resize(255); + + pSingle->Resize(60); + pChain->Resize(60); + + pSingle->Resize(60); + pChain->Resize(60); + + VERIFY_THROWS_SPECIFIC(pSingle->Resize(0), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + VERIFY_THROWS_SPECIFIC(pChain->Resize(0), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + + state.CleanupGlobalScreenBuffer(); + state.CleanupGlobalFont(); + } +}; diff --git a/src/host/ut_host/CharRowBaseTests.cpp b/src/host/ut_host/CharRowBaseTests.cpp new file mode 100644 index 000000000..e75f2cc5c --- /dev/null +++ b/src/host/ut_host/CharRowBaseTests.cpp @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +#include "CommonState.hpp" + +#include "../Ucs2CharRow.hpp" +#include "../Utf8CharRow.hpp" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class CharRowBaseTests +{ + TEST_CLASS(CharRowBaseTests); + + const size_t rowWidth = 80; + const Ucs2CharRow::glyph_type ucs2Glyph = L'a'; + // hiragana ka U+304B + const Utf8CharRow::glyph_type utf8Glyph = { '\xE3', '\x81', '\x8B'}; + + const Ucs2CharRow::glyph_type ucs2DefaultGlyph = UNICODE_SPACE; + const Utf8CharRow::glyph_type utf8DefaultGlyph = { UNICODE_SPACE }; + + Ucs2CharRow::string_type ucs2Text = L"Loremipsumdolorsitamet,consecteturadipiscingelit.Nullametrutrummetus.Namquiseratal"; + std::vector utf8Text; + + + Ucs2CharRow ucs2CharRow; + Utf8CharRow utf8CharRow; + + CharRowBaseTests() : + ucs2CharRow(rowWidth), + utf8CharRow(rowWidth) + { + ucs2Text.resize(rowWidth); + while (utf8Text.size() < rowWidth) + { + utf8Text.push_back({ 'a' }); + utf8Text.push_back({ '\xE3', '\x81', '\x9B' }); // hiragana se U+305B + utf8Text.push_back({ '\xD0', '\x94' }); // cyrillic de U+0414 + } + utf8Text.resize(rowWidth); + } + + // sets single cell at column to glyph value matching class + void SetGlyphAt(ICharRow* const pCharRow, const size_t column) + { + switch (pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + static_cast(pCharRow)->GetGlyphAt(column) = ucs2Glyph; + break; + case ICharRow::SupportedEncoding::Utf8: + static_cast(pCharRow)->GetGlyphAt(column) = utf8Glyph; + break; + default: + VERIFY_IS_TRUE(false); + } + } + + // sets cell data for class text data and passed in attrs + void SetCellData(ICharRow* const pCharRow, const std::vector& attrs) + { + VERIFY_ARE_EQUAL(attrs.size(), rowWidth); + switch (pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + { + auto& cells = static_cast(pCharRow)->_data; + for (size_t i = 0; i < cells.size(); ++i) + { + cells[i].first = ucs2Text[i]; + cells[i].second = attrs[i]; + } + break; + } + case ICharRow::SupportedEncoding::Utf8: + { + auto& cells = static_cast(pCharRow)->_data; + for (size_t i = 0; i < cells.size(); ++i) + { + cells[i].first = utf8Text[i]; + cells[i].second = attrs[i]; + } + break; + } + default: + VERIFY_IS_TRUE(false); + } + } + + TEST_METHOD_SETUP(MethodSetup) + { + // reset + ucs2CharRow.Reset(); + utf8CharRow.Reset(); + // resize + VERIFY_SUCCEEDED(ucs2CharRow.Resize(rowWidth)); + VERIFY_SUCCEEDED(utf8CharRow.Resize(rowWidth)); + + return true; + } + + TEST_METHOD(TestInitialize) + { + Ucs2CharRow row1(rowWidth); + Utf8CharRow row2(rowWidth); + VERIFY_ARE_EQUAL(row1.GetSupportedEncoding(), ICharRow::SupportedEncoding::Ucs2); + VERIFY_ARE_EQUAL(row2.GetSupportedEncoding(), ICharRow::SupportedEncoding::Utf8); + + std::vector rows{ &row1, &row2 }; + for (ICharRow* const pCharRow : rows) + { + VERIFY_IS_FALSE(pCharRow->WasWrapForced()); + VERIFY_IS_FALSE(pCharRow->WasDoubleBytePadded()); + VERIFY_ARE_EQUAL(pCharRow->size(), rowWidth); + + // check that cell data was initialized correctly + switch (pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + for (auto& cell : static_cast(pCharRow)->_data) + { + VERIFY_ARE_EQUAL(cell.first, ucs2DefaultGlyph); + VERIFY_IS_TRUE(cell.second.IsSingle()); + } + break; + case ICharRow::SupportedEncoding::Utf8: + for (auto& cell : static_cast(pCharRow)->_data) + { + VERIFY_ARE_EQUAL(cell.first, utf8DefaultGlyph); + VERIFY_IS_TRUE(cell.second.IsSingle()); + } + break; + default: + VERIFY_IS_TRUE(false); + } + } + } + + TEST_METHOD(TestContainsText) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + const size_t index = 10; + + for (ICharRow* const pCharRow : rows) + { + // After init, should have no text + VERIFY_IS_FALSE(pCharRow->ContainsText()); + + // add some text + SetGlyphAt(pCharRow, index); + + // should have text + VERIFY_IS_TRUE(pCharRow->ContainsText()); + } + } + + TEST_METHOD(TestMeasuring) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + + for (ICharRow* const pCharRow : rows) + { + std::vector, // locations to fill with characters + size_t, // MeasureLeft value + size_t // MeasureRight value + >> testData = + { + { + L"a row with all whitespace should measure the whole row", + {}, + rowWidth, + 0 + }, + + { + L"a character as far left as possible", + { 0 }, + 0, + 1 + }, + + { + L"a character as far right as possible", + { rowWidth - 1 }, + rowWidth - 1, + rowWidth + }, + + { + L"a character on the left side", + { 10 }, + 10, + 11 + }, + + { + L"a character on the right side", + { rowWidth - 12 }, + rowWidth - 12, + rowWidth - 11 + }, + + { + L"characters on both edges", + { 0, rowWidth - 1}, + 0, + rowWidth + }, + + { + L"characters near both edges", + { 7, rowWidth - 3 }, + 7, + rowWidth - 2 + }, + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + // apply the character changes + const auto cellLocations = std::get<1>(data); + for (size_t index : cellLocations) + { + SetGlyphAt(pCharRow, index); + } + + // test measuring + VERIFY_ARE_EQUAL(pCharRow->MeasureLeft(), std::get<2>(data)); + VERIFY_ARE_EQUAL(pCharRow->MeasureRight(), std::get<3>(data)); + + // reset character changes + for (size_t index : cellLocations) + { + pCharRow->ClearCell(index); + } + } + } + } + + TEST_METHOD(TestResize) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + + std::vector attrs(rowWidth); + // change a bunch of random dbcs attributes + for (auto& attr : attrs) + { + auto choice = rand() % 2; + switch (choice) + { + case 0: + attr.SetSingle(); + break; + case 1: + attr.SetLeading(); + break; + case 2: + attr.SetTrailing(); + break; + default: + VERIFY_IS_TRUE(false); + } + } + + const size_t smallSize = rowWidth / 2; + const size_t bigSize = rowWidth * 2; + + for (ICharRow* const pCharRow : rows) + { + // fill cells with data + SetCellData(pCharRow, attrs); + + // resize smaller + VERIFY_SUCCEEDED(pCharRow->Resize(smallSize)); + VERIFY_ARE_EQUAL(pCharRow->size(), smallSize); + + // resize bigger + VERIFY_SUCCEEDED(pCharRow->Resize(bigSize)); + VERIFY_ARE_EQUAL(pCharRow->size(), bigSize); + + switch (pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + // data not clipped should not have changed + for (size_t i = 0; i < smallSize; ++i) + { + auto cell = static_cast(pCharRow)->_data[i]; + VERIFY_ARE_EQUAL(cell.first, ucs2Text[i]); + VERIFY_ARE_EQUAL(cell.second, attrs[i]); + } + // newly added cells should be set to the defaults + for (size_t i = smallSize + 1; i < bigSize; ++i) + { + auto cell = static_cast(pCharRow)->_data[i]; + VERIFY_ARE_EQUAL(cell.first, ucs2DefaultGlyph); + VERIFY_IS_TRUE(cell.second.IsSingle()); + } + break; + case ICharRow::SupportedEncoding::Utf8: + // data not clipped should not have changed + for (size_t i = 0; i < smallSize; ++i) + { + auto cell = static_cast(pCharRow)->_data[i]; + VERIFY_ARE_EQUAL(cell.first, utf8Text[i]); + VERIFY_ARE_EQUAL(cell.second, attrs[i]); + } + // newly added cells should be set to the defaults + for (size_t i = smallSize + 1; i < bigSize; ++i) + { + auto cell = static_cast(pCharRow)->_data[i]; + VERIFY_ARE_EQUAL(cell.first, utf8DefaultGlyph); + VERIFY_IS_TRUE(cell.second.IsSingle()); + } + break; + default: + VERIFY_IS_TRUE(false); + } + } + } + + TEST_METHOD(TestClearCell) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + std::vector attrs(rowWidth, DbcsAttribute::Attribute::Leading); + + // generate random cell locations to clear + std::vector eraseIndices; + for (auto i = 0; i < 10; ++i) + { + eraseIndices.push_back(rand() % rowWidth); + } + + for (ICharRow* pCharRow : rows) + { + // fill cells with data + SetCellData(pCharRow, attrs); + + for (auto index : eraseIndices) + { + pCharRow->ClearCell(index); + switch(pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + { + auto& cell = static_cast(pCharRow)->_data[index]; + VERIFY_ARE_EQUAL(cell.first, ucs2DefaultGlyph); + VERIFY_ARE_EQUAL(cell.second, DbcsAttribute::Attribute::Single); + break; + } + case ICharRow::SupportedEncoding::Utf8: + { + auto& cell = static_cast(pCharRow)->_data[index]; + VERIFY_ARE_EQUAL(cell.first, utf8DefaultGlyph); + VERIFY_ARE_EQUAL(cell.second, DbcsAttribute::Attribute::Single); + break; + } + default: + VERIFY_IS_TRUE(false); + } + } + } + } + + TEST_METHOD(TestClearGlyph) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + std::vector attrs(rowWidth, DbcsAttribute::Attribute::Leading); + + // generate random cell locations to clear + std::vector eraseIndices; + for (auto i = 0; i < 10; ++i) + { + eraseIndices.push_back(rand() % rowWidth); + } + + for (ICharRow* pCharRow : rows) + { + // fill cells with data + SetCellData(pCharRow, attrs); + + for (auto index : eraseIndices) + { + pCharRow->ClearGlyph(index); + switch(pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + { + auto& cell = static_cast(pCharRow)->_data[index]; + VERIFY_ARE_EQUAL(cell.first, ucs2DefaultGlyph); + VERIFY_ARE_EQUAL(cell.second, DbcsAttribute::Attribute::Leading); + break; + } + case ICharRow::SupportedEncoding::Utf8: + { + auto& cell = static_cast(pCharRow)->_data[index]; + VERIFY_ARE_EQUAL(cell.first, utf8DefaultGlyph); + VERIFY_ARE_EQUAL(cell.second, DbcsAttribute::Attribute::Leading); + break; + } + default: + VERIFY_IS_TRUE(false); + } + } + } + } + + TEST_METHOD(TestGetText) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + std::vector attrs(rowWidth); + // want to make sure that trailing cells are filtered out + for (size_t i = 0; i < attrs.size(); ++i) + { + if (i % 2 == 0) + { + attrs[i].SetLeading(); + } + else + { + attrs[i].SetTrailing(); + } + } + + for (ICharRow* const pCharRow : rows) + { + // fill cells with data + SetCellData(pCharRow, attrs); + + switch(pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + { + Ucs2CharRow::string_type expectedText = L""; + for (size_t i = 0; i < ucs2Text.size(); ++i) + { + if (i % 2 == 0) + { + expectedText += ucs2Text[i]; + } + } + VERIFY_ARE_EQUAL(expectedText, static_cast(pCharRow)->GetText()); + break; + } + case ICharRow::SupportedEncoding::Utf8: + { + Utf8CharRow::string_type expectedText = ""; + for (size_t i = 0; i < utf8Text.size(); ++i) + { + if (i % 2 == 0) + { + auto glyph = utf8Text[i]; + for (auto ch : glyph) + { + expectedText += ch; + } + } + } + VERIFY_ARE_EQUAL(expectedText, static_cast(pCharRow)->GetText()); + break; + } + default: + VERIFY_IS_TRUE(false); + } + } + } + + TEST_METHOD(TestIterators) + { + std::vector rows = { &ucs2CharRow, &utf8CharRow }; + std::vector attrs(rowWidth, DbcsAttribute::Attribute::Trailing); + + for (ICharRow* const pCharRow : rows) + { + // fill cells with data + SetCellData(pCharRow, attrs); + + // make sure data received from iterators matches data written + switch(pCharRow->GetSupportedEncoding()) + { + case ICharRow::SupportedEncoding::Ucs2: + { + size_t index = 0; + const Ucs2CharRow& charRow = *static_cast(pCharRow); + for (Ucs2CharRow::const_iterator it = charRow.cbegin(); it != charRow.cend(); ++it) + { + VERIFY_ARE_EQUAL(ucs2Text[index], it->first); + VERIFY_ARE_EQUAL(attrs[index], it->second); + ++index; + } + break; + } + case ICharRow::SupportedEncoding::Utf8: + { + size_t index = 0; + const Utf8CharRow& charRow = *static_cast(pCharRow); + for (Utf8CharRow::const_iterator it = charRow.cbegin(); it != charRow.cend(); ++it) + { + VERIFY_ARE_EQUAL(utf8Text[index], it->first); + VERIFY_ARE_EQUAL(attrs[index], it->second); + ++index; + } + break; + } + default: + VERIFY_IS_TRUE(false); + } + } + } + +}; diff --git a/src/host/ut_host/ClipboardTests.cpp b/src/host/ut_host/ClipboardTests.cpp new file mode 100644 index 000000000..cb893f6e8 --- /dev/null +++ b/src/host/ut_host/ClipboardTests.cpp @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +#include "CommonState.hpp" + +#include "globals.h" + +#include "..\interactivity\win32\Clipboard.hpp" +#include "..\interactivity\inc\ServiceLocator.hpp" + +#include "dbcs.h" + +#include + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "..\..\interactivity\inc\VtApiRedirection.hpp" +#endif + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +#include "UnicodeLiteral.hpp" +#include "../../inc/consoletaeftemplates.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +static const WORD altScanCode = 0x38; +static const WORD leftShiftScanCode = 0x2A; + +class ClipboardTests +{ + TEST_CLASS(ClipboardTests); + + CommonState* m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->FillTextBuffer(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + return true; + } + + const UINT cRectsSelected = 4; + + std::vector SetupRetrieveFromBuffers(bool fLineSelection, std::vector& selection) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases + // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. + + // set up and try to retrieve the first 4 rows from the buffer + const auto& screenInfo = gci.GetActiveOutputBuffer(); + + selection.clear(); + selection.emplace_back(SMALL_RECT{ 0, 0, 8, 0 }); + selection.emplace_back(SMALL_RECT{ 0, 1, 14, 1 }); + selection.emplace_back(SMALL_RECT{ 0, 2, 14, 2 }); + selection.emplace_back(SMALL_RECT{ 0, 3, 8, 3 }); + + return Clipboard::Instance().RetrieveTextFromBuffer(screenInfo, + fLineSelection, + selection).text; + } + +#pragma prefast(push) +#pragma prefast(disable:26006, "Specifically trying to check unterminated strings in this test.") + TEST_METHOD(TestRetrieveFromBuffer) + { + // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases + // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. + + std::vector selection; + const auto text = SetupRetrieveFromBuffers(false, selection); + + // verify trailing bytes were trimmed + // there are 2 double-byte characters in our sample string (see CommonState.hpp for sample) + // the width is right - left + VERIFY_ARE_EQUAL((short)wcslen(text[0].data()), selection[0].Right - selection[0].Left + 1); + + // since we're not in line selection, the line should be \r\n terminated + PCWCHAR tempPtr = text[0].data(); + tempPtr += text[0].size(); + tempPtr -= 2; + VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n")); + + // since we're not in line selection, spaces should be trimmed from the end + tempPtr = text[0].data(); + tempPtr += selection[0].Right - selection[0].Left - 2; + tempPtr++; + VERIFY_IS_NULL(wcsrchr(tempPtr, L' ')); + + // final line of selection should not contain CR/LF + tempPtr = text[3].data(); + tempPtr += text[3].size(); + tempPtr -= 2; + VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n")); + } +#pragma prefast(pop) + + TEST_METHOD(TestRetrieveLineSelectionFromBuffer) + { + // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases + // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. + + + std::vector selection; + const auto text = SetupRetrieveFromBuffers(true, selection); + + // row 2, no wrap + // no wrap row before the end should have CR/LF + PCWCHAR tempPtr = text[2].data(); + tempPtr += text[2].size(); + tempPtr -= 2; + VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n")); + + // no wrap row should trim spaces at the end + tempPtr = text[2].data(); + VERIFY_IS_NULL(wcsrchr(tempPtr, L' ')); + + // row 1, wrap + // wrap row before the end should *not* have CR/LF + tempPtr = text[1].data(); + tempPtr += text[1].size(); + tempPtr -= 2; + VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n")); + + // wrap row should have spaces at the end + tempPtr = text[1].data(); + const wchar_t* ptr = wcsrchr(tempPtr, L' '); + VERIFY_IS_NOT_NULL(ptr); + } + + TEST_METHOD(CanConvertTextToInputEvents) + { + std::wstring wstr = L"hello world"; + std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), + wstr.size()); + VERIFY_ARE_EQUAL(wstr.size() * 2, events.size()); + IInputServices* pInputServices = ServiceLocator::LocateInputServices(); + for (wchar_t wch : wstr) + { + std::deque keydownPattern{ true, false }; + for (bool isKeyDown : keydownPattern) + { + VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); + std::unique_ptr keyEvent; + keyEvent.reset(static_cast(events.front().release())); + events.pop_front(); + + const short keyState = pInputServices->VkKeyScanW(wch); + VERIFY_ARE_NOT_EQUAL(-1, keyState); + const WORD virtualScanCode = static_cast(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC)); + + VERIFY_ARE_EQUAL(wch, keyEvent->GetCharData()); + VERIFY_ARE_EQUAL(isKeyDown, keyEvent->IsKeyDown()); + VERIFY_ARE_EQUAL(1, keyEvent->GetRepeatCount()); + VERIFY_ARE_EQUAL(static_cast(0), keyEvent->GetActiveModifierKeys()); + VERIFY_ARE_EQUAL(virtualScanCode, keyEvent->GetVirtualScanCode()); + VERIFY_ARE_EQUAL(LOBYTE(keyState), keyEvent->GetVirtualKeyCode()); + } + } + } + + TEST_METHOD(CanConvertUppercaseText) + { + std::wstring wstr = L"HeLlO WoRlD"; + size_t uppercaseCount = 0; + for (wchar_t wch : wstr) + { + std::isupper(wch) ? ++uppercaseCount : 0; + } + std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), + wstr.size()); + + + VERIFY_ARE_EQUAL((wstr.size() + uppercaseCount) * 2, events.size()); + IInputServices* pInputServices = ServiceLocator::LocateInputServices(); + VERIFY_IS_NOT_NULL(pInputServices); + for (wchar_t wch : wstr) + { + std::deque keydownPattern{ true, false }; + for (bool isKeyDown : keydownPattern) + { + Log::Comment(NoThrowString().Format(L"testing char: %C; keydown: %d", wch, isKeyDown)); + + VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); + std::unique_ptr keyEvent; + keyEvent.reset(static_cast(events.front().release())); + events.pop_front(); + + const short keyScanError = -1; + const short keyState = pInputServices->VkKeyScanW(wch); + VERIFY_ARE_NOT_EQUAL(keyScanError, keyState); + const WORD virtualScanCode = static_cast(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC)); + + if (std::isupper(wch)) + { + // uppercase letters have shift key events + // surrounding them, making two events per letter + // (and another two for the keyup) + VERIFY_IS_FALSE(events.empty()); + + VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); + std::unique_ptr keyEvent2; + keyEvent2.reset(static_cast(events.front().release())); + events.pop_front(); + + const short keyState2 = pInputServices->VkKeyScanW(wch); + VERIFY_ARE_NOT_EQUAL(keyScanError, keyState); + const WORD virtualScanCode2 = static_cast(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC)); + + if (isKeyDown) + { + // shift then letter + const KeyEvent shiftDownEvent({ TRUE, 1, VK_SHIFT, leftShiftScanCode, L'\0', SHIFT_PRESSED }); + VERIFY_ARE_EQUAL(shiftDownEvent, *keyEvent); + + const KeyEvent expectedKeyEvent({ TRUE, 1, LOBYTE(keyState2), virtualScanCode2, wch, SHIFT_PRESSED }); + VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent2); + } + else + { + // letter then shift + const KeyEvent expectedKeyEvent({ FALSE, 1, LOBYTE(keyState), virtualScanCode, wch, SHIFT_PRESSED }); + VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); + + const KeyEvent shiftUpEvent({ FALSE, 1, VK_SHIFT, leftShiftScanCode, L'\0', 0 }); + VERIFY_ARE_EQUAL(shiftUpEvent, *keyEvent2); + } + } + else + { + const KeyEvent expectedKeyEvent({ !!isKeyDown, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }); + VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); + } + } + } + } + +#ifdef __INSIDE_WINDOWS + TEST_METHOD(CanConvertCharsRequiringAltGr) + { + const std::wstring wstr = L"\x20ac"; // € char U+20AC + std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), + wstr.size()); + + // should be converted to: + // 1. AltGr keydown + // 2. € keydown + // 3. € keyup + // 4. AltGr keyup + const size_t convertedSize = 4; + VERIFY_ARE_EQUAL(convertedSize, events.size()); + + const short keyState = VkKeyScanW(wstr[0]); + const WORD virtualKeyCode = LOBYTE(keyState); + + std::deque expectedEvents; + expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); + expectedEvents.push_back({ TRUE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); + expectedEvents.push_back({ FALSE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); + expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, L'\0', ENHANCED_KEY }); + + for (size_t i = 0; i < events.size(); ++i) + { + const KeyEvent currentKeyEvent = *reinterpret_cast(events[i].get()); + VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); + } + } +#endif + + TEST_METHOD(CanConvertCharsOutsideKeyboardLayout) + { + const std::wstring wstr = L"\xbc"; // ¼ char U+00BC + const UINT outputCodepage = CP_JAPANESE; + ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP = outputCodepage; + std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), + wstr.size()); + + // should be converted to: + // 1. left alt keydown + // 2. 1st numpad keydown + // 3. 1st numpad keyup + // 4. 2nd numpad keydown + // 5. 2nd numpad keyup + // 6. left alt keyup + const size_t convertedSize = 6; + VERIFY_ARE_EQUAL(convertedSize, events.size()); + + std::deque expectedEvents; + expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); + expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 }); + + for (size_t i = 0; i < events.size(); ++i) + { + const KeyEvent currentKeyEvent = *reinterpret_cast(events[i].get()); + VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); + } + } + }; diff --git a/src/host/ut_host/CodepointWidthDetectorTests.cpp b/src/host/ut_host/CodepointWidthDetectorTests.cpp new file mode 100644 index 000000000..e5e584e25 --- /dev/null +++ b/src/host/ut_host/CodepointWidthDetectorTests.cpp @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" +#include "CommonState.hpp" + +#include "../types/inc/CodepointWidthDetector.hpp" + +using namespace WEX::Logging; + +static const std::wstring emoji = L"\xD83E\xDD22"; // U+1F922 nauseated face + +// codepoint and utf16 encoded string +static const std::vector> testData = +{ + { 0x7, L"\a", CodepointWidth::Narrow }, // BEL + { 0x20, L" ", CodepointWidth::Narrow }, + { 0x39, L"9", CodepointWidth::Narrow }, + { 0x414, L"\x414", CodepointWidth::Ambiguous }, // U+0414 cyrillic capital de + { 0x1104, L"\x1104", CodepointWidth::Wide }, // U+1104 hangul choseong ssangtikeut + { 0x306A, L"\x306A", CodepointWidth::Wide }, // U+306A hiragana na + { 0x30CA, L"\x30CA", CodepointWidth::Wide }, // U+30CA katakana na + { 0x72D7, L"\x72D7", CodepointWidth::Wide }, // U+72D7 + { 0x1F47E, L"\xD83D\xDC7E", CodepointWidth::Wide }, // U+1F47E alien monster + { 0x1F51C, L"\xD83D\xDD1C", CodepointWidth::Wide } // U+1F51C SOON +}; + +class CodepointWidthDetectorTests +{ + TEST_CLASS(CodepointWidthDetectorTests); + + + TEST_METHOD(CodepointWidthDetectDefersMapPopulation) + { + const CodepointWidthDetector widthDetector; + VERIFY_IS_TRUE(widthDetector._map.empty()); + widthDetector.IsWide(UNICODE_SPACE); + VERIFY_IS_TRUE(widthDetector._map.empty()); + // now force checking + widthDetector.GetWidth(emoji); + VERIFY_IS_FALSE(widthDetector._map.empty()); + } + + TEST_METHOD(CanLookUpEmoji) + { + const CodepointWidthDetector widthDetector; + VERIFY_IS_TRUE(widthDetector.IsWide(emoji)); + } + + TEST_METHOD(TestUnicodeRangeCompare) + { + const CodepointWidthDetector::UnicodeRangeCompare compare; + // test comparing 2 search terms + CodepointWidthDetector::UnicodeRange a{ 0x10 }; + CodepointWidthDetector::UnicodeRange b{ 0x15 }; + VERIFY_IS_TRUE(static_cast(compare(a, b))); + } + + TEST_METHOD(CanExtractCodepoint) + { + const CodepointWidthDetector widthDetector; + for (const auto& data : testData) + { + const auto& expected = std::get<0>(data); + const auto& wstr = std::get<1>(data); + const auto result = widthDetector._extractCodepoint({ wstr.c_str(), wstr.size() }); + VERIFY_ARE_EQUAL(result, expected); + } + } + + TEST_METHOD(CanGetWidths) + { + const CodepointWidthDetector widthDetector; + for (const auto& data : testData) + { + const auto& expected = std::get<2>(data); + const auto& wstr = std::get<1>(data); + const auto result = widthDetector.GetWidth({ wstr.c_str(), wstr.size() }); + VERIFY_ARE_EQUAL(result, expected); + } + } + +}; diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp new file mode 100644 index 000000000..4dae58d90 --- /dev/null +++ b/src/host/ut_host/CommandLineTests.cpp @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "../../interactivity/inc/ServiceLocator.hpp" + +#include "../cmdline.h" + + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +constexpr size_t PROMPT_SIZE = 512; + +class CommandLineTests +{ + std::unique_ptr m_state; + CommandHistory* m_pHistory; + + TEST_CLASS(CommandLineTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->PrepareGlobalFont(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalFont(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + m_state->PrepareReadHandle(); + m_state->PrepareCookedReadData(); + m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", (HANDLE)0); + if (!m_pHistory) + { + return false; + } + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + CommandHistory::s_Free((HANDLE)0); + m_pHistory = nullptr; + m_state->CleanupCookedReadData(); + m_state->CleanupReadHandle(); + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + return true; + } + + void VerifyPromptText(COOKED_READ_DATA& cookedReadData, const std::wstring wstr) + { + const auto span = cookedReadData.SpanWholeBuffer(); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, wstr.size() * sizeof(wchar_t)); + for (size_t i = 0; i < wstr.size(); ++i) + { + VERIFY_ARE_EQUAL(span.at(i), wstr.at(i)); + } + } + + void InitCookedReadData(COOKED_READ_DATA& cookedReadData, + CommandHistory* pHistory, + wchar_t* pBuffer, + const size_t cchBuffer) + { + cookedReadData._commandHistory = pHistory; + cookedReadData._userBuffer = pBuffer; + cookedReadData._userBufferSize = cchBuffer * sizeof(wchar_t); + cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); + cookedReadData._backupLimit = pBuffer; + cookedReadData._bufPtr = pBuffer; + cookedReadData._exeName = L"cmd.exe"; + cookedReadData.OriginalCursorPosition() = { 0, 0 }; + } + + void SetPrompt(COOKED_READ_DATA& cookedReadData, const std::wstring text) + { + std::copy(text.begin(), text.end(), cookedReadData._backupLimit); + cookedReadData._bytesRead = text.size() * sizeof(wchar_t); + cookedReadData._currentPosition = text.size(); + cookedReadData._bufPtr += text.size(); + cookedReadData._visibleCharCount = text.size(); + } + + void MoveCursor(COOKED_READ_DATA& cookedReadData, const size_t column) + { + cookedReadData._currentPosition = column; + cookedReadData._bufPtr = cookedReadData._backupLimit + column; + } + + TEST_METHOD(CanCycleCommandHistory) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); + + auto& commandLine = CommandLine::Instance(); + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + // should not have anything on the prompt + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); + + // go back one history item + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + VerifyPromptText(cookedReadData, L"echo 3"); + + // try to go to the next history item, prompt shouldn't change + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + VerifyPromptText(cookedReadData, L"echo 3"); + + // go back another + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + VerifyPromptText(cookedReadData, L"echo 2"); + + // go forward + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + VerifyPromptText(cookedReadData, L"echo 3"); + + // go back two + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + VerifyPromptText(cookedReadData, L"echo 1"); + + // make sure we can't go back further + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + VerifyPromptText(cookedReadData, L"echo 1"); + + // can still go forward + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + VerifyPromptText(cookedReadData, L"echo 2"); + } + + TEST_METHOD(CanSetPromptToOldestHistory) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); + + auto& commandLine = CommandLine::Instance(); + commandLine._setPromptToOldestCommand(cookedReadData); + VerifyPromptText(cookedReadData, L"echo 1"); + + // change prompt and go back to oldest + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); + commandLine._setPromptToOldestCommand(cookedReadData); + VerifyPromptText(cookedReadData, L"echo 1"); + } + + TEST_METHOD(CanSetPromptToNewestHistory) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); + + auto& commandLine = CommandLine::Instance(); + commandLine._setPromptToNewestCommand(cookedReadData); + VerifyPromptText(cookedReadData, L"echo 3"); + + // change prompt and go back to newest + commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); + commandLine._setPromptToNewestCommand(cookedReadData); + VerifyPromptText(cookedReadData, L"echo 3"); + } + + TEST_METHOD(CanDeletePromptAfterCursor) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + auto& commandLine = CommandLine::Instance(); + // set current cursor position somewhere in the middle of the prompt + MoveCursor(cookedReadData, 4); + commandLine.DeletePromptAfterCursor(cookedReadData); + VerifyPromptText(cookedReadData, L"test"); + } + + TEST_METHOD(CanDeletePromptBeforeCursor) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + // set current cursor position somewhere in the middle of the prompt + MoveCursor(cookedReadData, 5); + auto& commandLine = CommandLine::Instance(); + const COORD cursorPos = commandLine._deletePromptBeforeCursor(cookedReadData); + cookedReadData._currentPosition = cursorPos.X; + VerifyPromptText(cookedReadData, L"word blah"); + } + + TEST_METHOD(CanMoveCursorToEndOfPrompt) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + // make sure the cursor is not at the start of the prompt + VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); + VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + + // save current position for later checking + const auto expectedCursorPos = cookedReadData._currentPosition; + const auto expectedBufferPos = cookedReadData._bufPtr; + + MoveCursor(cookedReadData, 0); + + auto& commandLine = CommandLine::Instance(); + const COORD cursorPos = commandLine._moveCursorToEndOfPrompt(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, gsl::narrow(expectedCursorPos)); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedCursorPos); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufferPos); + + } + + TEST_METHOD(CanMoveCursorToStartOfPrompt) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + // make sure the cursor is not at the start of the prompt + VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); + VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + + auto& commandLine = CommandLine::Instance(); + const COORD cursorPos = commandLine._moveCursorToStartOfPrompt(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, 0); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + } + + TEST_METHOD(CanMoveCursorLeftByWord) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + auto& commandLine = CommandLine::Instance(); + // cursor position at beginning of "blah" + short expectedPos = 10; + COORD cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); + + // move again + expectedPos = 5; // before "word" + cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); + + // move again + expectedPos = 0; // before "test" + cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); + + // try to move again, nothing should happen + cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); + } + + TEST_METHOD(CanMoveCursorLeft) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + const std::wstring expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + // move left from end of prompt text to the beginning of the prompt + auto& commandLine = CommandLine::Instance(); + for (auto it = expected.crbegin(); it != expected.crend(); ++it) + { + const COORD cursorPos = commandLine._moveCursorLeft(cookedReadData); + VERIFY_ARE_EQUAL(*cookedReadData._bufPtr, *it); + } + // should now be at the start of the prompt + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + + // try to move left a final time, nothing should change + const COORD cursorPos = commandLine._moveCursorLeft(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, 0); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + } + + /* + // TODO MSFT:11285829 tcome back and turn these on once the system cursor isn't needed + TEST_METHOD(CanMoveCursorRightByWord) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto expected = L"test word blah"; + SetPrompt(cookedReadData, expected); + VerifyPromptText(cookedReadData, expected); + + // save current position for later checking + const auto endCursorPos = cookedReadData._currentPosition; + const auto endBufferPos = cookedReadData._bufPtr; + // NOTE: need to initialize the actualy cursor and keep it up to date with the changes here. remove + once functions are fixed + // try to move right, nothing should happen + short expectedPos = gsl::narrow(endCursorPos); + COORD cursorPos = MoveCursorRightByWord(cookedReadData); + VERIFY_ARE_EQUAL(cursorPos.X, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedPos); + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, endBufferPos); + + // move to beginning of prompt and walk to the right by word + } + + TEST_METHOD(CanMoveCursorRight) + { + } + + TEST_METHOD(CanDeleteFromRightOfCursor) + { + } + + */ + + TEST_METHOD(CanInsertCtrlZ) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); + + auto& commandLine = CommandLine::Instance(); + commandLine._insertCtrlZ(cookedReadData); + VerifyPromptText(cookedReadData, L"\x1a"); // ctrl-z + } + + TEST_METHOD(CanDeleteCommandHistory) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); + + auto& commandLine = CommandLine::Instance(); + commandLine._deleteCommandHistory(cookedReadData); + VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), 0u); + } + + TEST_METHOD(CanFillPromptWithPreviousCommandFragment) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); + SetPrompt(cookedReadData, L"short and stout"); + + auto& commandLine = CommandLine::Instance(); + commandLine._fillPromptWithPreviousCommandFragment(cookedReadData); + VerifyPromptText(cookedReadData, L"short and stoutapot"); + } + + TEST_METHOD(CanCycleMatchingCommandHistory) + { + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + + auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"short and stout", false)); + VERIFY_SUCCEEDED(m_pHistory->Add(L"inflammable", false)); + + SetPrompt(cookedReadData, L"i"); + + auto& commandLine = CommandLine::Instance(); + commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); + VerifyPromptText(cookedReadData, L"inflammable"); + + // make sure we skip to the next "i" history item + commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); + VerifyPromptText(cookedReadData, L"I'm a little teapot"); + + // should cycle back to the start of the command history + commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); + VerifyPromptText(cookedReadData, L"inflammable"); + } + + TEST_METHOD(CmdlineCtrlHomeFullwidthChars) + { + Log::Comment(L"Set up buffers, create cooked read data, get screen information."); + auto buffer = std::make_unique(PROMPT_SIZE); + VERIFY_IS_NOT_NULL(buffer.get()); + auto& consoleInfo = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& screenInfo = consoleInfo.GetActiveOutputBuffer(); + auto& cookedReadData = consoleInfo.CookedReadData(); + InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); + + Log::Comment(L"Create Japanese text string and calculate the distance we expect the cursor to move."); + const std::wstring text(L"\x30ab\x30ac\x30ad\x30ae\x30af"); // katakana KA GA KI GI KU + const auto bufferSize = screenInfo.GetBufferSize(); + const auto cursorBefore = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + auto cursorAfterExpected = cursorBefore; + for (size_t i = 0; i < text.length() * 2; i++) + { + bufferSize.IncrementInBounds(cursorAfterExpected); + } + + Log::Comment(L"Write the text into the buffer using the cooked read structures as if it came off of someone's input."); + const auto written = cookedReadData.Write(text); + VERIFY_ARE_EQUAL(text.length(), written); + + Log::Comment(L"Retrieve the position of the cursor after insertion and check that it moved as far as we expected."); + const auto cursorAfter = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + VERIFY_ARE_EQUAL(cursorAfterExpected, cursorAfter); + + Log::Comment(L"Walk through the screen buffer data and ensure that the text we wrote filled the cells up as we expected (2 cells per fullwidth char)"); + { + auto cellIterator = screenInfo.GetCellDataAt(cursorBefore); + for (size_t i = 0; i < text.length() * 2; i++) + { + // Our original string was 5 wide characters which we expected to take 10 cells. + // Therefore each index of the original string will be used twice ( divide by 2 ). + const auto expectedTextValue = text.at(i / 2); + const String expectedText(&expectedTextValue, 1); + + const auto actualTextValue = cellIterator->Chars(); + const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); + + VERIFY_ARE_EQUAL(expectedText, actualText); + cellIterator++; + } + } + + Log::Comment(L"Now perform the command that is triggered with Ctrl+Home keys normally to erase the entire edit line."); + auto& commandLine = CommandLine::Instance(); + commandLine._deletePromptBeforeCursor(cookedReadData); + + Log::Comment(L"Check that the entire span of the buffer where we had the fullwidth text is now cleared out and full of blanks (nothing left behind)."); + { + auto cursorPos = cursorBefore; + auto cellIterator = screenInfo.GetCellDataAt(cursorPos); + + while (Utils::s_CompareCoords(cursorPos, cursorAfter) < 0) + { + const String expectedText(L"\x20"); // unicode space character + + const auto actualTextValue = cellIterator->Chars(); + const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); + + VERIFY_ARE_EQUAL(expectedText, actualText); + cellIterator++; + + bufferSize.IncrementInBounds(cursorPos); + } + } + } +}; diff --git a/src/host/ut_host/CommandListPopupTests.cpp b/src/host/ut_host/CommandListPopupTests.cpp new file mode 100644 index 000000000..c10f185c5 --- /dev/null +++ b/src/host/ut_host/CommandListPopupTests.cpp @@ -0,0 +1,550 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "../../interactivity/inc/ServiceLocator.hpp" + +#include "../CommandListPopup.hpp" +#include "PopupTestHelper.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +static constexpr size_t BUFFER_SIZE = 256; +static constexpr UINT s_NumberOfHistoryBuffers = 4; +static constexpr UINT s_HistoryBufferSize = 50; + +class CommandListPopupTests +{ + TEST_CLASS(CommandListPopupTests); + + std::unique_ptr m_state; + CommandHistory* m_pHistory; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->PrepareGlobalFont(); + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetNumberOfHistoryBuffers(s_NumberOfHistoryBuffers); + gci.SetHistoryBufferSize(s_HistoryBufferSize); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalFont(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + m_state->PrepareReadHandle(); + m_state->PrepareCookedReadData(); + m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", (HANDLE)0); + // resize command history storage to 50 items so that we don't cycle on accident + // when PopupTestHelper::InitLongHistory() is called. + CommandHistory::s_ResizeAll(50); + if (!m_pHistory) + { + return false; + } + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + CommandHistory::s_Free((HANDLE)0); + m_pHistory = nullptr; + m_state->CleanupCookedReadData(); + m_state->CleanupReadHandle(); + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + return true; + } + + void InitReadData(COOKED_READ_DATA& cookedReadData, + wchar_t* const pBuffer, + const size_t cchBuffer, + const size_t cursorPosition) + { + cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); + cookedReadData._bufPtr = pBuffer + cursorPosition; + cookedReadData._backupLimit = pBuffer; + cookedReadData.OriginalCursorPosition() = { 0, 0 }; + cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); + cookedReadData._currentPosition = cursorPosition; + cookedReadData.VisibleCharCount() = cursorPosition; + } + + TEST_METHOD(CanDismiss) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + modifiers = 0; + wch = VK_ESCAPE; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + const std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should not be changed + const std::wstring resultString(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(testString, resultString); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); + + // popup has been dismissed + VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); + } + + TEST_METHOD(UpMovesSelection) + { + // function to simulate user pressing up arrow + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_UP; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + const short commandNumberBefore = popup._currentCommand; + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved up one line + VERIFY_ARE_EQUAL(commandNumberBefore - 1, popup._currentCommand); + } + + TEST_METHOD(DownMovesSelection) + { + // function to simulate user pressing down arrow + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_DOWN; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + // set the current command selection to the top of the list + popup._currentCommand = 0; + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + const short commandNumberBefore = popup._currentCommand; + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved down one line + VERIFY_ARE_EQUAL(commandNumberBefore + 1, popup._currentCommand); + } + + TEST_METHOD(EndMovesSelectionToEnd) + { + // function to simulate user pressing end key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_END; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + // set the current command selection to the top of the list + popup._currentCommand = 0; + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved to the bottom line + VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands() - 1, gsl::narrow(popup._currentCommand)); + } + + TEST_METHOD(HomeMovesSelectionToStart) + { + // function to simulate user pressing home key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_HOME; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved to the bottom line + VERIFY_ARE_EQUAL(0, popup._currentCommand); + } + + TEST_METHOD(PageUpMovesSelection) + { + // function to simulate user pressing page up key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_PRIOR; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitLongHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved up a page + VERIFY_ARE_EQUAL(static_cast(m_pHistory->GetNumberOfCommands()) - popup.Height() - 1, popup._currentCommand); + } + + TEST_METHOD(PageDownMovesSelection) + { + // function to simulate user pressing page down key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_NEXT; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitLongHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + // set the current command selection to the top of the list + popup._currentCommand = 0; + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // selection should have moved up a page + VERIFY_ARE_EQUAL(popup.Height(), popup._currentCommand); + } + + TEST_METHOD(SideArrowsFillsPrompt) + { + // function to simulate user pressing right arrow key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + wch = VK_RIGHT; + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + // set the current command selection to the top of the list + popup._currentCommand = 0; + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + // prompt should have history item in prompt + const std::wstring_view historyItem = m_pHistory->GetLastCommand(); + const std::wstring_view resultText{ buffer, historyItem.size() }; + VERIFY_ARE_EQUAL(historyItem, resultText); + } + + TEST_METHOD(CanLaunchCommandNumberPopup) + { + // function to simulate user pressing F9 + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + wch = VK_F9; + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + auto& commandLine = CommandLine::Instance(); + VERIFY_IS_FALSE(commandLine.HasPopup()); + // should spawn a CommandNumberPopup + auto scopeExit = wil::scope_exit([&]() { commandLine.EndAllPopups(); }); + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT)); + VERIFY_IS_TRUE(commandLine.HasPopup()); + + } + + TEST_METHOD(CanDeleteFromCommandHistory) + { + // function to simulate user pressing the delete key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_DELETE; + firstTime = false; + } + else + { + wch = VK_ESCAPE; + } + popupKey = true; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + const size_t startHistorySize = m_pHistory->GetNumberOfCommands(); + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), startHistorySize - 1); + } + + TEST_METHOD(CanReorderHistoryUp) + { + // function to simulate user pressing shift + up arrow + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static bool firstTime = true; + if (firstTime) + { + wch = VK_UP; + firstTime = false; + modifiers = SHIFT_PRESSED; + } + else + { + wch = VK_ESCAPE; + modifiers = 0; + } + popupKey = true; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); + VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); + } + + TEST_METHOD(CanReorderHistoryDown) + { + // function to simulate user pressing the up arrow, then shift + down arrow, then escape + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static unsigned int count = 0; + if (count == 0) + { + wch = VK_UP; + modifiers = 0; + } + else if (count == 1) + { + wch = VK_DOWN; + modifiers = SHIFT_PRESSED; + } + else + { + wch = VK_ESCAPE; + modifiers = 0; + } + popupKey = true; + ++count; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + PopupTestHelper::InitHistory(*m_pHistory); + CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); + VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); + } +}; diff --git a/src/host/ut_host/CommandNumberPopupTests.cpp b/src/host/ut_host/CommandNumberPopupTests.cpp new file mode 100644 index 000000000..e61c90426 --- /dev/null +++ b/src/host/ut_host/CommandNumberPopupTests.cpp @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" +#include "PopupTestHelper.hpp" + +#include "../../interactivity/inc/ServiceLocator.hpp" + +#include "../CommandNumberPopup.hpp" +#include "../CommandListPopup.hpp" + + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +static constexpr size_t BUFFER_SIZE = 256; + +class CommandNumberPopupTests +{ + TEST_CLASS(CommandNumberPopupTests); + + std::unique_ptr m_state; + CommandHistory* m_pHistory; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->PrepareGlobalFont(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalFont(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + m_state->PrepareReadHandle(); + m_state->PrepareCookedReadData(); + m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", (HANDLE)0); + if (!m_pHistory) + { + return false; + } + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + CommandHistory::s_Free((HANDLE)0); + m_pHistory = nullptr; + m_state->CleanupCookedReadData(); + m_state->CleanupReadHandle(); + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + return true; + } + + TEST_METHOD(CanDismiss) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = VK_ESCAPE; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + const std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should not be changed + const std::wstring resultString(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(testString, resultString); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); + + // popup has been dismissed + VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); + } + + TEST_METHOD(CanDismissAllPopups) + { + Log::Comment(L"that that all popups are dismissed when CommandNumberPopup is dismissed"); + // CommanNumberPopup is the only popup that can act as a 2nd popup. make sure that it dismisses all + // popups when exiting + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = VK_ESCAPE; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // add popups to CommandLine + auto& commandLine = CommandLine::Instance(); + commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer(), *m_pHistory)); + commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer())); + auto& numberPopup = *commandLine._popups.front(); + numberPopup.SetUserInputFunction(fn); + + VERIFY_ARE_EQUAL(commandLine._popups.size(), 2u); + + // prepare cookedReadData + const std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(numberPopup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + VERIFY_IS_FALSE(commandLine.HasPopup()); + + } + + TEST_METHOD(EmptyInputCountsAsOldestHistory) + { + Log::Comment(L"hitting enter with no input should grab the oldest history item"); + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = false; + wch = UNICODE_CARRIAGERETURN; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should contain the least recent history item + + const std::wstring_view expected = m_pHistory->GetLastCommand(); + const std::wstring resultString(buffer, buffer + expected.size()); + VERIFY_ARE_EQUAL(expected, resultString); + } + + TEST_METHOD(CanSelectHistoryItem) + { + PopupTestHelper::InitHistory(*m_pHistory); + for (unsigned int historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) + { + Popup::UserInputFunction fn = [historyIndex](COOKED_READ_DATA& /*cookedReadData*/, + bool& popupKey, + DWORD& modifiers, + wchar_t& wch) + { + static bool needReturn = false; + popupKey = false; + modifiers = 0; + if (!needReturn) + { + const auto str = std::to_string(historyIndex); + wch = str.at(0); + needReturn = true; + } + else + { + wch = UNICODE_CARRIAGERETURN; + needReturn = false; + } + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should contain the correct nth history item + + const auto expected = m_pHistory->GetNth(gsl::narrow(historyIndex)); + const std::wstring resultString(buffer, buffer + expected.size()); + VERIFY_ARE_EQUAL(expected, resultString); + } + } + + TEST_METHOD(LargeNumberGrabsNewestHistoryItem) + { + Log::Comment(L"entering a number larger than the number of history items should grab the most recent history item"); + + // simulates user pressing 1, 2, 3, 4, 5, enter + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + static int num = 1; + popupKey = false; + modifiers = 0; + if (num <= 5) + { + const auto str = std::to_string(num); + wch = str.at(0); + ++num; + } + else + { + wch = UNICODE_CARRIAGERETURN; + } + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should contain the most recent history item + + const std::wstring_view expected = m_pHistory->GetLastCommand(); + const std::wstring resultString(buffer, buffer + expected.size()); + VERIFY_ARE_EQUAL(expected, resultString); + } + + TEST_METHOD(InputIsLimited) + { + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; + + // input can't delete past zero number input + popup._pop(); + VERIFY_ARE_EQUAL(popup._parse(), 0); + + // input can only be numbers + VERIFY_THROWS_SPECIFIC(popup._push(L'$'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + VERIFY_THROWS_SPECIFIC(popup._push(L'A'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + + // input can't be more than 5 numbers + popup._push(L'1'); + VERIFY_ARE_EQUAL(popup._parse(), 1); + popup._push(L'2'); + VERIFY_ARE_EQUAL(popup._parse(), 12); + popup._push(L'3'); + VERIFY_ARE_EQUAL(popup._parse(), 123); + popup._push(L'4'); + VERIFY_ARE_EQUAL(popup._parse(), 1234); + popup._push(L'5'); + VERIFY_ARE_EQUAL(popup._parse(), 12345); + // this shouldn't affect the parsed number + popup._push(L'6'); + VERIFY_ARE_EQUAL(popup._parse(), 12345); + // make sure we can delete input correctly + popup._pop(); + VERIFY_ARE_EQUAL(popup._parse(), 1234); + } +}; diff --git a/src/host/ut_host/ConsoleArgumentsTests.cpp b/src/host/ut_host/ConsoleArgumentsTests.cpp new file mode 100644 index 000000000..8338b46ed --- /dev/null +++ b/src/host/ut_host/ConsoleArgumentsTests.cpp @@ -0,0 +1,1124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "globals.h" +#include "../ConsoleArguments.hpp" +#include "../../types/inc/utils.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace Microsoft::Console::Utils; + +class ConsoleArgumentsTests +{ +public: + TEST_CLASS(ConsoleArgumentsTests); + + TEST_METHOD(ArgSplittingTests); + TEST_METHOD(ClientCommandlineTests); + TEST_METHOD(LegacyFormatsTests); + + TEST_METHOD(IsUsingVtHandleTests); + TEST_METHOD(CombineVtPipeHandleTests); + TEST_METHOD(IsVtHandleValidTests); + + TEST_METHOD(InitialSizeTests); + + TEST_METHOD(HeadlessArgTests); + TEST_METHOD(SignalHandleTests); + TEST_METHOD(FeatureArgTests); + +}; + +ConsoleArguments CreateAndParse(std::wstring& commandline, HANDLE hVtIn, HANDLE hVtOut) +{ + ConsoleArguments args = ConsoleArguments(commandline, hVtIn, hVtOut); + VERIFY_SUCCEEDED(args.ParseCommandline()); + return args; +} + +// Used when you expect args to be invalid +ConsoleArguments CreateAndParseUnsuccessfully(std::wstring& commandline, HANDLE hVtIn, HANDLE hVtOut) +{ + ConsoleArguments args = ConsoleArguments(commandline, hVtIn, hVtOut); + VERIFY_FAILED(args.ParseCommandline()); + return args; +} + +void ArgTestsRunner(LPCWSTR comment, std::wstring commandline, HANDLE hVtIn, HANDLE hVtOut, const ConsoleArguments& expected, bool shouldBeSuccessful) +{ + Log::Comment(comment); + Log::Comment(commandline.c_str()); + const ConsoleArguments actual = shouldBeSuccessful ? + CreateAndParse(commandline, hVtIn, hVtOut) : + CreateAndParseUnsuccessfully(commandline, hVtIn, hVtOut); + + VERIFY_ARE_EQUAL(expected, actual); +} + +void ConsoleArgumentsTests::ArgSplittingTests() +{ + std::wstring commandline; + + commandline = L"conhost.exe --headless this is the commandline"; + ArgTestsRunner(L"#1 look for a valid commandline", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"this is the commandline", // clientCommandLine, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe \"this is the commandline\""; + ArgTestsRunner(L"#2 a commandline with quotes", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"this is the commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless \"--vtmode bar this is the commandline\""; + ArgTestsRunner(L"#3 quotes on an arg", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"--vtmode bar this is the commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless --server 0x4 this is the commandline"; + ArgTestsRunner(L"#4 Many spaces", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"this is the commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + false, // createServerHandle + 0x4, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless\t--vtmode\txterm\tthis\tis\tthe\tcommandline"; + ArgTestsRunner(L"#5\ttab\tdelimit", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"this is the commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"xterm", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless\\ foo\\ --outpipe\\ bar\\ this\\ is\\ the\\ commandline"; + ArgTestsRunner(L"#6 back-slashes won't escape spaces", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"--headless\\ foo\\ --outpipe\\ bar\\ this\\ is\\ the\\ commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless\\\tfoo\\\t--outpipe\\\tbar\\\tthis\\\tis\\\tthe\\\tcommandline"; + ArgTestsRunner(L"#7 back-slashes won't escape tabs (but the tabs are still converted to spaces)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"--headless\\ foo\\ --outpipe\\ bar\\ this\\ is\\ the\\ commandline", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --vtmode a\\\\\\\\\"b c\" d e"; + ArgTestsRunner(L"#8 Combo of backslashes and quotes from msdn", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"d e", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"a\\\\b c", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? +} + +void ConsoleArgumentsTests::ClientCommandlineTests() +{ + std::wstring commandline; + + commandline = L"conhost.exe -- foo"; + ArgTestsRunner(L"#1 Check that a simple explicit commandline is found", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"foo", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe foo"; + ArgTestsRunner(L"#2 Check that a simple implicit commandline is found", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"foo", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe foo -- bar"; + ArgTestsRunner(L"#3 Check that a implicit commandline with other expected args is treated as a whole client commandline (1)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"foo -- bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --vtmode foo foo -- bar"; + ArgTestsRunner(L"#4 Check that a implicit commandline with other expected args is treated as a whole client commandline (2)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"foo -- bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"foo", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe console --vtmode foo foo -- bar"; + ArgTestsRunner(L"#5 Check that a implicit commandline with other expected args is treated as a whole client commandline (3)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"console --vtmode foo foo -- bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe console --vtmode foo --outpipe foo -- bar"; + ArgTestsRunner(L"#6 Check that a implicit commandline with other expected args is treated as a whole client commandline (4)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"console --vtmode foo --outpipe foo -- bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --vtmode foo -- --outpipe foo bar"; + ArgTestsRunner(L"#7 Check splitting vt pipes across the explicit commandline does not pull both pipe names out", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"--outpipe foo bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"foo", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --vtmode -- --headless bar"; + ArgTestsRunner(L"#8 Let -- be used as a value of a parameter", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"bar", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"--", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --"; + ArgTestsRunner(L"#9 -- by itself does nothing successfully", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe"; + ArgTestsRunner(L"#10 An empty commandline should parse as an empty commandline", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? +} + +void ConsoleArgumentsTests::LegacyFormatsTests() +{ + std::wstring commandline; + + commandline = L"conhost.exe 0x4"; + ArgTestsRunner(L"#1 Check that legacy launch mechanisms via the system loader with a server handle ID work", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --server 0x4"; + ArgTestsRunner(L"#2 Check that launch mechanism with parameterized server handle ID works", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe 0x4 0x8"; + ArgTestsRunner(L"#3 Check that two handle IDs fails (1)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --server 0x4 0x8"; + ArgTestsRunner(L"#4 Check that two handle IDs fails (2)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe 0x4 --server 0x8"; + ArgTestsRunner(L"#5 Check that two handle IDs fails (3)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --server 0x4 --server 0x8"; + ArgTestsRunner(L"#6 Check that two handle IDs fails (4)", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe 0x4 -ForceV1"; + ArgTestsRunner(L"#7 Check that ConDrv handle + -ForceV1 succeeds", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + true, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe -ForceV1"; + ArgTestsRunner(L"#8 Check that -ForceV1 parses on its own", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + true, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? +} + +void ConsoleArgumentsTests::IsUsingVtHandleTests() +{ + ConsoleArguments args(L"", INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE); + VERIFY_IS_FALSE(args.HasVtHandles()); + + // Just some assorted positive values that could be valid handles. No specific correlation to anything. + args._vtInHandle = UlongToHandle(0x12); + VERIFY_IS_FALSE(args.HasVtHandles()); + + args._vtOutHandle = UlongToHandle(0x16); + VERIFY_IS_TRUE(args.HasVtHandles()); + + args._vtInHandle = UlongToHandle(0ul); + VERIFY_IS_FALSE(args.HasVtHandles()); + + args._vtInHandle = UlongToHandle(0x20); + args._vtOutHandle = UlongToHandle(0ul); + VERIFY_IS_FALSE(args.HasVtHandles()); +} + +void ConsoleArgumentsTests::CombineVtPipeHandleTests() +{ + std::wstring commandline; + + // Just some assorted positive values that could be valid handles. No specific correlation to anything. + HANDLE hInSample = UlongToHandle(0x10); + HANDLE hOutSample = UlongToHandle(0x24); + + commandline = L"conhost.exe"; + ArgTestsRunner(L"#1 Check that handles with no mode is OK", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", // clientCommandLine + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --vtmode telnet"; + ArgTestsRunner(L"#2 Check that handles with mode is OK", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", // clientCommandLine + hInSample, + hOutSample, + L"telnet", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? +} + +void ConsoleArgumentsTests::IsVtHandleValidTests() +{ + // We use both 0 and INVALID_HANDLE_VALUE as invalid handles since we're not sure + // exactly what will get passed in on the STDIN/STDOUT handles as it can vary wildly + // depending on who is passing it. + VERIFY_IS_FALSE(IsValidHandle(0), L"Zero handle invalid."); + VERIFY_IS_FALSE(IsValidHandle(INVALID_HANDLE_VALUE), L"Invalid handle invalid."); + VERIFY_IS_TRUE(IsValidHandle(UlongToHandle(0x4)), L"0x4 is valid."); +} + +void ConsoleArgumentsTests::InitialSizeTests() +{ + std::wstring commandline; + + commandline = L"conhost.exe --width 120 --height 30"; + ArgTestsRunner(L"#1 look for a valid commandline with both width and height", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 120, // width + 30, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --width 120"; + ArgTestsRunner(L"#2 look for a valid commandline with only width", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 120, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --height 30"; + ArgTestsRunner(L"#3 look for a valid commandline with only height", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 30, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --width 0"; + ArgTestsRunner(L"#4 look for a valid commandline passing 0", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --width -1"; + ArgTestsRunner(L"#5 look for a valid commandline passing -1", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + -1, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --width foo"; + ArgTestsRunner(L"#6 look for an ivalid commandline passing a string", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --width 2foo"; + ArgTestsRunner(L"#7 look for an ivalid commandline passing a string with a number at the start", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --width 65535"; + ArgTestsRunner(L"#7 look for an ivalid commandline passing a value that's too big", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + false); // successful parse? + +} + +void ConsoleArgumentsTests::HeadlessArgTests() +{ + std::wstring commandline; + + commandline = L"conhost.exe --headless"; + ArgTestsRunner(L"#1 Check that the headless arg works", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless 0x4"; + ArgTestsRunner(L"#2 Check that headless arg works with the server param", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + false, // createServerHandle + 4ul, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --headless --headless"; + ArgTestsRunner(L"#3 multiple --headless params are all treated as one", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + true, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe -- foo.exe --headless"; + ArgTestsRunner(L"#4 ---headless as a client commandline does not make us headless", + commandline, + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + ConsoleArguments(commandline, + L"foo.exe --headless", // clientCommandLine + INVALID_HANDLE_VALUE, + INVALID_HANDLE_VALUE, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false ), // inheritCursor + true); // successful parse? +} + +void ConsoleArgumentsTests::SignalHandleTests() +{ + // Just some assorted positive values that could be valid handles. No specific correlation to anything. + HANDLE hInSample = UlongToHandle(0x10); + HANDLE hOutSample = UlongToHandle(0x24); + + std::wstring commandline; + + commandline = L"conhost.exe --server 0x4 --signal 0x8"; + ArgTestsRunner(L"#1 Normal case, pass both server and signal handle", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 8ul, // signalHandle + false), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --server 0x4 --signal ASDF"; + ArgTestsRunner(L"#2 Pass bad signal handle", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + false, // createServerHandle + 4ul, // serverHandle + 0ul, // signalHandle + false), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --signal --server 0x4"; + ArgTestsRunner(L"#3 Pass null signal handle", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0ul, // serverHandle + 0ul, // signalHandle + false), // inheritCursor + false); // successful parse? +} + +void ConsoleArgumentsTests::FeatureArgTests() +{ + // Just some assorted positive values that could be valid handles. No specific correlation to anything. + HANDLE hInSample = UlongToHandle(0x10); + HANDLE hOutSample = UlongToHandle(0x24); + + std::wstring commandline; + + commandline = L"conhost.exe --feature pty"; + ArgTestsRunner(L"#1 Normal case, pass a supported feature", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + true); // successful parse? + commandline = L"conhost.exe --feature tty"; + ArgTestsRunner(L"#2 Error case, pass an unsupported feature", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --feature pty --feature pty"; + ArgTestsRunner(L"#3 Many supported features", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + true); // successful parse? + + commandline = L"conhost.exe --feature pty --feature tty"; + ArgTestsRunner(L"#4 At least one unsupported feature", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --feature pty --feature"; + ArgTestsRunner(L"#5 no value to the feature flag", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + false); // successful parse? + + commandline = L"conhost.exe --feature pty --feature --signal foo"; + ArgTestsRunner(L"#6 a invalid feature value that is otherwise a valid arg", + commandline, + hInSample, + hOutSample, + ConsoleArguments(commandline, + L"", + hInSample, + hOutSample, + L"", // vtMode + 0, // width + 0, // height + false, // forceV1 + false, // headless + true, // createServerHandle + 0, // serverHandle + 0, // signalHandle + false), // inheritCursor + false); // successful parse? +} diff --git a/src/host/ut_host/CopyFromCharPopupTests.cpp b/src/host/ut_host/CopyFromCharPopupTests.cpp new file mode 100644 index 000000000..ab7db21b5 --- /dev/null +++ b/src/host/ut_host/CopyFromCharPopupTests.cpp @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" +#include "PopupTestHelper.hpp" + +#include "../../interactivity/inc/ServiceLocator.hpp" + +#include "../CopyFromCharPopup.hpp" + + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +static constexpr size_t BUFFER_SIZE = 256; + +class CopyFromCharPopupTests +{ + TEST_CLASS(CopyFromCharPopupTests); + + std::unique_ptr m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->PrepareGlobalFont(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalFont(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + m_state->PrepareReadHandle(); + m_state->PrepareCookedReadData(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupCookedReadData(); + m_state->CleanupReadHandle(); + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + return true; + } + + TEST_METHOD(CanDismiss) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = VK_ESCAPE; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should not be changed + std::wstring resultString(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(testString, resultString); + VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), testString.size() * sizeof(wchar_t)); + + // popup has been dismissed + VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); + } + + TEST_METHOD(DeleteAllWhenCharNotFound) + { + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = false; + wch = L'x'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + // move cursor to beginning of prompt text + cookedReadData.InsertionPoint() = 0; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // all text to the right of the cursor should be gone + VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), 0u); + } + + TEST_METHOD(CanDeletePartialLine) + { + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = false; + wch = L'f'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + std::wstring testString = L"By the rude bridge that arched the flood"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + // move cursor to index 12 + const size_t index = 12; + cookedReadData.SetBufferCurrentPtr(buffer + index); + cookedReadData.InsertionPoint() = index; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + std::wstring expectedText = L"By the rude flood"; + VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), expectedText.size() * sizeof(wchar_t)); + std::wstring resultText(buffer, buffer + expectedText.size()); + VERIFY_ARE_EQUAL(resultText, expectedText); + } +}; diff --git a/src/host/ut_host/CopyToCharPopupTests.cpp b/src/host/ut_host/CopyToCharPopupTests.cpp new file mode 100644 index 000000000..0711fba90 --- /dev/null +++ b/src/host/ut_host/CopyToCharPopupTests.cpp @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" +#include "PopupTestHelper.hpp" + +#include "../../interactivity/inc/ServiceLocator.hpp" + +#include "../CopyToCharPopup.hpp" + + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +static constexpr size_t BUFFER_SIZE = 256; + +class CopyToCharPopupTests +{ + TEST_CLASS(CopyToCharPopupTests); + + std::unique_ptr m_state; + CommandHistory* m_pHistory; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->PrepareGlobalFont(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalFont(); + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + m_state->PrepareReadHandle(); + m_state->PrepareCookedReadData(); + m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", (HANDLE)0); + if (!m_pHistory) + { + return false; + } + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + CommandHistory::s_Free((HANDLE)0); + m_pHistory = nullptr; + m_state->CleanupCookedReadData(); + m_state->CleanupReadHandle(); + m_state->CleanupGlobalInputBuffer(); + m_state->CleanupGlobalScreenBuffer(); + return true; + } + + TEST_METHOD(CanDismiss) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = VK_ESCAPE; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + const std::wstring testString = L"hello world"; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should not be changed + const std::wstring resultString(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(testString, resultString); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); + + // popup has been dismissed + VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); + } + + TEST_METHOD(NothingHappensWhenCharNotFound) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = L'x'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // the buffer should not be changed + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); + } + + TEST_METHOD(CanCopyToEmptyPrompt) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = L's'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + const std::wstring expectedText = L"here i"; + + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedText.size()); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedText.size() * sizeof(wchar_t)); + + // make sure that the text matches + const std::wstring resultText(buffer, buffer + expectedText.size()); + VERIFY_ARE_EQUAL(resultText, expectedText); + // make sure that more wasn't copied + VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); + } + + TEST_METHOD(WontCopyTextBeforeCursor) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = L's'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData with a string longer than the previous history + const std::wstring testString = L"Whose woods there are I think I know."; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + const wchar_t* const expectedBufPtr = cookedReadData._bufPtr; + const size_t expectedBytesRead = cookedReadData._bytesRead; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + // nothing should have changed + VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufPtr); + VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedBytesRead); + const std::wstring resultText(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(resultText, testString); + // make sure that more wasn't copied + VERIFY_ARE_EQUAL(buffer[testString.size()], UNICODE_SPACE); + + } + + TEST_METHOD(CanMergeLine) + { + // function to simulate user pressing escape key + Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) + { + popupKey = true; + wch = L's'; + modifiers = 0; + return STATUS_SUCCESS; + }; + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // prepare popup + CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; + popup.SetUserInputFunction(fn); + + // prepare cookedReadData with a string longer than the previous history + const std::wstring testString = L"fear "; + wchar_t buffer[BUFFER_SIZE]; + std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); + std::copy(testString.begin(), testString.end(), std::begin(buffer)); + auto& cookedReadData = gci.CookedReadData(); + PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); + PopupTestHelper::InitHistory(*m_pHistory); + cookedReadData._commandHistory = m_pHistory; + + VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); + + const std::wstring expectedText = L"fear is"; + const std::wstring resultText(buffer, buffer + testString.size()); + VERIFY_ARE_EQUAL(resultText, testString); + // make sure that more wasn't copied + VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); + } + +}; diff --git a/src/host/ut_host/DbcsTests.cpp b/src/host/ut_host/DbcsTests.cpp new file mode 100644 index 000000000..9f260410c --- /dev/null +++ b/src/host/ut_host/DbcsTests.cpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" +#include "../buffer/out/textBuffer.hpp" + +#include "dbcs.h" + +#include "input.h" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class DbcsTests +{ + TEST_CLASS(DbcsTests); + + TEST_METHOD(TestUnicodeRasterFontCellMungeOnRead) + { + const size_t cchTestSize = 20; + + // create test array of 20 characters + CHAR_INFO rgci[cchTestSize]; + + // pick a color to use for attributes to ensure it's preserved. + WORD const wAttrTest = FOREGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_INTENSITY; + + // target array will look like + // abcdeLTLTLTLTLTpqrst + // where L is a leading half of a double-wide char sequence + // and T is the trailing half of a double-wide char sequence + + // fill ASCII characters first by looping and + // incrementing. we'll cover up the middle later + WCHAR wch = L'a'; + for (size_t i = 0; i < ARRAYSIZE(rgci); i++) + { + rgci[i].Char.UnicodeChar = wch; + rgci[i].Attributes = wAttrTest; + wch++; + } + + // we're going to do katakana KA, GA, KI, GI, KU + // for the double wide characters. + WCHAR wchDouble = 0x30AB; + for (size_t i = 5; i < 15; i += 2) + { + rgci[i].Char.UnicodeChar = wchDouble; + rgci[i].Attributes = COMMON_LVB_LEADING_BYTE | wAttrTest; + rgci[i + 1].Char.UnicodeChar = wchDouble; + rgci[i + 1].Attributes = COMMON_LVB_TRAILING_BYTE | wAttrTest; + wchDouble++; + } + + const gsl::span buffer(rgci, ARRAYSIZE(rgci)); + + // feed it into UnicodeRasterFontCellMungeOnRead to confirm that it is working properly. + // do it in-place to confirm that it can operate properly in the common case. + DWORD dwResult = UnicodeRasterFontCellMungeOnRead(buffer); + + // the final length returned should be the same as the length we started with + if (VERIFY_ARE_EQUAL(ARRAYSIZE(rgci), dwResult, L"Ensure the length claims that we are the same before and after.")) + { + Log::Comment(L"Ensure the letters are now as expected."); + // the expected behavior is to reduce the LEADING/TRAILING double copies into a single copy + WCHAR wchExpected[]{ 'a', 'b', 'c', 'd', 'e', 0x30AB, 0x30AC, 0x30AD, 0x30AE, 0x30AF, 'p', 'q', 'r', 's', 't' }; + for (size_t i = 0; i < ARRAYSIZE(wchExpected); i++) + { + VERIFY_ARE_EQUAL(wchExpected[i], rgci[i].Char.UnicodeChar); + + // and simultaneously strip the LEADING/TRAILING attributes + // no other attributes should be affected (test against color flags we set). + VERIFY_ARE_EQUAL(wAttrTest, rgci[i].Attributes); + } + + // and all extra portions of the array should be zeroed. + for (size_t i = ARRAYSIZE(wchExpected); i < ARRAYSIZE(rgci); i++) + { + VERIFY_ARE_EQUAL(rgci[i].Char.UnicodeChar, 0); + VERIFY_ARE_EQUAL(rgci[i].Attributes, 0); + } + } + + } +}; diff --git a/src/host/ut_host/DefaultResource.rc b/src/host/ut_host/DefaultResource.rc new file mode 100644 index 000000000..85ec2648d --- /dev/null +++ b/src/host/ut_host/DefaultResource.rc @@ -0,0 +1,12 @@ +//Autogenerated file name + version resource file for Device Guard whitelisting effort + +#include +#include + +#define VER_FILETYPE VFT_UNKNOWN +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR ___TARGETNAME +#define VER_INTERNALNAME_STR ___TARGETNAME +#define VER_ORIGINALFILENAME_STR ___TARGETNAME + +#include "common.ver" diff --git a/src/host/ut_host/HistoryTests.cpp b/src/host/ut_host/HistoryTests.cpp new file mode 100644 index 000000000..23a4e9490 --- /dev/null +++ b/src/host/ut_host/HistoryTests.cpp @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "search.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class HistoryTests +{ + TEST_CLASS(HistoryTests); + + TEST_CLASS_SETUP(ClassSetup) + { + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetNumberOfHistoryBuffers(s_NumberOfBuffers); + gci.SetHistoryBufferSize(s_BufferSize); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + // Get a fresh storage for each test since it's stored internally as a persistent static for the lifetime of the session. + CommandHistory::s_ClearHistoryListStorage(); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + return true; + } + + TEST_METHOD(AllocateAndFreeOneApp) + { + const std::wstring app{ L"testapp1.exe" }; + const HANDLE handle = _MakeHandle(0); + + const auto history = CommandHistory::s_Allocate(app, handle); + VERIFY_IS_NOT_NULL(history); + + VERIFY_IS_TRUE(WI_IsFlagSet(history->Flags, CLE_ALLOCATED)); + VERIFY_ARE_EQUAL(1ul, CommandHistory::s_historyLists.size()); + + CommandHistory::s_Free(handle); + // We preserve the app history list for re-use if it reattaches in this session and doesn't age out. + VERIFY_IS_TRUE(WI_IsFlagClear(history->Flags, CLE_ALLOCATED), L"Shouldn't actually be gone, just deallocated."); + VERIFY_ARE_EQUAL(1ul, CommandHistory::s_historyLists.size()); + } + + TEST_METHOD(AllocateTooManyApps) + { + VERIFY_IS_LESS_THAN(s_NumberOfBuffers, _manyApps.size(), L"Make sure we declared too many apps for the necessary size."); + + for (size_t i = 0; i < _manyApps.size(); i++) + { + CommandHistory::s_Allocate(_manyApps[i], _MakeHandle(i)); + } + + VERIFY_ARE_EQUAL(s_NumberOfBuffers, CommandHistory::s_CountOfHistories(), L"We should have maxed out histories."); + + Log::Comment(L"Since they were all in use, the last app shouldn't have made an entry"); + for (size_t i = 0; i < _manyApps.size() - 1; i++) + { + VERIFY_IS_NOT_NULL(CommandHistory::s_FindByExe(_manyApps[i])); + } + + VERIFY_IS_NULL(CommandHistory::s_FindByExe(_manyApps[4]), L"Verify we can't find the last app."); + } + + TEST_METHOD(EnsureHistoryRestoredAfterClientLeavesAndRejoins) + { + const HANDLE h = _MakeHandle(0); + Log::Comment(L"Allocate a history and fill it with items."); + auto history = CommandHistory::s_Allocate(_manyApps[0], h); + VERIFY_IS_NOT_NULL(history); + + for (size_t i = 0; i < s_BufferSize; i++) + { + VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[i], false)); + } + + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands(), L"Ensure that it is filled."); + + Log::Comment(L"Free it and recreate it with the same name."); + CommandHistory::s_Free(h); + + // Using a different handle on purpose. Handle shouldn't matter. + history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(14)); + VERIFY_IS_NOT_NULL(history); + + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands(), L"Ensure that we still have full commands after freeing and reallocating, same app name, different handle ID"); + } + + TEST_METHOD(TooManyAppsDoesntTakeList) + { + Log::Comment(L"Fill up the number of buffers and each history list to the max."); + for (size_t i = 0; i < s_NumberOfBuffers; i++) + { + auto history = CommandHistory::s_Allocate(_manyApps[i], _MakeHandle(i)); + VERIFY_IS_NOT_NULL(history); + for (size_t j = 0; j < s_BufferSize; j++) + { + VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[j], false)); + } + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands()); + } + VERIFY_ARE_EQUAL(s_NumberOfBuffers, CommandHistory::s_historyLists.size()); + + Log::Comment(L"Add one more app and it should re-use a buffer but it should be clear."); + auto history = CommandHistory::s_Allocate(_manyApps[4], _MakeHandle(444)); + VERIFY_IS_NULL(history); + VERIFY_ARE_EQUAL(s_NumberOfBuffers, CommandHistory::s_historyLists.size()); + } + + TEST_METHOD(AppNamesMatchInsensitive) + { + auto history = CommandHistory::s_Allocate(L"testApp", _MakeHandle(777)); + VERIFY_IS_NOT_NULL(history); + VERIFY_IS_TRUE(history->IsAppNameMatch(L"TEsTaPP")); + } + + TEST_METHOD(ReallocUp) + { + Log::Comment(L"Allocate and fill with too many items."); + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + for (size_t j = 0; j < _manyHistoryItems.size(); j++) + { + VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[j], false)); + } + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands()); + + Log::Comment(L"Retrieve items/order."); + std::vector commandsStored; + for (SHORT i = 0; i < (SHORT)history->GetNumberOfCommands(); i++) + { + commandsStored.emplace_back(history->GetNth(i)); + } + + Log::Comment(L"Reallocate larger and ensure items and order are preserved."); + history->Realloc(_manyHistoryItems.size()); + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands()); + for (SHORT i = 0; i < (SHORT)commandsStored.size(); i++) + { + VERIFY_ARE_EQUAL(String(commandsStored[i].data()), String(history->GetNth(i).data())); + } + + Log::Comment(L"Fill up the larger buffer and ensure they fit this time."); + for (size_t j = 0; j < _manyHistoryItems.size(); j++) + { + VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[j], false)); + } + VERIFY_ARE_EQUAL(_manyHistoryItems.size(), history->GetNumberOfCommands()); + } + + TEST_METHOD(ReallocDown) + { + Log::Comment(L"Allocate and fill with just enough items."); + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + for (size_t j = 0; j < s_BufferSize; j++) + { + VERIFY_SUCCEEDED(history->Add(_manyHistoryItems[j], false)); + } + VERIFY_ARE_EQUAL(s_BufferSize, history->GetNumberOfCommands()); + + Log::Comment(L"Retrieve items/order."); + std::vector commandsStored; + for (SHORT i = 0; i < (SHORT)history->GetNumberOfCommands(); i++) + { + commandsStored.emplace_back(history->GetNth(i)); + } + + Log::Comment(L"Reallocate smaller and ensure items and order are preserved. Items at end of list should be trimmed."); + history->Realloc(5); + for (SHORT i = 0; i < 5; i++) + { + VERIFY_ARE_EQUAL(String(commandsStored[i].data()), String(history->GetNth(i).data())); + } + } + + TEST_METHOD(AddSequentialDuplicates) + { + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + + // The same command twice is always suppressed. + VERIFY_SUCCEEDED(history->Add(L"dir", false)); + VERIFY_SUCCEEDED(history->Add(L"dir", false)); + + VERIFY_ARE_EQUAL(1ul, history->GetNumberOfCommands()); + } + + TEST_METHOD(AddSequentialNoDuplicates) + { + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + + // The same command twice is always suppressed. + VERIFY_SUCCEEDED(history->Add(L"dir", true)); + VERIFY_SUCCEEDED(history->Add(L"dir", true)); + + VERIFY_ARE_EQUAL(1ul, history->GetNumberOfCommands()); + } + + TEST_METHOD(AddNonsequentialDuplicates) + { + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + + // Duplicates not suppressed here. Dir (3rd line) will not replace/merge with 1st line. + VERIFY_SUCCEEDED(history->Add(L"dir", false)); + VERIFY_SUCCEEDED(history->Add(L"cd", false)); + VERIFY_SUCCEEDED(history->Add(L"dir", false)); + + VERIFY_ARE_EQUAL(3ul, history->GetNumberOfCommands()); + } + + TEST_METHOD(AddNonsequentialNoDuplicates) + { + auto history = CommandHistory::s_Allocate(_manyApps[0], _MakeHandle(0)); + VERIFY_IS_NOT_NULL(history); + + // Duplicates suppressed here. Dir (3rd line) will replace/merge with 1st line. + VERIFY_SUCCEEDED(history->Add(L"dir", true)); + VERIFY_SUCCEEDED(history->Add(L"cd", false)); + VERIFY_SUCCEEDED(history->Add(L"dir", true)); + + VERIFY_ARE_EQUAL(2ul, history->GetNumberOfCommands()); + } + +private: + + const std::array _manyApps = + { + L"foo.exe", + L"bar.exe", + L"baz.exe", + L"apple.exe", + L"banana.exe" + }; + + const std::array _manyHistoryItems = + { + L"dir", + L"dir /w", + L"dir /p /w", + L"telnet 127.0.0.1", + L"ipconfig", + L"ipconfig /all", + L"net", + L"ping 127.0.0.1", + L"cd ..", + L"bcz", + L"notepad sources", + L"git push" + }; + + static constexpr UINT s_NumberOfBuffers = 4; + static constexpr UINT s_BufferSize = 10; + + HANDLE _MakeHandle(size_t index) + { + return reinterpret_cast((index + 1) * 4); + } +}; diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj new file mode 100644 index 000000000..5da6b9665 --- /dev/null +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + {ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00} + + + {48d21369-3d7b-4431-9967-24e81292cf62} + + + {990F2657-8580-4828-943F-5DD657D11843} + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {1c959542-bac2-4e55-9a6d-13251914cbb9} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + + + + + + + + {531C23E7-4B76-4C08-8AAD-04164CB628C9} + Win32Proj + HostUnitTests + Host.Tests.Unit + Conhost.Unit.Tests + + + + ..;$(SolutionDir)src\inc;$(SolutionDir)src\inc\test;%(AdditionalIncludeDirectories) + + + + + + + \ No newline at end of file diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters new file mode 100644 index 000000000..388cab428 --- /dev/null +++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters @@ -0,0 +1,132 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + \ No newline at end of file diff --git a/src/host/ut_host/InitTests.cpp b/src/host/ut_host/InitTests.cpp new file mode 100644 index 000000000..916c1bdf9 --- /dev/null +++ b/src/host/ut_host/InitTests.cpp @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +#include "CommonState.hpp" + +#include "globals.h" +#include "srvinit.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class InitTests +{ + TEST_CLASS(InitTests); + + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx + static UINT const s_uiOEMJapaneseCP = 932; + static UINT const s_uiOEMSimplifiedChineseCP = 936; + static UINT const s_uiOEMKoreanCP = 949; + static UINT const s_uiOEMTraditionalChineseCP = 950; + + static LANGID const s_langIdJapanese = MAKELANGID(LANG_JAPANESE, SUBLANG_DEFAULT); + static LANGID const s_langIdSimplifiedChinese = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED); + static LANGID const s_langIdKorean = MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN); + static LANGID const s_langIdTraditionalChinese = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL); + static LANGID const s_langIdEnglish = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + + // This test exists to ensure the continued behavior of the code in the Windows loader. + // See the LOAD BEARING CODE comment inside GetConsoleLangId or the investigation results in MSFT: 9808579 for more detail. + TEST_METHOD(TestGetConsoleLangId) + { + BEGIN_TEST_METHOD_PROPERTIES() + // https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx + // The interesting ones for us are: + // Japanese Shift JIS = 932 + // Chinese Simplified GB2312 = 936 + // Korean Unified Hangul = 949 + // Chinese Traditional Big5 = 950 + TEST_METHOD_PROPERTY(L"Data:uiStartupCP", L"{437, 850, 932, 936, 949, 950}") + TEST_METHOD_PROPERTY(L"Data:uiOutputCP", L"{437, 850, 932, 936, 949, 950}") + END_TEST_METHOD_PROPERTIES() + + // if ServiceLocator::LocateGlobals().uiWindowsCP = a CJK one + // we should get SUCCESS and a matching result to our input + // for any other ServiceLocator::LocateGlobals().uiWindowsCP we should get STATUS_NOT_SUPPORTED and do nothing with the langid. + + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiStartupCP", ServiceLocator::LocateGlobals().uiWindowsCP)); + + UINT outputCP; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiOutputCP", outputCP)); + + LANGID langId = 0; + NTSTATUS const status = GetConsoleLangId(outputCP, &langId); + + if (s_uiOEMJapaneseCP == ServiceLocator::LocateGlobals().uiWindowsCP || + s_uiOEMSimplifiedChineseCP == ServiceLocator::LocateGlobals().uiWindowsCP || + s_uiOEMKoreanCP == ServiceLocator::LocateGlobals().uiWindowsCP || + s_uiOEMTraditionalChineseCP == ServiceLocator::LocateGlobals().uiWindowsCP) + { + VERIFY_ARE_EQUAL(STATUS_SUCCESS, status); + + LANGID langIdExpected; + switch (outputCP) + { + case s_uiOEMJapaneseCP: + langIdExpected = s_langIdJapanese; + break; + case s_uiOEMSimplifiedChineseCP: + langIdExpected = s_langIdSimplifiedChinese; + break; + case s_uiOEMKoreanCP: + langIdExpected = s_langIdKorean; + break; + case s_uiOEMTraditionalChineseCP: + langIdExpected = s_langIdTraditionalChinese; + break; + default: + langIdExpected = s_langIdEnglish; + break; + } + + VERIFY_ARE_EQUAL(langIdExpected, langId); + } + else + { + VERIFY_ARE_EQUAL(STATUS_NOT_SUPPORTED, status); + } + + } +}; diff --git a/src/host/ut_host/InputBufferTests.cpp b/src/host/ut_host/InputBufferTests.cpp new file mode 100644 index 000000000..15838fc1b --- /dev/null +++ b/src/host/ut_host/InputBufferTests.cpp @@ -0,0 +1,651 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" +#include "CommonState.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "..\types\inc\IInputEvent.hpp" + +using namespace WEX::Logging; + +class InputBufferTests +{ + TEST_CLASS(InputBufferTests); + + std::unique_ptr m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + m_state->InitEvents(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WI_ClearFlag(gci.Flags, CONSOLE_OUTPUT_SUSPENDED); + return true; + } + + static const size_t RECORD_INSERT_COUNT = 12; + + INPUT_RECORD MakeKeyEvent(BOOL bKeyDown, + WORD wRepeatCount, + WORD wVirtualKeyCode, + WORD wVirtualScanCode, + WCHAR UnicodeChar, + DWORD dwControlKeyState) + { + INPUT_RECORD retval; + retval.EventType = KEY_EVENT; + retval.Event.KeyEvent.bKeyDown = bKeyDown; + retval.Event.KeyEvent.wRepeatCount = wRepeatCount; + retval.Event.KeyEvent.wVirtualKeyCode = wVirtualKeyCode; + retval.Event.KeyEvent.wVirtualScanCode = wVirtualScanCode; + retval.Event.KeyEvent.uChar.UnicodeChar = UnicodeChar; + retval.Event.KeyEvent.dwControlKeyState = dwControlKeyState; + return retval; + } + + TEST_METHOD(CanGetNumberOfReadyEvents) + { + InputBuffer inputBuffer; + INPUT_RECORD record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); + // add another event, check again + INPUT_RECORD record2; + record2.EventType = MENU_EVENT; + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record2)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 2u); + } + + TEST_METHOD(CanInsertIntoInputBufferIndividually) + { + InputBuffer inputBuffer; + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + INPUT_RECORD record; + record.EventType = MENU_EVENT; + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_ARE_EQUAL(record, inputBuffer._storage.back()->ToInputRecord()); + } + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + } + + TEST_METHOD(CanBulkInsertIntoInputBuffer) + { + InputBuffer inputBuffer; + std::deque> events; + INPUT_RECORD record; + record.EventType = MENU_EVENT; + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + events.push_back(IInputEvent::Create(record)); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + // verify that the events are the same in storage + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(inputBuffer._storage[i]->ToInputRecord(), record); + } + } + + TEST_METHOD(InputBufferCoalescesMouseEvents) + { + InputBuffer inputBuffer; + + INPUT_RECORD mouseRecord; + mouseRecord.EventType = MOUSE_EVENT; + mouseRecord.Event.MouseEvent.dwEventFlags = MOUSE_MOVED; + + // add a bunch of mouse event records + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + mouseRecord.Event.MouseEvent.dwMousePosition.X = static_cast(i + 1); + mouseRecord.Event.MouseEvent.dwMousePosition.Y = static_cast(i + 1) * 2; + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(mouseRecord)), 0u); + } + + // check that they coalesced + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); + // check that the mouse position is being updated correctly + const IInputEvent* const pOutEvent = inputBuffer._storage.front().get(); + const MouseEvent* const pMouseEvent = static_cast(pOutEvent); + VERIFY_ARE_EQUAL(pMouseEvent->GetPosition().X, static_cast(RECORD_INSERT_COUNT)); + VERIFY_ARE_EQUAL(pMouseEvent->GetPosition().Y, static_cast(RECORD_INSERT_COUNT * 2)); + + // add a key event and another mouse event to make sure that + // an event between two mouse events stopped the coalescing. + INPUT_RECORD keyRecord; + keyRecord.EventType = KEY_EVENT; + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(keyRecord)), 0u); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(mouseRecord)), 0u); + + // verify + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 3u); + } + + TEST_METHOD(InputBufferDoesNotCoalesceBulkMouseEvents) + { + Log::Comment(L"The input buffer should not coalesce mouse events if more than one event is sent at a time"); + + InputBuffer inputBuffer; + INPUT_RECORD mouseRecords[RECORD_INSERT_COUNT]; + std::deque> events; + + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + mouseRecords[i].EventType = MOUSE_EVENT; + mouseRecords[i].Event.MouseEvent.dwEventFlags = MOUSE_MOVED; + events.push_back(IInputEvent::Create(mouseRecords[i])); + } + // add an extra event + events.push_front(IInputEvent::Create(mouseRecords[0])); + inputBuffer.Flush(); + // send one mouse event to possibly coalesce into later + VERIFY_IS_GREATER_THAN(inputBuffer.Write(std::move(events.front())), 0u); + events.pop_front(); + // write the others in bulk + VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); + // no events should have been coalesced + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT + 1); + // check that the events stored match those inserted + VERIFY_ARE_EQUAL(inputBuffer._storage.front()->ToInputRecord(), mouseRecords[0]); + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1]->ToInputRecord(), mouseRecords[i]); + } + } + + TEST_METHOD(InputBufferCoalescesKeyEvents) + { + Log::Comment(L"The input buffer should coalesce identical key events if they are send one at a time"); + + InputBuffer inputBuffer; + INPUT_RECORD record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); + + // send a bunch of identical events + inputBuffer.Flush(); + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + } + + // all events should have been coalesced into one + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); + + // the single event should have a repeat count for each + // coalesced event + std::unique_ptr outEvent; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvent, + true, + false, + false, + false)); + + VERIFY_ARE_NOT_EQUAL(nullptr, outEvent.get()); + const KeyEvent* const pKeyEvent = static_cast(outEvent.get()); + VERIFY_ARE_EQUAL(pKeyEvent->GetRepeatCount(), RECORD_INSERT_COUNT); + } + + TEST_METHOD(InputBufferDoesNotCoalesceBulkKeyEvents) + { + Log::Comment(L"The input buffer should not coalesce key events if more than one event is sent at a time"); + + InputBuffer inputBuffer; + INPUT_RECORD keyRecords[RECORD_INSERT_COUNT]; + std::deque> events; + + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + keyRecords[i] = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); + events.push_back(IInputEvent::Create(keyRecords[i])); + } + inputBuffer.Flush(); + // send one key event to possibly coalesce into later + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(keyRecords[0])), 0u); + // write the others in bulk + VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); + // no events should have been coalesced + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT + 1); + // check that the events stored match those inserted + VERIFY_ARE_EQUAL(inputBuffer._storage.front()->ToInputRecord(), keyRecords[0]); + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(inputBuffer._storage[i + 1]->ToInputRecord(), keyRecords[i]); + } + } + + TEST_METHOD(InputBufferDoesNotCoalesceFullWidthChars) + { + InputBuffer inputBuffer; + WCHAR hiraganaA = 0x3042; // U+3042 hiragana A + INPUT_RECORD record = MakeKeyEvent(true, 1, hiraganaA, 0, hiraganaA, 0); + + // send a bunch of identical events + inputBuffer.Flush(); + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_ARE_EQUAL(inputBuffer._storage.back()->ToInputRecord(), record); + } + + // The events shouldn't be coalesced + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + } + + TEST_METHOD(CanFlushAllOutput) + { + InputBuffer inputBuffer; + std::deque> events; + + // put some events in the buffer so we can remove them + INPUT_RECORD record; + record.EventType = MENU_EVENT; + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + events.push_back(IInputEvent::Create(record)); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(events), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + + // remove them + inputBuffer.Flush(); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + } + + TEST_METHOD(CanFlushAllButKeys) + { + InputBuffer inputBuffer; + INPUT_RECORD records[RECORD_INSERT_COUNT] = { 0 }; + std::deque> inEvents; + + // create alternating mouse and key events + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + records[i].EventType = (i % 2 == 0) ? MENU_EVENT : KEY_EVENT; + inEvents.push_back(IInputEvent::Create(records[i])); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + + // remove them + inputBuffer.FlushAllButKeys(); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT / 2); + + // make sure that the non key events were the ones removed + std::deque> outEvents; + size_t amountToRead = RECORD_INSERT_COUNT / 2; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); + VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); + + for (size_t i = 0; i < outEvents.size(); ++i) + { + VERIFY_ARE_EQUAL(outEvents[i]->EventType(), InputEventType::KeyEvent); + } + } + + TEST_METHOD(CanReadInput) + { + InputBuffer inputBuffer; + INPUT_RECORD records[RECORD_INSERT_COUNT]; + std::deque> inEvents; + + // write some input records + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); + inEvents.push_back(IInputEvent::Create(records[i])); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + + // read them back out + std::deque> outEvents; + size_t amountToRead = RECORD_INSERT_COUNT; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); + VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + for (size_t i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + } + } + + TEST_METHOD(CanPeekAtEvents) + { + InputBuffer inputBuffer; + + // add some events so that we have something to peek at + INPUT_RECORD records[RECORD_INSERT_COUNT]; + std::deque> inEvents; + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); + inEvents.push_back(IInputEvent::Create(records[i])); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + + // peek at events + std::deque> outEvents; + size_t amountToRead = RECORD_INSERT_COUNT; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + true, + false, + false, + false)); + + VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + } + } + + TEST_METHOD(EmptyingBufferDuringReadSetsResetWaitEvent) + { + Log::Comment(L"ResetWaitEvent should be true if a read to the buffer completely empties it"); + + InputBuffer inputBuffer; + + // add some events so that we have something to stick in front of + INPUT_RECORD records[RECORD_INSERT_COUNT]; + std::deque> inEvents; + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); + inEvents.push_back(IInputEvent::Create(records[i])); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + + // read one record, make sure ResetWaitEvent isn't set + std::deque> outEvents; + size_t eventsRead = 0; + bool resetWaitEvent = false; + inputBuffer._ReadBuffer(outEvents, + 1, + eventsRead, + false, + resetWaitEvent, + true, + false); + VERIFY_ARE_EQUAL(eventsRead, 1u); + VERIFY_IS_FALSE(!!resetWaitEvent); + + // read the rest, resetWaitEvent should be set to true + outEvents.clear(); + inputBuffer._ReadBuffer(outEvents, + RECORD_INSERT_COUNT - 1, + eventsRead, + false, + resetWaitEvent, + true, + false); + VERIFY_ARE_EQUAL(eventsRead, RECORD_INSERT_COUNT - 1); + VERIFY_IS_TRUE(!!resetWaitEvent); + } + + TEST_METHOD(ReadingDbcsCharsPadsOutputArray) + { + Log::Comment(L"During a non-unicode read, the input buffer should count twice for each dbcs key event"); + + // write a mouse event, key event, dbcs key event, mouse event + InputBuffer inputBuffer; + const unsigned int recordInsertCount = 4; + INPUT_RECORD inRecords[recordInsertCount]; + inRecords[0].EventType = MOUSE_EVENT; + inRecords[1] = MakeKeyEvent(TRUE, 1, L'A', 0, L'A', 0); + inRecords[2] = MakeKeyEvent(TRUE, 1, 0x3042, 0, 0x3042, 0); // U+3042 hiragana A + inRecords[3].EventType = MOUSE_EVENT; + + std::deque> inEvents; + for (size_t i = 0; i < recordInsertCount; ++i) + { + inEvents.push_back(IInputEvent::Create(inRecords[i])); + } + + inputBuffer.Flush(); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + + // read them out non-unicode style and compare + std::deque> outEvents; + size_t eventsRead = 0; + bool resetWaitEvent = false; + inputBuffer._ReadBuffer(outEvents, + recordInsertCount, + eventsRead, + false, + resetWaitEvent, + false, + false); + // the dbcs record should have counted for two elements in + // the array, making it so that we get less events read + VERIFY_ARE_EQUAL(eventsRead, recordInsertCount - 1); + VERIFY_ARE_EQUAL(eventsRead, outEvents.size()); + for (size_t i = 0; i < eventsRead; ++i) + { + VERIFY_ARE_EQUAL(outEvents[i]->ToInputRecord(), inRecords[i]); + } + } + + TEST_METHOD(CanPrependEvents) + { + InputBuffer inputBuffer; + + // add some events so that we have something to stick in front of + INPUT_RECORD records[RECORD_INSERT_COUNT]; + std::deque> inEvents; + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + records[i] = MakeKeyEvent(TRUE, 1, static_cast(L'A' + i), 0, static_cast(L'A' + i), 0); + inEvents.push_back(IInputEvent::Create(records[i])); + } + VERIFY_IS_GREATER_THAN(inputBuffer.Write(inEvents), 0u); + + // prepend some other events + inEvents.clear(); + INPUT_RECORD prependRecords[RECORD_INSERT_COUNT]; + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + prependRecords[i] = MakeKeyEvent(TRUE, 1, static_cast(L'a' + i), 0, static_cast(L'a' + i), 0); + inEvents.push_back(IInputEvent::Create(prependRecords[i])); + } + size_t eventsWritten = inputBuffer.Prepend(inEvents); + VERIFY_ARE_EQUAL(eventsWritten, RECORD_INSERT_COUNT); + + // grab the first set of events and ensure they match prependRecords + std::deque> outEvents; + size_t amountToRead = RECORD_INSERT_COUNT; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); + VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), RECORD_INSERT_COUNT); + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(prependRecords[i], outEvents[i]->ToInputRecord()); + } + + outEvents.clear(); + // verify the rest of the records + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + false, + false, + false, + false)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + VERIFY_ARE_EQUAL(amountToRead, outEvents.size()); + for (unsigned int i = 0; i < RECORD_INSERT_COUNT; ++i) + { + VERIFY_ARE_EQUAL(records[i], outEvents[i]->ToInputRecord()); + } + } + + TEST_METHOD(CanReinitializeInputBuffer) + { + InputBuffer inputBuffer; + DWORD originalInputMode = inputBuffer.InputMode; + + // change the buffer's state a bit + INPUT_RECORD record; + record.EventType = MENU_EVENT; + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(record)), 0u); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); + inputBuffer.InputMode = 0x0; + inputBuffer.ReinitializeInputBuffer(); + + // check that the changes were reverted + VERIFY_ARE_EQUAL(originalInputMode, inputBuffer.InputMode); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + } + + TEST_METHOD(HandleConsoleSuspensionEventsRemovesPauseKeys) + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + InputBuffer inputBuffer; + INPUT_RECORD pauseRecord = MakeKeyEvent(true, 1, VK_PAUSE, 0, 0, 0); + + // make sure we aren't currently paused and have an empty buffer + VERIFY_IS_FALSE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + + VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(pauseRecord)), 0u); + + // we should now be paused and the input record should be discarded + VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + + // the next key press should unpause us but be discarded + INPUT_RECORD unpauseRecord = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); + VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(unpauseRecord)), 0u); + + VERIFY_IS_FALSE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + } + + TEST_METHOD(SystemKeysDontUnpauseConsole) + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + InputBuffer inputBuffer; + INPUT_RECORD pauseRecord = MakeKeyEvent(true, 1, VK_PAUSE, 0, 0, 0); + + // make sure we aren't currently paused and have an empty buffer + VERIFY_IS_FALSE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + + // pause the screen + VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(pauseRecord)), 0u); + + // we should now be paused and the input record should be discarded + VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 0u); + + // sending a system key event should not stop the pause and + // the record should be stored in the input buffer + INPUT_RECORD systemRecord = MakeKeyEvent(true, 1, VK_CONTROL, 0, 0, 0); + VERIFY_IS_GREATER_THAN(inputBuffer.Write(IInputEvent::Create(systemRecord)), 0u); + + VERIFY_IS_TRUE(WI_IsFlagSet(gci.Flags, CONSOLE_OUTPUT_SUSPENDED)); + VERIFY_ARE_EQUAL(inputBuffer.GetNumberOfReadyEvents(), 1u); + + std::deque> outEvents; + size_t amountToRead = 2; + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + amountToRead, + true, + false, + false, + false)); + } + + TEST_METHOD(WritingToEmptyBufferSignalsWaitEvent) + { + InputBuffer inputBuffer; + INPUT_RECORD record = MakeKeyEvent(true, 1, L'a', 0, L'a', 0); + std::unique_ptr inputEvent = IInputEvent::Create(record); + size_t eventsWritten; + bool waitEvent = false; + inputBuffer.Flush(); + // write one event to an empty buffer + std::deque> storage; + storage.push_back(std::move(inputEvent)); + inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent); + VERIFY_IS_TRUE(waitEvent); + // write another, it shouldn't signal this time + INPUT_RECORD record2 = MakeKeyEvent(true, 1, L'b', 0, L'b', 0); + std::unique_ptr inputEvent2 = IInputEvent::Create(record2); + // write another event to a non-empty buffer + waitEvent = false; + storage.push_back(std::move(inputEvent2)); + inputBuffer._WriteBuffer(storage, eventsWritten, waitEvent); + + VERIFY_IS_FALSE(waitEvent); + } + + TEST_METHOD(StreamReadingDeCoalesces) + { + InputBuffer inputBuffer; + const WORD repeatCount = 5; + INPUT_RECORD record = MakeKeyEvent(true, repeatCount, L'a', 0, L'a', 0); + std::deque> outEvents; + + VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(record)), 1u); + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + 1, + false, + false, + true, + true)); + VERIFY_ARE_EQUAL(outEvents.size(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); + VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount - 1); + VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + } + + TEST_METHOD(StreamPeekingDeCoalesces) + { + InputBuffer inputBuffer; + const WORD repeatCount = 5; + INPUT_RECORD record = MakeKeyEvent(true, repeatCount, L'a', 0, L'a', 0); + std::deque> outEvents; + + VERIFY_ARE_EQUAL(inputBuffer.Write(IInputEvent::Create(record)), 1u); + VERIFY_SUCCESS_NTSTATUS(inputBuffer.Read(outEvents, + 1, + true, + false, + true, + true)); + VERIFY_ARE_EQUAL(outEvents.size(), 1u); + VERIFY_ARE_EQUAL(inputBuffer._storage.size(), 1u); + VERIFY_ARE_EQUAL(static_cast(*inputBuffer._storage.front()).GetRepeatCount(), repeatCount); + VERIFY_ARE_EQUAL(static_cast(*outEvents.front()).GetRepeatCount(), 1u); + } + +}; diff --git a/src/host/ut_host/OutputCellIteratorTests.cpp b/src/host/ut_host/OutputCellIteratorTests.cpp new file mode 100644 index 000000000..000e313b6 --- /dev/null +++ b/src/host/ut_host/OutputCellIteratorTests.cpp @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "../buffer/out/outputCellIterator.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +static constexpr TextAttribute InvalidTextAttribute{ INVALID_COLOR, INVALID_COLOR }; + +class OutputCellIteratorTests +{ + CommonState* m_state; + + TEST_CLASS(OutputCellIteratorTests); + + TEST_METHOD(CharacterFillDoubleWidth) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const wchar_t wch = L'\x30a2'; // katakana A + const size_t limit = 5; + + OutputCellIterator it(wch, limit); + + OutputCellView expectedLead({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Leading), + InvalidTextAttribute, + TextAttributeBehavior::Current); + + OutputCellView expectedTrail({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Trailing), + InvalidTextAttribute, + TextAttributeBehavior::Current); + + for (size_t i = 0; i < limit; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expectedLead, *it); + it++; + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expectedTrail, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(CharacterFillLimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const wchar_t wch = L'Q'; + const size_t limit = 5; + + OutputCellIterator it(wch, limit); + + OutputCellView expected({ &wch, 1 }, + DbcsAttribute{}, + InvalidTextAttribute, + TextAttributeBehavior::Current); + + for (size_t i = 0; i < limit; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(CharacterFillUnlimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const wchar_t wch = L'Q'; + + OutputCellIterator it(wch); + + OutputCellView expected({ &wch, 1 }, + DbcsAttribute{}, + InvalidTextAttribute, + TextAttributeBehavior::Current); + + for (size_t i = 0; i < SHORT_MAX; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_TRUE(it); + } + + TEST_METHOD(AttributeFillLimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + TextAttribute attr; + attr.SetFromLegacy(FOREGROUND_RED | BACKGROUND_BLUE); + + const size_t limit = 5; + + OutputCellIterator it(attr, limit); + + OutputCellView expected({}, + {}, + attr, + TextAttributeBehavior::StoredOnly); + + for (size_t i = 0; i < limit; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(AttributeFillUnlimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + TextAttribute attr; + attr.SetFromLegacy(FOREGROUND_RED | BACKGROUND_BLUE); + + OutputCellIterator it(attr); + + OutputCellView expected({}, + {}, + attr, + TextAttributeBehavior::StoredOnly); + + for (size_t i = 0; i < SHORT_MAX; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_TRUE(it); + } + + TEST_METHOD(TextAndAttributeFillLimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const wchar_t wch = L'Q'; + + TextAttribute attr; + attr.SetFromLegacy(FOREGROUND_RED | BACKGROUND_BLUE); + + const size_t limit = 5; + + OutputCellIterator it(wch, attr, limit); + + OutputCellView expected({ &wch, 1 }, + {}, + attr, + TextAttributeBehavior::Stored); + + for (size_t i = 0; i < limit; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(TextAndAttributeFillUnlimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const wchar_t wch = L'Q'; + + TextAttribute attr; + attr.SetFromLegacy(FOREGROUND_RED | BACKGROUND_BLUE); + + OutputCellIterator it(wch, attr); + + OutputCellView expected({ &wch, 1 }, + {}, + attr, + TextAttributeBehavior::Stored); + + for (size_t i = 0; i < SHORT_MAX; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_TRUE(it); + } + + TEST_METHOD(CharInfoFillLimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + CHAR_INFO ci; + ci.Char.UnicodeChar = L'Q'; + ci.Attributes = FOREGROUND_RED | BACKGROUND_BLUE; + + const size_t limit = 5; + + OutputCellIterator it(ci, limit); + + OutputCellView expected({ &ci.Char.UnicodeChar, 1 }, + {}, + TextAttribute(ci.Attributes), + TextAttributeBehavior::Stored); + + for (size_t i = 0; i < limit; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(CharInfoFillUnlimited) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + CHAR_INFO ci; + ci.Char.UnicodeChar = L'Q'; + ci.Attributes = FOREGROUND_RED | BACKGROUND_BLUE; + + OutputCellIterator it(ci); + + OutputCellView expected({ &ci.Char.UnicodeChar, 1 }, + {}, + TextAttribute(ci.Attributes), + TextAttributeBehavior::Stored); + + for (size_t i = 0; i < SHORT_MAX; i++) + { + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_TRUE(it); + } + + TEST_METHOD(StringData) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"The quick brown fox jumps over the lazy dog."); + + OutputCellIterator it(testText); + + for (const auto& wch : testText) + { + OutputCellView expected({ &wch, 1 }, + {}, + InvalidTextAttribute, + TextAttributeBehavior::Current); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(FullWidthStringData) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"\x30a2\x30a3\x30a4\x30a5\x30a6"); + + OutputCellIterator it(testText); + + for (const auto& wch : testText) + { + auto expected = OutputCellView({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Leading), + InvalidTextAttribute, + TextAttributeBehavior::Current); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + + expected = OutputCellView({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Trailing), + InvalidTextAttribute, + TextAttributeBehavior::Current); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(StringDataWithColor) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"The quick brown fox jumps over the lazy dog."); + TextAttribute color; + color.SetFromLegacy(FOREGROUND_GREEN | FOREGROUND_INTENSITY); + + OutputCellIterator it(testText, color); + + for (const auto& wch : testText) + { + OutputCellView expected({ &wch, 1 }, + {}, + color, + TextAttributeBehavior::Stored); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(FullWidthStringDataWithColor) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"\x30a2\x30a3\x30a4\x30a5\x30a6"); + TextAttribute color; + color.SetFromLegacy(FOREGROUND_GREEN | FOREGROUND_INTENSITY); + + OutputCellIterator it(testText, color); + + for (const auto& wch : testText) + { + auto expected = OutputCellView({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Leading), + color, + TextAttributeBehavior::Stored); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + + expected = OutputCellView({ &wch, 1 }, + DbcsAttribute(DbcsAttribute::Attribute::Trailing), + color, + TextAttributeBehavior::Stored); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(LegacyColorDataRun) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::vector colors{ FOREGROUND_GREEN, FOREGROUND_RED | BACKGROUND_BLUE, FOREGROUND_BLUE | FOREGROUND_INTENSITY, BACKGROUND_GREEN }; + const std::basic_string_view view{ colors.data(), colors.size() }; + + OutputCellIterator it(view, false); + + for (const auto& color : colors) + { + auto expected = OutputCellView({}, + {}, + { color }, + TextAttributeBehavior::StoredOnly); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(LegacyCharInfoRun) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + std::vector charInfos; + + for (auto i = 0; i < 5; i++) + { + CHAR_INFO ci; + ci.Char.UnicodeChar = static_cast(L'A' + i); + ci.Attributes = gsl::narrow(i); + + charInfos.push_back(ci); + } + + const std::basic_string_view view{ charInfos.data(), charInfos.size() }; + + OutputCellIterator it(view); + + for (const auto& ci : charInfos) + { + auto expected = OutputCellView({&ci.Char.UnicodeChar, 1}, + {}, + { ci.Attributes}, + TextAttributeBehavior::Stored); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(OutputCellRun) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + std::vector cells; + + for (auto i = 0; i < 5; i++) + { + const std::wstring pair(L"\xd834\xdd1e"); + OutputCell cell(pair, {}, gsl::narrow(i)); + cells.push_back(cell); + } + + const std::basic_string_view view{ cells.data(), cells.size() }; + + OutputCellIterator it(view); + + for (const auto& cell : cells) + { + auto expected = OutputCellView(cell.Chars(), + cell.DbcsAttr(), + cell.TextAttr(), + cell.TextAttrBehavior()); + + VERIFY_IS_TRUE(it); + VERIFY_ARE_EQUAL(expected, *it); + it++; + } + + VERIFY_IS_FALSE(it); + } + + TEST_METHOD(DistanceStandard) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"The quick brown fox jumps over the lazy dog."); + + OutputCellIterator it(testText); + + const auto original = it; + + ptrdiff_t expected = 0; + for (const auto& wch : testText) + { + wch; // unused + VERIFY_IS_TRUE(it); + it++; + + expected++; + } + + VERIFY_IS_FALSE(it); + VERIFY_ARE_EQUAL(expected, it.GetCellDistance(original)); + VERIFY_ARE_EQUAL(expected, it.GetInputDistance(original)); + } + + TEST_METHOD(DistanceFullWidth) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + + const std::wstring testText(L"QWER\x30a2\x30a3\x30a4\x30a5\x30a6TYUI"); + + OutputCellIterator it(testText); + + const auto original = it; + + ptrdiff_t cellsExpected = 0; + ptrdiff_t inputExpected = 0; + for (const auto& wch : testText) + { + wch; // unused + VERIFY_IS_TRUE(it); + const auto value = *it; + it++; + + if (value.DbcsAttr().IsLeading() || value.DbcsAttr().IsTrailing()) + { + VERIFY_IS_TRUE(it); + it++; + cellsExpected++; + } + + cellsExpected++; + inputExpected++; + } + + VERIFY_IS_FALSE(it); + VERIFY_ARE_EQUAL(cellsExpected, it.GetCellDistance(original)); + VERIFY_ARE_EQUAL(inputExpected, it.GetInputDistance(original)); + } +}; diff --git a/src/host/ut_host/PopupTestHelper.hpp b/src/host/ut_host/PopupTestHelper.hpp new file mode 100644 index 000000000..0cb8f7968 --- /dev/null +++ b/src/host/ut_host/PopupTestHelper.hpp @@ -0,0 +1,88 @@ +/*++ + +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PopupTestHelper.hpp + +Abstract: +- helper functions for unit testing the various popups + +Author(s): +- Austin Diviness (AustDi) 06-Sep-2018 + +--*/ + + +#pragma once + +#include "../history.h" +#include "../readDataCooked.hpp" + + +class PopupTestHelper final +{ +public: + + static void InitReadData(COOKED_READ_DATA& cookedReadData, + wchar_t* const pBuffer, + const size_t cchBuffer, + const size_t cursorPosition) noexcept + { + cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); + cookedReadData._bufPtr = pBuffer + cursorPosition; + cookedReadData._backupLimit = pBuffer; + cookedReadData.OriginalCursorPosition() = { 0, 0 }; + cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); + cookedReadData._currentPosition = cursorPosition; + cookedReadData.VisibleCharCount() = cursorPosition; + } + + static void InitHistory(CommandHistory& history) noexcept + { + history.Empty(); + history.Flags |= CLE_ALLOCATED; + VERIFY_SUCCEEDED(history.Add(L"I'm a little teapot", false)); + VERIFY_SUCCEEDED(history.Add(L"hear me shout", false)); + VERIFY_SUCCEEDED(history.Add(L"here is my handle", false)); + VERIFY_SUCCEEDED(history.Add(L"here is my spout", false)); + VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 4u); + } + + static void InitLongHistory(CommandHistory& history) noexcept + { + history.Empty(); + history.Flags |= CLE_ALLOCATED; + VERIFY_SUCCEEDED(history.Add(L"Because I could not stop for Death", false)); + VERIFY_SUCCEEDED(history.Add(L"He kindly stopped for me", false)); + VERIFY_SUCCEEDED(history.Add(L"The carriage held but just Ourselves", false)); + VERIFY_SUCCEEDED(history.Add(L"And Immortality", false)); + VERIFY_SUCCEEDED(history.Add(L"~", false)); + VERIFY_SUCCEEDED(history.Add(L"We slowly drove - He knew no haste", false)); + VERIFY_SUCCEEDED(history.Add(L"And I had put away", false)); + VERIFY_SUCCEEDED(history.Add(L"My labor and my leisure too", false)); + VERIFY_SUCCEEDED(history.Add(L"For His Civility", false)); + VERIFY_SUCCEEDED(history.Add(L"~", false)); + VERIFY_SUCCEEDED(history.Add(L"We passed the School, where Children strove", false)); + VERIFY_SUCCEEDED(history.Add(L"At Recess - in the Ring", false)); + VERIFY_SUCCEEDED(history.Add(L"We passed the Fields of Gazing Grain", false)); + VERIFY_SUCCEEDED(history.Add(L"We passed the Setting Sun", false)); + VERIFY_SUCCEEDED(history.Add(L"~", false)); + VERIFY_SUCCEEDED(history.Add(L"Or rather - He passed us,", false)); + VERIFY_SUCCEEDED(history.Add(L"The Dews drew quivering and chill,", false)); + VERIFY_SUCCEEDED(history.Add(L"For only Gossamer, my Gown,", false)); + VERIFY_SUCCEEDED(history.Add(L"My Tippet - only Tulle", false)); + VERIFY_SUCCEEDED(history.Add(L"~", false)); + VERIFY_SUCCEEDED(history.Add(L"We paused before a House that seemed", false)); + VERIFY_SUCCEEDED(history.Add(L"A Swelling of the Ground -", false)); + VERIFY_SUCCEEDED(history.Add(L"The Roof was scarcely visible -", false)); + VERIFY_SUCCEEDED(history.Add(L"The Cornice - in the Ground -", false)); + VERIFY_SUCCEEDED(history.Add(L"~", false)); + VERIFY_SUCCEEDED(history.Add(L"Since then - 'tis Centuries - and yet", false)); + VERIFY_SUCCEEDED(history.Add(L"Feels shorter than the Day", false)); + VERIFY_SUCCEEDED(history.Add(L"~ Emily Dickinson", false)); + VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 28u); + } + +}; diff --git a/src/host/ut_host/ReadWaitTests.cpp b/src/host/ut_host/ReadWaitTests.cpp new file mode 100644 index 000000000..2d6ea4182 --- /dev/null +++ b/src/host/ut_host/ReadWaitTests.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "misc.h" +#include "dbcs.h" +#include "../../types/inc/IInputEvent.hpp" + +#include "../interactivity/inc/ServiceLocator.hpp" + +#include +#include + +using namespace WEX::Logging; + +class InputRecordConversionTests +{ + TEST_CLASS(InputRecordConversionTests); + + static const size_t INPUT_RECORD_COUNT = 10; + UINT savedCodepage = 0; + + TEST_CLASS_SETUP(ClassSetup) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + savedCodepage = gci.CP; + gci.CP = CP_JAPANESE; + VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo)); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.CP = savedCodepage; + VERIFY_IS_TRUE(!!GetCPInfo(gci.CP, &gci.CPInfo)); + return true; + } + + TEST_METHOD(SplitToOemLeavesNonKeyEventsAlone) + { + Log::Comment(L"nothing should happen to input events that aren't key events"); + + std::deque> inEvents; + INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 }; + for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) + { + inRecords[i].EventType = MOUSE_EVENT; + inRecords[i].Event.MouseEvent.dwMousePosition.X = static_cast(i); + inRecords[i].Event.MouseEvent.dwMousePosition.Y = static_cast(i * 2); + inEvents.push_back(IInputEvent::Create(inRecords[i])); + } + + SplitToOem(inEvents); + VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size()); + + for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) + { + VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord()); + } + } + + TEST_METHOD(SplitToOemLeavesNonDbcsCharsAlone) + { + Log::Comment(L"non-dbcs chars shouldn't be split"); + + std::deque> inEvents; + INPUT_RECORD inRecords[INPUT_RECORD_COUNT] = { 0 }; + for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) + { + inRecords[i].EventType = KEY_EVENT; + inRecords[i].Event.KeyEvent.uChar.UnicodeChar = static_cast(L'a' + i); + inEvents.push_back(IInputEvent::Create(inRecords[i])); + } + + SplitToOem(inEvents); + VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT, inEvents.size()); + + for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) + { + VERIFY_ARE_EQUAL(inRecords[i], inEvents[i]->ToInputRecord()); + } + } + + TEST_METHOD(SplitToOemSplitsDbcsChars) + { + Log::Comment(L"dbcs chars should be split"); + + const UINT codepage = ServiceLocator::LocateGlobals().getConsoleInformation().CP; + + INPUT_RECORD inRecords[INPUT_RECORD_COUNT * 2] = { 0 }; + std::deque> inEvents; + // U+3042 hiragana letter A + wchar_t hiraganaA = 0x3042; + wchar_t inChars[INPUT_RECORD_COUNT]; + for (size_t i = 0; i < INPUT_RECORD_COUNT; ++i) + { + wchar_t currentChar = static_cast(hiraganaA + (i * 2)); + inRecords[i].EventType = KEY_EVENT; + inRecords[i].Event.KeyEvent.uChar.UnicodeChar = currentChar; + inChars[i] = currentChar; + inEvents.push_back(IInputEvent::Create(inRecords[i])); + } + + SplitToOem(inEvents); + VERIFY_ARE_EQUAL(INPUT_RECORD_COUNT * 2, inEvents.size()); + + // create the data to compare the output to + char dbcsChars[INPUT_RECORD_COUNT * 2] = { 0 }; + int writtenBytes = WideCharToMultiByte(codepage, + 0, + inChars, + INPUT_RECORD_COUNT, + dbcsChars, + INPUT_RECORD_COUNT * 2, + nullptr, + false); + VERIFY_ARE_EQUAL(writtenBytes, static_cast(INPUT_RECORD_COUNT * 2)); + for (size_t i = 0; i < INPUT_RECORD_COUNT * 2; ++i) + { + const KeyEvent* const pKeyEvent = static_cast(inEvents[i].get()); + VERIFY_ARE_EQUAL(static_cast(pKeyEvent->GetCharData()), dbcsChars[i]); + } + } +}; diff --git a/src/host/ut_host/RendererTests.cpp b/src/host/ut_host/RendererTests.cpp new file mode 100644 index 000000000..0d37b7449 --- /dev/null +++ b/src/host/ut_host/RendererTests.cpp @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" + +#include "CommonState.hpp" + +#include "..\..\host\renderData.hpp" +#include "..\..\renderer\base\renderer.hpp" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class RendererTests +{ + TEST_CLASS(RendererTests); + + std::unique_ptr m_state; + std::unique_ptr m_renderData; + std::unique_ptr m_renderer; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = std::make_unique(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + m_state->PrepareGlobalInputBuffer(); + + m_renderData = std::make_unique(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_renderData.reset(nullptr); + + m_state->CleanupGlobalInputBuffer(); + + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + m_state.reset(nullptr); + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + Renderer* pRenderer = nullptr; + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + VERIFY_SUCCEEDED(Renderer::s_CreateInstance(gci.renderData, &pRenderer)); + m_renderer.reset(pRenderer); + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_renderer.reset(nullptr); + return true; + } + + TEST_METHOD(Sample) + { + m_renderer->TriggerTitleChange(); + } +}; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp new file mode 100644 index 000000000..200059579 --- /dev/null +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -0,0 +1,3017 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" +#include "screenInfo.hpp" + +#include "input.h" +#include "getset.h" +#include "_stream.h" // For WriteCharsLegacy + +#include "..\interactivity\inc\ServiceLocator.hpp" +#include "..\..\inc\conattrs.hpp" +#include "..\..\types\inc\Viewport.hpp" + +#include + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace Microsoft::Console::Types; + +class ScreenBufferTests +{ + CommonState* m_state; + + TEST_CLASS(ScreenBufferTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->InitEvents(); + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + m_state->PrepareGlobalInputBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + m_state->CleanupGlobalInputBuffer(); + + delete m_state; + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + // Set up some sane defaults + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.SetDefaultForegroundColor(INVALID_COLOR); + gci.SetDefaultBackgroundColor(INVALID_COLOR); + gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK + + + m_state->PrepareNewTextBufferInfo(); + auto& currentBuffer = gci.GetActiveOutputBuffer(); + // Make sure a test hasn't left us in the alt buffer on accident + VERIFY_IS_FALSE(currentBuffer._IsAltBuffer()); + VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, {0, 0}, true)); + VERIFY_ARE_EQUAL(COORD({0, 0}), currentBuffer.GetTextBuffer().GetCursor().GetPosition()); + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupNewTextBufferInfo(); + + return true; + } + + TEST_METHOD(SingleAlternateBufferCreationTest); + + TEST_METHOD(MultipleAlternateBufferCreationTest); + + TEST_METHOD(MultipleAlternateBuffersFromMainCreationTest); + + TEST_METHOD(TestReverseLineFeed); + + TEST_METHOD(TestAddTabStop); + + TEST_METHOD(TestClearTabStops); + + TEST_METHOD(TestClearTabStop); + + TEST_METHOD(TestGetForwardTab); + + TEST_METHOD(TestGetReverseTab); + + TEST_METHOD(TestAreTabsSet); + + TEST_METHOD(TestAltBufferDefaultTabStops); + + TEST_METHOD(EraseAllTests); + + TEST_METHOD(VtResize); + TEST_METHOD(VtResizeComprehensive); + + TEST_METHOD(VtSoftResetCursorPosition); + + TEST_METHOD(VtScrollMarginsNewlineColor); + + TEST_METHOD(VtNewlinePastViewport); + + TEST_METHOD(VtSetColorTable); + + TEST_METHOD(ResizeTraditionalDoesntDoubleFreeAttrRows); + + TEST_METHOD(ResizeCursorUnchanged); + + TEST_METHOD(ResizeAltBuffer); + + TEST_METHOD(ResizeAltBufferGetScreenBufferInfo); + + TEST_METHOD(VtEraseAllPersistCursor); + TEST_METHOD(VtEraseAllPersistCursorFillColor); + + TEST_METHOD(GetWordBoundary); + void GetWordBoundaryTrimZeros(bool on); + TEST_METHOD(GetWordBoundaryTrimZerosOn); + TEST_METHOD(GetWordBoundaryTrimZerosOff); + + TEST_METHOD(TestAltBufferCursorState); + + TEST_METHOD(TestAltBufferVtDispatching); + + TEST_METHOD(SetDefaultsIndividuallyBothDefault); + TEST_METHOD(SetDefaultsTogether); + + TEST_METHOD(ReverseResetWithDefaultBackground); + + TEST_METHOD(BackspaceDefaultAttrs); + TEST_METHOD(BackspaceDefaultAttrsWriteCharsLegacy); + + TEST_METHOD(BackspaceDefaultAttrsInPrompt); + + TEST_METHOD(SetGlobalColorTable); + + TEST_METHOD(SetColorTableThreeDigits); + + TEST_METHOD(DeleteCharsNearEndOfLine); + TEST_METHOD(DeleteCharsNearEndOfLineSimpleFirstCase); + TEST_METHOD(DeleteCharsNearEndOfLineSimpleSecondCase); + + TEST_METHOD(DontResetColorsAboveVirtualBottom); + + TEST_METHOD(ScrollUpInMargins); + TEST_METHOD(ScrollDownInMargins); + +}; + +void ScreenBufferTests::SingleAlternateBufferCreationTest() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + Log::Comment(L"Testing creating one alternate buffer, then returning to the main buffer."); + SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); + VERIFY_IS_NULL(psiOriginal->_psiAlternateBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + + NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"First alternate buffer successfully created"); + SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); + VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); + + psiFirstAlternate->UseMainScreenBuffer(); + Log::Comment(L"successfully swapped to the main buffer"); + SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiFinal, psiOriginal); + VERIFY_IS_NULL(psiFinal->_psiMainBuffer); + VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); + } +} + +void ScreenBufferTests::MultipleAlternateBufferCreationTest() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + Log::Comment( + L"Testing creating one alternate buffer, then creating another " + L"alternate from that first alternate, before returning to the " + L"main buffer." + ); + + SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); + NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"First alternate buffer successfully created"); + SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); + VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); + + Status = psiFirstAlternate->UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"Second alternate buffer successfully created"); + SCREEN_INFORMATION* psiSecondAlternate = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate); + VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer); + VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer); + + psiSecondAlternate->UseMainScreenBuffer(); + Log::Comment(L"successfully swapped to the main buffer"); + SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); + VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate); + VERIFY_ARE_EQUAL(psiFinal, psiOriginal); + VERIFY_IS_NULL(psiFinal->_psiMainBuffer); + VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); + } + } +} + +void ScreenBufferTests::MultipleAlternateBuffersFromMainCreationTest() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + Log::Comment( + L"Testing creating one alternate buffer, then creating another" + L" alternate from the main, before returning to the main buffer." + ); + SCREEN_INFORMATION* const psiOriginal = &gci.GetActiveOutputBuffer(); + NTSTATUS Status = psiOriginal->UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"First alternate buffer successfully created"); + SCREEN_INFORMATION* const psiFirstAlternate = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiOriginal, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiFirstAlternate, psiOriginal->_psiAlternateBuffer); + VERIFY_ARE_EQUAL(psiOriginal, psiFirstAlternate->_psiMainBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + VERIFY_IS_NULL(psiFirstAlternate->_psiAlternateBuffer); + + Status = psiOriginal->UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"Second alternate buffer successfully created"); + SCREEN_INFORMATION* const psiSecondAlternate = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiOriginal, psiSecondAlternate); + VERIFY_ARE_NOT_EQUAL(psiSecondAlternate, psiFirstAlternate); + VERIFY_ARE_EQUAL(psiSecondAlternate, psiOriginal->_psiAlternateBuffer); + VERIFY_ARE_EQUAL(psiOriginal, psiSecondAlternate->_psiMainBuffer); + VERIFY_IS_NULL(psiOriginal->_psiMainBuffer); + VERIFY_IS_NULL(psiSecondAlternate->_psiAlternateBuffer); + + psiSecondAlternate->UseMainScreenBuffer(); + Log::Comment(L"successfully swapped to the main buffer"); + SCREEN_INFORMATION* const psiFinal = &gci.GetActiveOutputBuffer(); + VERIFY_ARE_NOT_EQUAL(psiFinal, psiFirstAlternate); + VERIFY_ARE_NOT_EQUAL(psiFinal, psiSecondAlternate); + VERIFY_ARE_EQUAL(psiFinal, psiOriginal); + VERIFY_IS_NULL(psiFinal->_psiMainBuffer); + VERIFY_IS_NULL(psiFinal->_psiAlternateBuffer); + } + } +} + +void ScreenBufferTests::TestReverseLineFeed() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + auto& stateMachine = screenInfo.GetStateMachine(); + auto& cursor = screenInfo._textBuffer->GetCursor(); + auto viewport = screenInfo.GetViewport(); + + VERIFY_ARE_EQUAL(viewport.Top(), 0); + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 1: RI from below top of viewport"); + + stateMachine.ProcessString(L"foo\nfoo", 7); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 1); + VERIFY_ARE_EQUAL(viewport.Top(), 0); + + VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo)); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + viewport = screenInfo.GetViewport(); + VERIFY_ARE_EQUAL(viewport.Top(), 0); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 2: RI from top of viewport"); + cursor.SetPosition({0, 0}); + stateMachine.ProcessString(L"123456789", 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 0); + + VERIFY_SUCCEEDED(DoSrvPrivateReverseLineFeed(screenInfo)); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + viewport = screenInfo.GetViewport(); + VERIFY_ARE_EQUAL(viewport.Top(), 0); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + auto c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); + VERIFY_ARE_EQUAL(c.Y, 2); // This is the coordinates of the second "foo" from before. + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 3: RI from top of viewport, when viewport is below top of buffer"); + + cursor.SetPosition({0, 5}); + VERIFY_SUCCEEDED(screenInfo.SetViewportOrigin(true, {0, 5}, true)); + stateMachine.ProcessString(L"ABCDEFGH", 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + VERIFY_ARE_EQUAL(screenInfo.GetViewport().Top(), 5); + + LOG_IF_FAILED(DoSrvPrivateReverseLineFeed(screenInfo)); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 9); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + viewport = screenInfo.GetViewport(); + VERIFY_ARE_EQUAL(viewport.Top(), 5); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + c = screenInfo._textBuffer->GetLastNonSpaceCharacter(); + VERIFY_ARE_EQUAL(c.Y, 6); +} + +void ScreenBufferTests::TestAddTabStop() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + screenInfo.ClearTabStops(); + auto scopeExit = wil::scope_exit([&]() { screenInfo.ClearTabStops(); }); + + std::list expectedStops{ 12 }; + Log::Comment(L"Add tab to empty list."); + screenInfo.AddTabStop(12); + VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops); + + Log::Comment(L"Add tab to head of existing list."); + screenInfo.AddTabStop(4); + expectedStops.push_front(4); + VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops); + + Log::Comment(L"Add tab to tail of existing list."); + screenInfo.AddTabStop(30); + expectedStops.push_back(30); + VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops); + + Log::Comment(L"Add tab to middle of existing list."); + screenInfo.AddTabStop(24); + expectedStops.push_back(24); + expectedStops.sort(); + VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops); + + Log::Comment(L"Add tab that duplicates an item in the existing list."); + screenInfo.AddTabStop(24); + VERIFY_ARE_EQUAL(expectedStops, screenInfo._tabStops); +} + +void ScreenBufferTests::TestClearTabStops() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + Log::Comment(L"Clear non-existant tab stops."); + { + screenInfo.ClearTabStops(); + VERIFY_IS_TRUE(screenInfo._tabStops.empty()); + } + + Log::Comment(L"Clear handful of tab stops."); + { + for (auto x : { 3, 6, 13, 2, 25 }) + { + screenInfo.AddTabStop(gsl::narrow(x)); + } + VERIFY_IS_FALSE(screenInfo._tabStops.empty()); + screenInfo.ClearTabStops(); + VERIFY_IS_TRUE(screenInfo._tabStops.empty()); + } +} + +void ScreenBufferTests::TestClearTabStop() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + Log::Comment(L"Try to clear nonexistant list."); + { + screenInfo.ClearTabStop(0); + + VERIFY_IS_TRUE(screenInfo._tabStops.empty(), L"List should remain empty"); + } + + Log::Comment(L"Allocate 1 list item and clear it."); + { + screenInfo._tabStops.push_back(0); + screenInfo.ClearTabStop(0); + + VERIFY_IS_TRUE(screenInfo._tabStops.empty()); + } + + Log::Comment(L"Allocate 1 list item and clear non-existant."); + { + screenInfo._tabStops.push_back(0); + + Log::Comment(L"Free greater"); + screenInfo.ClearTabStop(1); + VERIFY_IS_FALSE(screenInfo._tabStops.empty()); + + Log::Comment(L"Free less than"); + screenInfo.ClearTabStop(-1); + VERIFY_IS_FALSE(screenInfo._tabStops.empty()); + + // clear all tab stops + screenInfo._tabStops.clear(); + } + + Log::Comment(L"Allocate many (5) list items and clear head."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + screenInfo._tabStops = inputData; + screenInfo.ClearTabStop(inputData.front()); + + inputData.pop_front(); + VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops); + + // clear all tab stops + screenInfo._tabStops.clear(); + } + + Log::Comment(L"Allocate many (5) list items and clear middle."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + screenInfo._tabStops = inputData; + screenInfo.ClearTabStop(*std::next(inputData.begin())); + + inputData.erase(std::next(inputData.begin())); + VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops); + + // clear all tab stops + screenInfo._tabStops.clear(); + } + + Log::Comment(L"Allocate many (5) list items and clear tail."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + screenInfo._tabStops = inputData; + screenInfo.ClearTabStop(inputData.back()); + + inputData.pop_back(); + VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops); + + // clear all tab stops + screenInfo._tabStops.clear(); + } + + Log::Comment(L"Allocate many (5) list items and clear non-existant item."); + { + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + screenInfo._tabStops = inputData; + screenInfo.ClearTabStop(9000); + + VERIFY_ARE_EQUAL(inputData, screenInfo._tabStops); + + // clear all tab stops + screenInfo._tabStops.clear(); + } +} + +void ScreenBufferTests::TestGetForwardTab() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + si._tabStops = inputData; + + const COORD coordScreenBufferSize = si.GetBufferSize().Dimensions(); + COORD coordCursor; + coordCursor.Y = coordScreenBufferSize.Y / 2; // in the middle of the buffer, it doesn't make a difference. + + Log::Comment(L"Find next tab from before front."); + { + coordCursor.X = 0; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = inputData.front(); + + COORD const coordCursorResult = si.GetForwardTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor advanced to first tab stop from sample list."); + } + + Log::Comment(L"Find next tab from in the middle."); + { + coordCursor.X = 6; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = *std::next(inputData.begin(), 3); + + COORD const coordCursorResult = si.GetForwardTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor advanced to middle tab stop from sample list."); + } + + Log::Comment(L"Find next tab from end."); + { + coordCursor.X = 30; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = coordScreenBufferSize.X - 1; + + COORD const coordCursorResult = si.GetForwardTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor advanced to end of screen buffer."); + } + + si._tabStops.clear(); +} + +void ScreenBufferTests::TestGetReverseTab() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + std::list inputData = { 3, 5, 6, 10, 15, 17 }; + si._tabStops = inputData; + + COORD coordCursor; + // in the middle of the buffer, it doesn't make a difference. + coordCursor.Y = si.GetBufferSize().Height() / 2; + + Log::Comment(L"Find previous tab from before front."); + { + coordCursor.X = 1; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = 0; + + COORD const coordCursorResult = si.GetReverseTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor adjusted to beginning of the buffer when it started before sample list."); + } + + Log::Comment(L"Find previous tab from in the middle."); + { + coordCursor.X = 6; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = *std::next(inputData.begin()); + + COORD const coordCursorResult = si.GetReverseTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor adjusted back one tab spot from middle of sample list."); + } + + Log::Comment(L"Find next tab from end."); + { + coordCursor.X = 30; + + COORD coordCursorExpected; + coordCursorExpected = coordCursor; + coordCursorExpected.X = inputData.back(); + + COORD const coordCursorResult = si.GetReverseTab(coordCursor); + VERIFY_ARE_EQUAL(coordCursorExpected, + coordCursorResult, + L"Cursor adjusted to last item in the sample list from position beyond end."); + } + + si._tabStops.clear(); +} + +void ScreenBufferTests::TestAreTabsSet() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + + si._tabStops.clear(); + VERIFY_IS_FALSE(si.AreTabsSet()); + + si.AddTabStop(1); + VERIFY_IS_TRUE(si.AreTabsSet()); +} + +void ScreenBufferTests::TestAltBufferDefaultTabStops() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to swap buffers. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); + // Make sure we're in VT mode + WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + mainBuffer.SetDefaultVtTabStops(); + VERIFY_IS_TRUE(mainBuffer.AreTabsSet()); + + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); + auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + + Log::Comment(NoThrowString().Format( + L"Manually enable VT mode for the alt buffer - " + L"usually the ctor will pick this up from GCI, but not in the tests." + )); + WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + VERIFY_IS_TRUE(altBuffer.AreTabsSet()); + VERIFY_IS_TRUE(altBuffer._tabStops.size() > 3); + + const COORD origin{ 0, 0 }; + auto& cursor = altBuffer.GetTextBuffer().GetCursor(); + cursor.SetPosition(origin); + auto& stateMachine = altBuffer.GetStateMachine(); + + Log::Comment(NoThrowString().Format( + L"Tab a few times - make sure the cursor is where we expect." + )); + + stateMachine.ProcessString(L"\t"); + COORD expected{8, 0}; + VERIFY_ARE_EQUAL(expected, cursor.GetPosition()); + + stateMachine.ProcessString(L"\t"); + expected = {16, 0}; + VERIFY_ARE_EQUAL(expected, cursor.GetPosition()); + + stateMachine.ProcessString(L"\n"); + expected = {0, 1}; + VERIFY_ARE_EQUAL(expected, cursor.GetPosition()); + + altBuffer.ClearTabStops(); + VERIFY_IS_FALSE(altBuffer.AreTabsSet()); + stateMachine.ProcessString(L"\t"); + expected = {altBuffer.GetBufferSize().Width()-1, 1}; + + VERIFY_ARE_EQUAL(expected, cursor.GetPosition()); + + useMain.release(); + altBuffer.UseMainScreenBuffer(); + VERIFY_IS_TRUE(mainBuffer.AreTabsSet()); +} + +void ScreenBufferTests::EraseAllTests() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si._textBuffer->GetCursor(); + + VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0); + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 1: Erase a single line of text in the buffer\n"); + + stateMachine.ProcessString(L"foo", 3); + COORD originalRelativePosition = {3, 0}; + VERIFY_ARE_EQUAL(si.GetViewport().Top(), 0); + VERIFY_ARE_EQUAL(cursor.GetPosition(), originalRelativePosition); + + VERIFY_SUCCEEDED(si.VtEraseAll()); + + auto viewport = si._viewport; + VERIFY_ARE_EQUAL(viewport.Top(), 1); + COORD newRelativePos = originalRelativePosition; + viewport.ConvertFromOrigin(&newRelativePos); + VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 2: Erase multiple lines, below the top of the buffer\n"); + + stateMachine.ProcessString(L"bar\nbar\nbar", 11); + viewport = si._viewport; + originalRelativePosition = cursor.GetPosition(); + viewport.ConvertToOrigin(&originalRelativePosition); + VERIFY_ARE_EQUAL(viewport.Top(), 1); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + + VERIFY_SUCCEEDED(si.VtEraseAll()); + viewport = si._viewport; + VERIFY_ARE_EQUAL(viewport.Top(), 4); + newRelativePos = originalRelativePosition; + viewport.ConvertFromOrigin(&newRelativePos); + VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + + + //////////////////////////////////////////////////////////////////////// + Log::Comment(L"Case 3: multiple lines at the bottom of the buffer\n"); + + cursor.SetPosition({0, 275}); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, {0, 220}, true)); + stateMachine.ProcessString(L"bar\nbar\nbar", 11); + viewport = si._viewport; + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 3); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 277); + originalRelativePosition = cursor.GetPosition(); + viewport.ConvertToOrigin(&originalRelativePosition); + + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); + VERIFY_SUCCEEDED(si.VtEraseAll()); + + viewport = si._viewport; + auto heightFromBottom = si.GetBufferSize().Height() - (viewport.Height()); + VERIFY_ARE_EQUAL(viewport.Top(), heightFromBottom); + newRelativePos = originalRelativePosition; + viewport.ConvertFromOrigin(&newRelativePos); + VERIFY_ARE_EQUAL(cursor.GetPosition(), newRelativePos); + Log::Comment(NoThrowString().Format( + L"viewport={L:%d,T:%d,R:%d,B:%d}", + viewport.Left(), viewport.Top(), viewport.RightInclusive(), viewport.BottomInclusive() + )); +} + +void ScreenBufferTests::VtResize() +{ + // Run this test in isolation - for one reason or another, this breaks other tests. + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD_PROPERTIES() + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + auto initialSbHeight = si.GetBufferSize().Height(); + auto initialSbWidth = si.GetBufferSize().Width(); + auto initialViewHeight = si.GetViewport().Height(); + auto initialViewWidth = si.GetViewport().Width(); + + Log::Comment(NoThrowString().Format( + L"Write '\x1b[8;30;80t'" + L" The Screen buffer height should remain unchanged, but the width should be 80 columns" + L" The viewport should be w,h=80,30" + )); + + std::wstring sequence = L"\x1b[8;30;80t"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + auto newSbHeight = si.GetBufferSize().Height(); + auto newSbWidth = si.GetBufferSize().Width(); + auto newViewHeight = si.GetViewport().Height(); + auto newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); + VERIFY_ARE_EQUAL(80, newSbWidth); + VERIFY_ARE_EQUAL(30, newViewHeight); + VERIFY_ARE_EQUAL(80, newViewWidth); + + initialSbHeight = newSbHeight; + initialSbWidth = newSbWidth; + initialViewHeight = newViewHeight; + initialViewWidth = newViewWidth; + + Log::Comment(NoThrowString().Format( + L"Write '\x1b[8;40;80t'" + L" The Screen buffer height should remain unchanged, but the width should be 80 columns" + L" The viewport should be w,h=80,40" + )); + + sequence = L"\x1b[8;40;80t"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + newSbHeight = si.GetBufferSize().Height(); + newSbWidth = si.GetBufferSize().Width(); + newViewHeight = si.GetViewport().Height(); + newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); + VERIFY_ARE_EQUAL(80, newSbWidth); + VERIFY_ARE_EQUAL(40, newViewHeight); + VERIFY_ARE_EQUAL(80, newViewWidth); + + initialSbHeight = newSbHeight; + initialSbWidth = newSbWidth; + initialViewHeight = newViewHeight; + initialViewWidth = newViewWidth; + + Log::Comment(NoThrowString().Format( + L"Write '\x1b[8;40;90t'" + L" The Screen buffer height should remain unchanged, but the width should be 90 columns" + L" The viewport should be w,h=90,40" + )); + + sequence = L"\x1b[8;40;90t"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + newSbHeight = si.GetBufferSize().Height(); + newSbWidth = si.GetBufferSize().Width(); + newViewHeight = si.GetViewport().Height(); + newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); + VERIFY_ARE_EQUAL(90, newSbWidth); + VERIFY_ARE_EQUAL(40, newViewHeight); + VERIFY_ARE_EQUAL(90, newViewWidth); + + initialSbHeight = newSbHeight; + initialSbWidth = newSbWidth; + initialViewHeight = newViewHeight; + initialViewWidth = newViewWidth; + + Log::Comment(NoThrowString().Format( + L"Write '\x1b[8;12;12t'" + L" The Screen buffer height should remain unchanged, but the width should be 12 columns" + L" The viewport should be w,h=12,12" + )); + + sequence = L"\x1b[8;12;12t"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + newSbHeight = si.GetBufferSize().Height(); + newSbWidth = si.GetBufferSize().Width(); + newViewHeight = si.GetViewport().Height(); + newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); + VERIFY_ARE_EQUAL(12, newSbWidth); + VERIFY_ARE_EQUAL(12, newViewHeight); + VERIFY_ARE_EQUAL(12, newViewWidth); + + initialSbHeight = newSbHeight; + initialSbWidth = newSbWidth; + initialViewHeight = newViewHeight; + initialViewWidth = newViewWidth; + + Log::Comment(NoThrowString().Format( + L"Write '\x1b[8;0;0t'" + L" Nothing should change" + )); + + sequence = L"\x1b[8;0;0t"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + newSbHeight = si.GetBufferSize().Height(); + newSbWidth = si.GetBufferSize().Width(); + newViewHeight = si.GetViewport().Height(); + newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(initialSbHeight, newSbHeight); + VERIFY_ARE_EQUAL(initialSbWidth, newSbWidth); + VERIFY_ARE_EQUAL(initialViewHeight, newViewHeight); + VERIFY_ARE_EQUAL(initialViewWidth, newViewWidth); + +} + + +void ScreenBufferTests::VtResizeComprehensive() +{ + // Run this test in isolation - for one reason or another, this breaks other tests. + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}") + TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}") + END_TEST_METHOD_PROPERTIES() + + int dx, dy; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + auto initialViewHeight = si.GetViewport().Height(); + auto initialViewWidth = si.GetViewport().Width(); + + auto expectedViewWidth = initialViewWidth + dx; + auto expectedViewHeight = initialViewHeight + dy; + + std::wstringstream ss; + ss << L"\x1b[8;" << expectedViewHeight << L";" << expectedViewWidth << L"t"; + + Log::Comment(NoThrowString().Format( + L"Write '\\x1b[8;%d;%dt'" + L" The viewport should be w,h=%d,%d", + expectedViewHeight, expectedViewWidth, + expectedViewWidth, expectedViewHeight + )); + + std::wstring sequence = ss.str(); + stateMachine.ProcessString(sequence); + + auto newViewHeight = si.GetViewport().Height(); + auto newViewWidth = si.GetViewport().Width(); + + VERIFY_ARE_EQUAL(expectedViewWidth, newViewWidth); + VERIFY_ARE_EQUAL(expectedViewHeight, newViewHeight); +} + +void ScreenBufferTests::VtSoftResetCursorPosition() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + + Log::Comment(NoThrowString().Format( + L"Move the cursor to 2,2, then execute a soft reset.\n" + L"The cursor should not move." + )); + + std::wstring seq = L"\x1b[2;2H"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({1, 1}), cursor.GetPosition()); + + seq = L"\x1b[!p"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({1, 1}), cursor.GetPosition()); + + Log::Comment(NoThrowString().Format( + L"Set some margins. The cursor should move home." + )); + + seq = L"\x1b[2;10r"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({0, 0}), cursor.GetPosition()); + + Log::Comment(NoThrowString().Format( + L"Move the cursor to 2,2, then execute a soft reset.\n" + L"The cursor should not move, even though there are margins." + )); + seq = L"\x1b[2;2H"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({1, 1}), cursor.GetPosition()); + seq = L"\x1b[!p"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({1, 1}), cursor.GetPosition()); +} + +void ScreenBufferTests::VtScrollMarginsNewlineColor() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition(COORD({0, 0})); + + const COLORREF yellow = RGB(255, 255, 0); + const COLORREF magenta = RGB(255, 0, 255); + gci.SetDefaultForegroundColor(yellow); + gci.SetDefaultBackgroundColor(magenta); + const TextAttribute defaultAttrs = gci.GetDefaultAttributes(); + si.SetAttributes(defaultAttrs); + + Log::Comment(NoThrowString().Format(L"Begin by clearing the screen.")); + + std::wstring seq = L"\x1b[2J"; + stateMachine.ProcessString(seq); + seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format( + L"Set the margins to 2, 5, then emit 10 'X\\n' strings. " + L"Each time, check that rows 0-10 have default attributes in their entire row." + )); + seq = L"\x1b[2;5r"; + stateMachine.ProcessString(seq); + // Make sure we clear the margins to not screw up another test. + auto clearMargins = wil::scope_exit([&]{stateMachine.ProcessString(L"\x1b[r");}); + + for (int iteration = 0; iteration < 10; iteration++) + { + Log::Comment(NoThrowString().Format( + L"Iteration:%d", iteration + )); + seq = L"X"; + stateMachine.ProcessString(seq); + seq = L"\n"; + stateMachine.ProcessString(seq); + + const COORD cursorPos = cursor.GetPosition(); + + Log::Comment(NoThrowString().Format( + L"Cursor=%s", + VerifyOutputTraits::ToString(cursorPos).GetBuffer() + )); + const auto viewport = si.GetViewport(); + Log::Comment(NoThrowString().Format( + L"Viewport=%s", + VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer() + )); + const auto viewTop = viewport.Top(); + for (int y = viewTop; y < viewTop + 10; y++) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + const ROW& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x < viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(false, attr.IsLegacy()); + VERIFY_ARE_EQUAL(defaultAttrs, attr); + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attr)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attr)); + } + } + } +} + +void ScreenBufferTests::VtNewlinePastViewport() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + // Make sure we're in VT mode + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition(COORD({0, 0})); + + std::wstring seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + seq = L"\x1b[2J"; + stateMachine.ProcessString(seq); + + const TextAttribute defaultAttrs{}; + const TextAttribute expectedTwo{FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE}; + + Log::Comment(NoThrowString().Format( + L"Move the cursor to the bottom of the viewport" + )); + + const auto initialViewport = si.GetViewport(); + Log::Comment(NoThrowString().Format( + L"initialViewport=%s", + VerifyOutputTraits::ToString(initialViewport.ToInclusive()).GetBuffer() + )); + + cursor.SetPosition(COORD({0, initialViewport.BottomInclusive()})); + + seq = L"\x1b[92;44m"; // bright-green on dark-blue + stateMachine.ProcessString(seq); + seq = L"\n"; + stateMachine.ProcessString(seq); + + const auto viewport = si.GetViewport(); + Log::Comment(NoThrowString().Format( + L"viewport=%s", + VerifyOutputTraits::ToString(viewport.ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(viewport.BottomInclusive(), cursor.GetPosition().Y); + VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + + for (int y = viewport.Top(); y < viewport.BottomInclusive(); y++) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + const ROW& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x < viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(defaultAttrs, attr); + } + } + + const ROW& row = tbi.GetRowByOffset(viewport.BottomInclusive()); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x < viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(expectedTwo, attr); + } +} + +void ScreenBufferTests::VtSetColorTable() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + + // Start with a known value + gci.SetColorTableEntry(0, RGB(0, 0, 0)); + + Log::Comment(NoThrowString().Format( + L"Process some valid sequences for setting the table" + )); + + std::wstring seq = L"\x1b]4;0;rgb:1/1/1\x7"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(1,1,1), gci.GetColorTableEntry(::XtermToWindowsIndex(0))); + + seq = L"\x1b]4;1;rgb:1/23/1\x7"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(1,0x23,1), gci.GetColorTableEntry(::XtermToWindowsIndex(1))); + + seq = L"\x1b]4;2;rgb:1/23/12\x7"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(1,0x23,0x12), gci.GetColorTableEntry(::XtermToWindowsIndex(2))); + + seq = L"\x1b]4;3;rgb:12/23/12\x7"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(0x12,0x23,0x12), gci.GetColorTableEntry(::XtermToWindowsIndex(3))); + + seq = L"\x1b]4;4;rgb:ff/a1/1b\x7"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(0xff,0xa1,0x1b), gci.GetColorTableEntry(::XtermToWindowsIndex(4))); + + seq = L"\x1b]4;5;rgb:ff/a1/1b\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(0xff,0xa1,0x1b), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"Try a bunch of invalid sequences." + )); + Log::Comment(NoThrowString().Format( + L"First start by setting an entry to a known value to compare to." + )); + seq = L"\x1b]4;5;rgb:9/9/9\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: Missing the first component" + )); + seq = L"\x1b]4;5;rgb:/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: too many characters in a component" + )); + seq = L"\x1b]4;5;rgb:111/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: too many componenets" + )); + seq = L"\x1b]4;5;rgb:1/1/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: no second component" + )); + seq = L"\x1b]4;5;rgb:1//1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: no components" + )); + seq = L"\x1b]4;5;rgb://\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: no third component" + )); + seq = L"\x1b]4;5;rgb:1/11/\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: rgbi is not a supported color space" + )); + seq = L"\x1b]4;5;rgbi:1/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: cmyk is not a supported color space" + )); + seq = L"\x1b]4;5;cmyk:1/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: no table index should do nothing" + )); + seq = L"\x1b]4;;rgb:1/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); + + Log::Comment(NoThrowString().Format( + L"invalid: need to specify a color space" + )); + seq = L"\x1b]4;5;1/1/1\x1b\\"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL(RGB(9,9,9), gci.GetColorTableEntry(::XtermToWindowsIndex(5))); +} + +void ScreenBufferTests::ResizeTraditionalDoesntDoubleFreeAttrRows() +{ + // there is not much to verify here, this test passes if the console doesn't crash. + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + + gci.SetWrapText(false); + COORD newBufferSize = si.GetBufferSize().Dimensions(); + newBufferSize.Y--; + + VERIFY_SUCCEEDED(si.ResizeTraditional(newBufferSize)); +} + +void ScreenBufferTests::ResizeCursorUnchanged() +{ + // Created for MSFT:19863799. Make sure whewn we resize the buffer, the + // cursor looks the same as it did before. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:useResizeWithReflow", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}") + TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}") + END_TEST_METHOD_PROPERTIES(); + bool useResizeWithReflow; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"useResizeWithReflow", useResizeWithReflow), L"Use ResizeWithReflow or not"); + + int dx, dy; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const auto& initialCursor = si.GetTextBuffer().GetCursor(); + + // Get initial cursor values + const CursorType initialType = initialCursor.GetType(); + const auto initialSize = initialCursor.GetSize(); + const COLORREF initialColor = initialCursor.GetColor(); + + // set our wrap mode accordingly - ResizeScreenBuffer will be smart enough + // to call the appropriate implementation + gci.SetWrapText(useResizeWithReflow); + + COORD newBufferSize = si.GetBufferSize().Dimensions(); + newBufferSize.X += static_cast(dx); + newBufferSize.Y += static_cast(dy); + + VERIFY_SUCCEEDED(si.ResizeScreenBuffer(newBufferSize, false)); + + const auto& finalCursor = si.GetTextBuffer().GetCursor(); + const CursorType finalType = finalCursor.GetType(); + const auto finalSize = finalCursor.GetSize(); + const COLORREF finalColor = finalCursor.GetColor(); + + VERIFY_ARE_EQUAL(initialType, finalType); + VERIFY_ARE_EQUAL(initialColor, finalColor); + VERIFY_ARE_EQUAL(initialSize, finalSize); +} + + +void ScreenBufferTests::ResizeAltBuffer() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + + Log::Comment(NoThrowString().Format( + L"Try resizing the alt buffer. Make sure the call doesn't stack overflow." + )); + + VERIFY_IS_FALSE(si._IsAltBuffer()); + const Viewport originalMainSize = Viewport(si._viewport); + + Log::Comment(NoThrowString().Format( + L"Switch to alt buffer" + )); + std::wstring seq = L"\x1b[?1049h"; + stateMachine.ProcessString(&seq[0], seq.length()); + + VERIFY_IS_FALSE(si._IsAltBuffer()); + VERIFY_IS_NOT_NULL(si._psiAlternateBuffer); + SCREEN_INFORMATION* const psiAlt = si._psiAlternateBuffer; + + COORD newSize = originalMainSize.Dimensions(); + newSize.X += 2; + newSize.Y += 2; + + Log::Comment(NoThrowString().Format( + L"MSFT:15917333 This call shouldn't stack overflow" + )); + psiAlt->SetViewportSize(&newSize); + VERIFY_IS_TRUE(true); + + Log::Comment(NoThrowString().Format( + L"Switch back from buffer" + )); + seq = L"\x1b[?1049l"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_IS_FALSE(si._IsAltBuffer()); + VERIFY_IS_NULL(si._psiAlternateBuffer); +} + +void ScreenBufferTests::ResizeAltBufferGetScreenBufferInfo() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 1, 10}") + TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 1, 10}") + END_TEST_METHOD_PROPERTIES(); + + int dx, dy; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer"); + + // Tests MSFT:19918103 + Log::Comment(NoThrowString().Format( + L"Switch to the alt buffer, then resize the buffer. " + L"GetConsoleScreenBufferInfoEx(mainBuffer) should return the alt " + L"buffer's size, not the main buffer's size." + )); + + auto& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer().GetActiveBuffer(); + StateMachine& stateMachine = mainBuffer.GetStateMachine(); + + VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); + const Viewport originalMainSize = Viewport(mainBuffer._viewport); + + Log::Comment(NoThrowString().Format( + L"Switch to alt buffer" + )); + std::wstring seq = L"\x1b[?1049h"; + stateMachine.ProcessString(seq); + + VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); + VERIFY_IS_NOT_NULL(mainBuffer._psiAlternateBuffer); + + auto& altBuffer = *(mainBuffer._psiAlternateBuffer); + auto useMain = wil::scope_exit([&]{ altBuffer.UseMainScreenBuffer(); }); + + COORD newBufferSize = originalMainSize.Dimensions(); + newBufferSize.X += static_cast(dx); + newBufferSize.Y += static_cast(dy); + + const Viewport originalAltSize = Viewport(altBuffer._viewport); + + VERIFY_ARE_EQUAL(originalMainSize.Width(), originalAltSize.Width()); + VERIFY_ARE_EQUAL(originalMainSize.Height(), originalAltSize.Height()); + + altBuffer.SetViewportSize(&newBufferSize); + + CONSOLE_SCREEN_BUFFER_INFOEX csbiex{0}; + g.api.GetConsoleScreenBufferInfoExImpl(mainBuffer, csbiex); + const auto newActualMainView = mainBuffer.GetViewport(); + const auto newActualAltView = altBuffer.GetViewport(); + + const auto newApiViewport = Viewport::FromExclusive(csbiex.srWindow); + + VERIFY_ARE_NOT_EQUAL(originalAltSize.Width(), newActualAltView.Width()); + VERIFY_ARE_NOT_EQUAL(originalAltSize.Height(), newActualAltView.Height()); + + VERIFY_ARE_NOT_EQUAL(originalMainSize.Width(), newActualAltView.Width()); + VERIFY_ARE_NOT_EQUAL(originalMainSize.Height(), newActualAltView.Height()); + + VERIFY_ARE_EQUAL(newActualAltView.Width(), newApiViewport.Width()); + VERIFY_ARE_EQUAL(newActualAltView.Height(), newApiViewport.Height()); +} + +void ScreenBufferTests::VtEraseAllPersistCursor() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + Log::Comment(NoThrowString().Format( + L"Make sure the viewport is at 0,0" + )); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + + Log::Comment(NoThrowString().Format( + L"Move the cursor to 2,2, then execute a Erase All.\n" + L"The cursor should not move relative to the viewport." + )); + + std::wstring seq = L"\x1b[2;2H"; + stateMachine.ProcessString(&seq[0], seq.length()); + VERIFY_ARE_EQUAL( COORD({1, 1}), cursor.GetPosition()); + + seq = L"\x1b[2J"; + stateMachine.ProcessString(&seq[0], seq.length()); + + auto newViewport = si._viewport; + COORD expectedCursor = {1, 1}; + newViewport.ConvertFromOrigin(&expectedCursor); + + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + +} + +void ScreenBufferTests::VtEraseAllPersistCursorFillColor() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + + Log::Comment(NoThrowString().Format( + L"Make sure the viewport is at 0,0" + )); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + + Log::Comment(NoThrowString().Format( + L"Change the colors to dark_red on bright_blue, then execute a Erase All.\n" + L"The viewport should be full of dark_red on bright_blue" + )); + + auto expectedAttr = TextAttribute(XtermToLegacy(1, 12)); + std::wstring seq = L"\x1b[31;104m"; + stateMachine.ProcessString(&seq[0], seq.length()); + + VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); + + seq = L"\x1b[2J"; + stateMachine.ProcessString(&seq[0], seq.length()); + + VERIFY_ARE_EQUAL(expectedAttr, si.GetAttributes()); + + auto newViewport = si._viewport; + Log::Comment(NoThrowString().Format( + L"new Viewport: %s", + VerifyOutputTraits::ToString(newViewport.ToInclusive()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"Buffer Size: %s", + VerifyOutputTraits::ToString(si.GetBufferSize().ToInclusive()).GetBuffer() + )); + + auto iter = tbi.GetCellDataAt(newViewport.Origin()); + auto height = newViewport.Height(); + auto width = newViewport.Width(); + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width; j++) + { + VERIFY_ARE_EQUAL(expectedAttr, iter->TextAttr()); + iter++; + } + } +} + +void ScreenBufferTests::GetWordBoundary() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + + const auto text = L"This is some test text for word boundaries."; + const auto length = wcslen(text); + + // Make the buffer as big as our test text. + const COORD newBufferSize = { gsl::narrow(length), 10 }; + VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + + const OutputCellIterator it(text, si.GetAttributes()); + si.Write(it, { 0,0 }); + + // Now find some words in it. + Log::Comment(L"Find first word from its front."); + COORD expectedFirst = { 0, 0 }; + COORD expectedSecond = { 4, 0 }; + + auto boundary = si.GetWordBoundary({ 0, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find first word from its middle."); + boundary = si.GetWordBoundary({ 1, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find first word from its end."); + boundary = si.GetWordBoundary({ 3, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find middle word from its front."); + expectedFirst = { 13, 0 }; + expectedSecond = { 17, 0 }; + boundary = si.GetWordBoundary({ 13, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find middle word from its middle."); + boundary = si.GetWordBoundary({ 15, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find middle word from its end."); + boundary = si.GetWordBoundary({ 16, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find end word from its front."); + expectedFirst = { 32, 0 }; + expectedSecond = { 43, 0 }; + boundary = si.GetWordBoundary({ 32, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find end word from its middle."); + boundary = si.GetWordBoundary({ 39, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find end word from its end."); + boundary = si.GetWordBoundary({ 43, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find a word starting from a boundary character."); + expectedFirst = { 8, 0 }; + expectedSecond = { 12, 0 }; + boundary = si.GetWordBoundary({ 12, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); +} + +void ScreenBufferTests::GetWordBoundaryTrimZeros(const bool on) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + + const auto text = L"000fe12 0xfe12 0Xfe12 0nfe12 0Nfe12"; + const auto length = wcslen(text); + + // Make the buffer as big as our test text. + const COORD newBufferSize = { gsl::narrow(length), 10 }; + VERIFY_SUCCEEDED(si.GetTextBuffer().ResizeTraditional(newBufferSize)); + + const OutputCellIterator it(text, si.GetAttributes()); + si.Write(it, { 0, 0 }); + + gci.SetTrimLeadingZeros(on); + + COORD expectedFirst; + COORD expectedSecond; + std::pair boundary; + + Log::Comment(L"Find lead with 000"); + expectedFirst = on ? COORD{ 3, 0 } : COORD{ 0, 0 }; + expectedSecond = COORD{ 7, 0 }; + boundary = si.GetWordBoundary({ 0, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find lead with 0x"); + expectedFirst = COORD{ 8, 0 }; + expectedSecond = COORD{ 14, 0 }; + boundary = si.GetWordBoundary({ 8, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find lead with 0X"); + expectedFirst = COORD{ 15, 0 }; + expectedSecond = COORD{ 21, 0 }; + boundary = si.GetWordBoundary({ 15, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find lead with 0n"); + expectedFirst = COORD{ 22, 0 }; + expectedSecond = COORD{ 28, 0 }; + boundary = si.GetWordBoundary({ 22, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); + + Log::Comment(L"Find lead with 0N"); + expectedFirst = on ? COORD{ 30, 0 } : COORD{ 29, 0 }; + expectedSecond = COORD{ 35, 0 }; + boundary = si.GetWordBoundary({ 29, 0 }); + VERIFY_ARE_EQUAL(expectedFirst, boundary.first); + VERIFY_ARE_EQUAL(expectedSecond, boundary.second); +} + +void ScreenBufferTests::GetWordBoundaryTrimZerosOn() +{ + GetWordBoundaryTrimZeros(true); +} + +void ScreenBufferTests::GetWordBoundaryTrimZerosOff() +{ + GetWordBoundaryTrimZeros(false); +} + +void ScreenBufferTests::TestAltBufferCursorState() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + Log::Comment(L"Creating one alternate buffer"); + auto& original = gci.GetActiveOutputBuffer(); + VERIFY_IS_NULL(original._psiAlternateBuffer); + VERIFY_IS_NULL(original._psiMainBuffer); + + NTSTATUS Status = original.UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"Alternate buffer successfully created"); + auto& alternate = gci.GetActiveOutputBuffer(); + // Make sure that when the test is done, we switch back to the main buffer. + // Otherwise, one test could pollute another. + auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); }); + + const auto* pMain = &original; + const auto* pAlt = &alternate; + // Validate that the pointers were mapped appropriately to link + // alternate and main buffers + VERIFY_ARE_NOT_EQUAL(pMain, pAlt); + VERIFY_ARE_EQUAL(pAlt, original._psiAlternateBuffer); + VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer); + VERIFY_IS_NULL(original._psiMainBuffer); + VERIFY_IS_NULL(alternate._psiAlternateBuffer); + + auto& mainCursor = original.GetTextBuffer().GetCursor(); + auto& altCursor = alternate.GetTextBuffer().GetCursor(); + + // Validate that the cursor state was copied appropriately into the + // alternate buffer + VERIFY_ARE_EQUAL(mainCursor.GetSize(), altCursor.GetSize()); + VERIFY_ARE_EQUAL(mainCursor.GetColor(), altCursor.GetColor()); + VERIFY_ARE_EQUAL(mainCursor.GetType(), altCursor.GetType()); + } +} + +void ScreenBufferTests::TestAltBufferVtDispatching() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to manipulate buffer. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + Log::Comment(L"Creating one alternate buffer"); + auto& mainBuffer = gci.GetActiveOutputBuffer(); + // Make sure we're in VT mode + WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + // Make sure we're suing the default attributes at the start of the test, + // Otherwise they could be polluted from a previous test. + mainBuffer.SetAttributes(gci.GetDefaultAttributes()); + + VERIFY_IS_NULL(mainBuffer._psiAlternateBuffer); + VERIFY_IS_NULL(mainBuffer._psiMainBuffer); + + NTSTATUS Status = mainBuffer.UseAlternateScreenBuffer(); + if(VERIFY_IS_TRUE(NT_SUCCESS(Status))) + { + Log::Comment(L"Alternate buffer successfully created"); + auto& alternate = gci.GetActiveOutputBuffer(); + // Make sure that when the test is done, we switch back to the main buffer. + // Otherwise, one test could pollute another. + auto useMain = wil::scope_exit([&] { alternate.UseMainScreenBuffer(); }); + // Manually turn on VT mode - usually gci enables this for you. + WI_SetFlag(alternate.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto* pMain = &mainBuffer; + const auto* pAlt = &alternate; + // Validate that the pointers were mapped appropriately to link + // alternate and main buffers + VERIFY_ARE_NOT_EQUAL(pMain, pAlt); + VERIFY_ARE_EQUAL(pAlt, mainBuffer._psiAlternateBuffer); + VERIFY_ARE_EQUAL(pMain, alternate._psiMainBuffer); + VERIFY_IS_NULL(mainBuffer._psiMainBuffer); + VERIFY_IS_NULL(alternate._psiAlternateBuffer); + + auto& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); + auto& altCursor = alternate.GetTextBuffer().GetCursor(); + + const COORD origin = {0, 0}; + mainCursor.SetPosition(origin); + altCursor.SetPosition(origin); + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, origin, true)); + VERIFY_SUCCEEDED(alternate.SetViewportOrigin(true, origin, true)); + VERIFY_ARE_EQUAL(origin, mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(origin, altCursor.GetPosition()); + + // We're going to write some data to either the main buffer or the alt + // buffer, as if we were using the API. + + std::unique_ptr waiter; + std::wstring seq = L"\x1b[5;6H"; + size_t seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + + VERIFY_ARE_EQUAL(COORD({0, 0}), mainCursor.GetPosition()); + // recall: vt coordinates are (row, column), 1-indexed + VERIFY_ARE_EQUAL(COORD({5, 4}), altCursor.GetPosition()); + + const TextAttribute expectedDefaults = gci.GetDefaultAttributes(); + TextAttribute expectedRgb = expectedDefaults; + expectedRgb.SetBackground(RGB(255, 0, 255)); + + VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes()); + VERIFY_ARE_EQUAL(expectedDefaults, alternate.GetAttributes()); + + seq = L"\x1b[48;2;255;0;255m"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + + VERIFY_ARE_EQUAL(expectedDefaults, mainBuffer.GetAttributes()); + VERIFY_ARE_EQUAL(expectedRgb, alternate.GetAttributes()); + + seq = L"X"; + seqCb = 2 * seq.size(); + VERIFY_SUCCEEDED(DoWriteConsole(&seq[0], &seqCb, mainBuffer, waiter)); + + VERIFY_ARE_EQUAL(COORD({0, 0}), mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(COORD({6, 4}), altCursor.GetPosition()); + + // Recall we didn't print an 'X' to the main buffer, so there's no + // char to inspect the attributes of. + const ROW& altRow = alternate.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); + const auto altAttrRow = &altRow.GetAttrRow(); + const std::vector altAttrs{ altAttrRow->begin(), altAttrRow->end() }; + const auto altAttrA = altAttrs[altCursor.GetPosition().X - 1]; + VERIFY_ARE_EQUAL(expectedRgb, altAttrA); + } +} + +void ScreenBufferTests::SetDefaultsIndividuallyBothDefault() +{ + // Tests MSFT:19828103 + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + COLORREF yellow = RGB(255, 255, 0); + COLORREF brightGreen = gci.GetColorTableEntry(::XtermToWindowsIndex(10)); + COLORREF darkBlue = gci.GetColorTableEntry(::XtermToWindowsIndex(4)); + + gci.SetDefaultForegroundColor(yellow); + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + Log::Comment(NoThrowString().Format(L"Write 6 X's:")); + Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)")); + Log::Comment(NoThrowString().Format(L" The second with bright-green on dark-blue")); + Log::Comment(NoThrowString().Format(L" The third with default-fg on dark-blue")); + Log::Comment(NoThrowString().Format(L" The fourth in default-fg on default-bg (yellow on magenta)")); + Log::Comment(NoThrowString().Format(L" The fifth with bright-green on dark-blue")); + Log::Comment(NoThrowString().Format(L" The sixth with bright-green on default-bg")); + + std::wstring seq = L"\x1b[m"; // Reset to defaults + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[92;44m"; // bright-green on dark-blue + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[39m"; // reset fg + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[49m"; // reset bg + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[92;44m"; // bright-green on dark-blue + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[49m"; // reset bg + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + // See the log comment above for description of these values. + TextAttribute expectedDefaults{}; + TextAttribute expectedTwo{FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE}; + TextAttribute expectedThree{FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE}; + expectedThree.SetDefaultForeground(); + // Four is the same as Defaults + // Five is the same as two + TextAttribute expectedSix{FOREGROUND_GREEN | FOREGROUND_INTENSITY | BACKGROUND_BLUE}; + expectedSix.SetDefaultBackground(); + + COORD expectedCursor{6, 0}; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const ROW& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + const auto attrC = attrs[2]; + const auto attrD = attrs[3]; + const auto attrE = attrs[4]; + const auto attrF = attrs[5]; + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + LOG_ATTR(attrD); + LOG_ATTR(attrE); + LOG_ATTR(attrF); + + VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); + VERIFY_ARE_EQUAL(true, attrB.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrD.IsLegacy()); + VERIFY_ARE_EQUAL(true, attrE.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrF.IsLegacy()); + + VERIFY_ARE_EQUAL(expectedDefaults, attrA); + VERIFY_ARE_EQUAL(expectedTwo, attrB); + VERIFY_ARE_EQUAL(expectedThree, attrC); + VERIFY_ARE_EQUAL(expectedDefaults, attrD); + VERIFY_ARE_EQUAL(expectedTwo, attrE); + VERIFY_ARE_EQUAL(expectedSix, attrF); + + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrA)); + VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrB)); + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrC)); + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrD)); + VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrE)); + VERIFY_ARE_EQUAL(brightGreen, gci.LookupForegroundColor(attrF)); + + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrB)); + VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrC)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrD)); + VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrE)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrF)); +} + +void ScreenBufferTests::SetDefaultsTogether() +{ + // Tests MSFT:19828103 + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + COLORREF yellow = RGB(255, 255, 0); + COLORREF color250 = gci.GetColorTableEntry(250); + + gci.SetDefaultForegroundColor(yellow); + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + Log::Comment(NoThrowString().Format(L"Write 6 X's:")); + Log::Comment(NoThrowString().Format(L" The first in default-fg on default-bg (yellow on magenta)")); + Log::Comment(NoThrowString().Format(L" The second with default-fg on xterm(250)")); + Log::Comment(NoThrowString().Format(L" The third with defaults again")); + + std::wstring seq = L"\x1b[m"; // Reset to defaults + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[48;5;250m"; // bright-green on dark-blue + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + seq = L"\x1b[39;49m"; // reset fg + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + // See the log comment above for description of these values. + TextAttribute expectedDefaults{}; + TextAttribute expectedTwo{}; + expectedTwo.SetBackground(color250); + + COORD expectedCursor{3, 0}; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const ROW& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + const auto attrC = attrs[2]; + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + + VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); + + VERIFY_ARE_EQUAL(expectedDefaults, attrA); + VERIFY_ARE_EQUAL(expectedTwo, attrB); + VERIFY_ARE_EQUAL(expectedDefaults, attrC); + + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrA)); + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrB)); + VERIFY_ARE_EQUAL(yellow, gci.LookupForegroundColor(attrC)); + + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(color250, gci.LookupBackgroundColor(attrB)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrC)); +} + + +void ScreenBufferTests::ReverseResetWithDefaultBackground() +{ + // Tests MSFT:19694089 + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + + gci.SetDefaultForegroundColor(INVALID_COLOR); + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + Log::Comment(NoThrowString().Format(L"Write 3 X's:")); + Log::Comment(NoThrowString().Format(L" The first in default-attr on default color (magenta)")); + Log::Comment(NoThrowString().Format(L" The second with reversed attrs")); + Log::Comment(NoThrowString().Format(L" The third after resetting the attrs back")); + + std::wstring seq = L"X"; + stateMachine.ProcessString(seq); + seq = L"\x1b[7m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + seq = L"\x1b[27m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + + TextAttribute expectedDefaults{gci.GetFillAttribute()}; + expectedDefaults.SetDefaultBackground(); + TextAttribute expectedReversed = expectedDefaults; + expectedReversed.Invert(); + + COORD expectedCursor{3, 0}; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const ROW& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + const auto attrC = attrs[2]; + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + + VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrC.IsLegacy()); + + VERIFY_ARE_EQUAL(false, WI_IsFlagSet(attrA.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO)); + VERIFY_ARE_EQUAL(true, WI_IsFlagSet(attrB.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO)); + VERIFY_ARE_EQUAL(false, WI_IsFlagSet(attrC.GetMetaAttributes(), COMMON_LVB_REVERSE_VIDEO)); + + VERIFY_ARE_EQUAL(expectedDefaults, attrA); + VERIFY_ARE_EQUAL(expectedReversed, attrB); + VERIFY_ARE_EQUAL(expectedDefaults, attrC); + + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(magenta, gci.LookupForegroundColor(attrB)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrC)); +} + +void ScreenBufferTests::BackspaceDefaultAttrs() +{ + // Created for MSFT:19735050, but doesn't actually test that. + // That bug actually involves the input line, and that needs to use + // TextAttributes instead of WORDs + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one.")); + + std::wstring seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + seq = L"XX"; + stateMachine.ProcessString(seq); + + seq = UNICODE_BACKSPACE; + stateMachine.ProcessString(seq); + + TextAttribute expectedDefaults{}; + expectedDefaults.SetDefaultBackground(); + + COORD expectedCursor{1, 0}; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const ROW& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); + + VERIFY_ARE_EQUAL(expectedDefaults, attrA); + VERIFY_ARE_EQUAL(expectedDefaults, attrB); + + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrB)); +} + +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}") + END_TEST_METHOD_PROPERTIES(); + + bool writeSingly; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false"); + + DWORD writeCharsLegacyMode; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L""); + + // Created for MSFT:19735050. + // Kinda the same as above, but with WriteCharsLegacy instead. + // The variable that really breaks this scenario + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + Log::Comment(NoThrowString().Format(L"Write 2 X's, then backspace one.")); + + std::wstring seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + + if (writeSingly) + { + wchar_t* str = L"X"; + size_t seqCb = 2; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + str = L"\x08"; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + } + else + { + wchar_t* str = L"XX\x08"; + size_t seqCb = 6; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, writeCharsLegacyMode, nullptr)); + } + + TextAttribute expectedDefaults{}; + expectedDefaults.SetDefaultBackground(); + + COORD expectedCursor{1, 0}; + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const ROW& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(false, attrA.IsLegacy()); + VERIFY_ARE_EQUAL(false, attrB.IsLegacy()); + + VERIFY_ARE_EQUAL(expectedDefaults, attrA); + VERIFY_ARE_EQUAL(expectedDefaults, attrB); + + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(magenta, gci.LookupBackgroundColor(attrB)); +} + +void ScreenBufferTests::BackspaceDefaultAttrsInPrompt() +{ + // Tests MSFT:19853701 - when you edit the prompt line at a bash prompt, + // make sure that the end of the line isn't filled with default/garbage attributes. + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = si.GetTextBuffer().GetCursor(); + // Make sure we're in VT mode + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(si.SetViewportOrigin(true, COORD({0, 0}), true)); + cursor.SetPosition({0, 0}); + + COLORREF magenta = RGB(255, 0, 255); + + gci.SetDefaultBackgroundColor(magenta); + si.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + TextAttribute expectedDefaults{}; + + Log::Comment(NoThrowString().Format(L"Write 3 X's, move to the left, then delete-char the second.")); + Log::Comment(NoThrowString().Format(L"This emulates editing the prompt line on bash")); + + std::wstring seq = L"\x1b[m"; + stateMachine.ProcessString(seq); + Log::Comment(NoThrowString().Format( + L"Clear the screen - make sure the line is filled with the current attributes." + )); + seq = L"\x1b[2J"; + stateMachine.ProcessString(seq); + + const auto viewport = si.GetViewport(); + const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + Log::Comment(NoThrowString().Format( + L"Make sure the row contains what we're expecting before we start." + L"It should entirely be filled with defaults" + )); + + const std::vector initialAttrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x <= viewport.RightInclusive(); x++) + { + const auto& attr = initialAttrs[x]; + VERIFY_ARE_EQUAL(expectedDefaults, attr); + } + } + Log::Comment(NoThrowString().Format( + L"Print 'XXX', move the cursor left 2, delete a character." + )); + + seq = L"XXX"; + stateMachine.ProcessString(seq); + seq = L"\x1b[2D"; + stateMachine.ProcessString(seq); + seq = L"\x1b[P"; + stateMachine.ProcessString(seq); + + COORD expectedCursor{1, 1}; // We're expecting y=1, because the 2J above + // should have moved the viewport down a line. + VERIFY_ARE_EQUAL(expectedCursor, cursor.GetPosition()); + + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + for (int x = 0; x <= viewport.RightInclusive(); x++) + { + const auto& attr = attrs[x]; + VERIFY_ARE_EQUAL(expectedDefaults, attr); + } +} + +void ScreenBufferTests::SetGlobalColorTable() +{ + // Created for MSFT:19723934. + // Changing the value of the color table should apply to the attributes in + // both the alt AND main buffer. While many other properties should be + // reset upon returning to the main buffer, the color table is a + // global property. This behavior is consistent with other terminals + // tested. + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to swap buffers. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); + VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); + WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + StateMachine& stateMachine = mainBuffer.GetStateMachine(); + Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({0, 0}), true)); + mainCursor.SetPosition({0, 0}); + + const COLORREF originalRed = gci.GetColorTableEntry(4); + const COLORREF testColor = RGB(0x11, 0x22, 0x33); + VERIFY_ARE_NOT_EQUAL(originalRed, testColor); + + std::wstring seq = L"\x1b[41m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + COORD expectedCursor{1, 0}; + VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); + { + const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + LOG_ATTR(attrA); + VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA)); + } + + Log::Comment(NoThrowString().Format(L"Create an alt buffer")); + + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); + auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + + WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor(); + altCursor.SetPosition({0, 0}); + + Log::Comment(NoThrowString().Format( + L"Print one X in red, should be the original red color" + )); + seq = L"\x1b[41m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); + { + const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + LOG_ATTR(attrA); + VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA)); + } + + Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)")); + seq = L"\x1b]4;1;rgb:11/22/33\x07"; + stateMachine.ProcessString(seq); + Log::Comment(NoThrowString().Format( + L"Print another X, both should be the new \"red\" color" + )); + seq = L"X"; + stateMachine.ProcessString(seq); + VERIFY_ARE_EQUAL(COORD({2, 0}), altCursor.GetPosition()); + { + const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + LOG_ATTR(attrA); + LOG_ATTR(attrB); + VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB)); + } + + Log::Comment(NoThrowString().Format(L"Switch back to the main buffer")); + useMain.release(); + altBuffer.UseMainScreenBuffer(); + + const auto& mainBufferPostSwitch = gci.GetActiveOutputBuffer(); + VERIFY_ARE_EQUAL(&mainBufferPostSwitch, &mainBuffer); + + Log::Comment(NoThrowString().Format( + L"Print another X, both should be the new \"red\" color" + )); + seq = L"X"; + stateMachine.ProcessString(seq); + VERIFY_ARE_EQUAL(COORD({2, 0}), mainCursor.GetPosition()); + { + const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + LOG_ATTR(attrA); + LOG_ATTR(attrB); + VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrA)); + VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB)); + } +} + +void ScreenBufferTests::SetColorTableThreeDigits() +{ + // Created for MSFT:19723934. + // Changing the value of the color table above index 99 should work + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); // Lock must be taken to swap buffers. + auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); }); + + SCREEN_INFORMATION& mainBuffer = gci.GetActiveOutputBuffer(); + VERIFY_IS_FALSE(mainBuffer._IsAltBuffer()); + WI_SetFlag(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(mainBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + StateMachine& stateMachine = mainBuffer.GetStateMachine(); + Cursor& mainCursor = mainBuffer.GetTextBuffer().GetCursor(); + + Log::Comment(NoThrowString().Format(L"Make sure the viewport is at 0,0")); + VERIFY_SUCCEEDED(mainBuffer.SetViewportOrigin(true, COORD({0, 0}), true)); + mainCursor.SetPosition({0, 0}); + + const COLORREF originalRed = gci.GetColorTableEntry(123); + const COLORREF testColor = RGB(0x11, 0x22, 0x33); + VERIFY_ARE_NOT_EQUAL(originalRed, testColor); + + std::wstring seq = L"\x1b[48;5;123m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + COORD expectedCursor{1, 0}; + VERIFY_ARE_EQUAL(expectedCursor, mainCursor.GetPosition()); + { + const ROW& row = mainBuffer.GetTextBuffer().GetRowByOffset(mainCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + LOG_ATTR(attrA); + VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA)); + } + + Log::Comment(NoThrowString().Format(L"Create an alt buffer")); + + VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer()); + SCREEN_INFORMATION& altBuffer = gci.GetActiveOutputBuffer(); + auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); + + WI_SetFlag(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + VERIFY_IS_TRUE(WI_IsFlagSet(altBuffer.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)); + + Cursor& altCursor = altBuffer.GetTextBuffer().GetCursor(); + altCursor.SetPosition({0, 0}); + + Log::Comment(NoThrowString().Format( + L"Print one X in red, should be the original red color" + )); + seq = L"\x1b[48;5;123m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + VERIFY_ARE_EQUAL(expectedCursor, altCursor.GetPosition()); + { + const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + LOG_ATTR(attrA); + VERIFY_ARE_EQUAL(originalRed, gci.LookupBackgroundColor(attrA)); + } + + Log::Comment(NoThrowString().Format(L"Change the value of red to RGB(0x11, 0x22, 0x33)")); + seq = L"\x1b]4;123;rgb:11/22/33\x07"; + stateMachine.ProcessString(seq); + Log::Comment(NoThrowString().Format( + L"Print another X, it should be the new \"red\" color" + )); + // TODO MSFT:20105972 - + // You shouldn't need to manually update the attributes again. + seq = L"\x1b[48;5;123m"; + stateMachine.ProcessString(seq); + seq = L"X"; + stateMachine.ProcessString(seq); + VERIFY_ARE_EQUAL(COORD({2, 0}), altCursor.GetPosition()); + { + const ROW& row = altBuffer.GetTextBuffer().GetRowByOffset(altCursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrB = attrs[1]; + // TODO MSFT:20105972 - attrA and attrB should both be the same color now + LOG_ATTR(attrB); + VERIFY_ARE_EQUAL(testColor, gci.LookupBackgroundColor(attrB)); + } + +} + +void ScreenBufferTests::DeleteCharsNearEndOfLine() +{ + // Created for MSFT:19888564. + // There are some cases when you DCH N chars, where there are artifacts left + // from the previous contents of the row after the DCH finishes. + // If you are deleting N chars, + // and there are N+X chars left in the row after the cursor, such that X v_w - 1 - c_x - d) && (v_w - 1 - c_x - d >= 0)` + // where: + // - `d`: num chars to delete + // - `v_w`: viewport.Width() + // - `c_x`: cursor.X + // + // Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase) + // start with the following buffer contents, and the cursor on the "D" + // [ABCDEFG ] + // ^ + // When you DCH(3) here, we are trying to delete the D, E and F. + // We do that by shifting the contents of the line after the deleted + // characters to the left. HOWEVER, there are only 2 chars left to move. + // So (before the fix) the buffer end up like this: + // [ABCG F ] + // ^ + // The G and " " have moved, but the F did not get overwritten. + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:dx", L"{1, 2, 3, 5, 8, 13, 21, 34}") + TEST_METHOD_PROPERTY(L"Data:numCharsToDelete", L"{1, 2, 3, 5, 8, 13, 21, 34}") + END_TEST_METHOD_PROPERTIES(); + + int dx; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"Distance to move the cursor back into the line"); + + int numCharsToDelete; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"numCharsToDelete", numCharsToDelete), L"Number of characters to delete"); + + // let W = viewport.Width + // Print W 'X' chars + // Move to (0, W-dx) + // DCH(numCharsToDelete) + // There should be N 'X' chars, and then numSpaces spaces + // where + // numSpaces = min(dx, numCharsToDelete) + // N = W - numSpaces + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& mainBuffer = gci.GetActiveOutputBuffer(); + auto& tbi = mainBuffer.GetTextBuffer(); + auto& stateMachine = mainBuffer.GetStateMachine(); + auto& mainCursor = tbi.GetCursor(); + auto& mainView = mainBuffer.GetViewport(); + + VERIFY_ARE_EQUAL(COORD({0, 0}), mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); + VERIFY_IS_GREATER_THAN(mainView.Width(), (dx + numCharsToDelete)); + + std::wstring seq = L"X"; + for (int x = 0; x < mainView.Width(); x++) + { + stateMachine.ProcessString(seq); + } + + VERIFY_ARE_EQUAL(COORD({mainView.Width() - 1, 0}), mainCursor.GetPosition()); + + Log::Comment(NoThrowString().Format( + L"row_i=[%s]", + tbi.GetRowByOffset(0).GetText().c_str() + )); + + mainCursor.SetPosition({mainView.Width() - static_cast(dx), 0}); + std::wstringstream ss; + ss << L"\x1b[" << numCharsToDelete << L"P"; + seq = ss.str(); // Delete N chars + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format( + L"row_f=[%s]", + tbi.GetRowByOffset(0).GetText().c_str() + )); + VERIFY_ARE_EQUAL(COORD({mainView.Width() - static_cast(dx), 0}), mainCursor.GetPosition()); + auto iter = tbi.GetCellDataAt({0, 0}); + auto expectedNumSpaces = std::min(dx, numCharsToDelete); + for (int x = 0; x < mainView.Width() - expectedNumSpaces; x++) + { + SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures); + if (iter->Chars() != L"X") + { + Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x)); + } + VERIFY_ARE_EQUAL(L"X", iter->Chars()); + iter++; + } + for (int x = mainView.Width() - expectedNumSpaces; x < mainView.Width(); x++) + { + if (iter->Chars() != L"\x20" ) + { + Log::Comment(NoThrowString().Format(L"character [%d] was mismatched", x)); + } + VERIFY_ARE_EQUAL(L"\x20" , iter->Chars()); + iter++; + } + +} + +void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleFirstCase() +{ + // Created for MSFT:19888564. + // This is a single case that I'm absolutely sure will repro this bug - + // DeleteCharsNearEndOfLine is the more comprehensive version of this test. + // Write a string, move the cursor into it, then delete some chars. + // There should be no artifacts left behind. + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& stateMachine = si.GetStateMachine(); + const auto newBufferWidth = 8; + + VERIFY_SUCCEEDED(si.ResizeScreenBuffer({newBufferWidth, si.GetBufferSize().Height()}, false)); + auto& mainBuffer = gci.GetActiveOutputBuffer(); + + const COORD newViewSize{newBufferWidth, mainBuffer.GetViewport().Height()}; + mainBuffer.SetViewportSize(&newViewSize); + auto& tbi = mainBuffer.GetTextBuffer(); + auto& mainView = mainBuffer.GetViewport(); + auto& mainCursor = tbi.GetCursor(); + + VERIFY_ARE_EQUAL(COORD({0, 0}), mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width()); + VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); + + std::wstring seq = L"ABCDEFG"; + stateMachine.ProcessString(seq); + + VERIFY_ARE_EQUAL(COORD({7, 0}), mainCursor.GetPosition()); + // Place the cursor on the 'D' + mainCursor.SetPosition({3, 0}); + + Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); + // Delete 3 chars - [D, E, F] + std::wstringstream ss; + ss << L"\x1b[" << 3 << L"P"; + seq = ss.str(); + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); + + // Cursor shouldn't have moved + VERIFY_ARE_EQUAL(COORD({3, 0}), mainCursor.GetPosition()); + + auto iter = tbi.GetCellDataAt({0, 0}); + VERIFY_ARE_EQUAL(L"A", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"B", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"C", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"G", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; +} + +void ScreenBufferTests::DeleteCharsNearEndOfLineSimpleSecondCase() +{ + // Created for MSFT:19888564. + // This is another single case that I'm absolutely sure will repro this bug + // DeleteCharsNearEndOfLine is the more comprehensive version of this test. + // Write a string, move the cursor into it, then delete some chars. + // There should be no artifacts left behind. + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& stateMachine = si.GetStateMachine(); + + const auto newBufferWidth = 8; + VERIFY_SUCCEEDED(si.ResizeScreenBuffer({newBufferWidth, si.GetBufferSize().Height()}, false)); + auto& mainBuffer = gci.GetActiveOutputBuffer(); + + const COORD newViewSize{newBufferWidth, mainBuffer.GetViewport().Height()}; + mainBuffer.SetViewportSize(&newViewSize); + auto& tbi = mainBuffer.GetTextBuffer(); + auto& mainView = mainBuffer.GetViewport(); + auto& mainCursor = tbi.GetCursor(); + + VERIFY_ARE_EQUAL(COORD({0, 0}), mainCursor.GetPosition()); + VERIFY_ARE_EQUAL(newBufferWidth, mainView.Width()); + VERIFY_ARE_EQUAL(mainBuffer.GetBufferSize().Width(), mainView.Width()); + + std::wstring seq = L"ABCDEFG"; + stateMachine.ProcessString(seq); + + VERIFY_ARE_EQUAL(COORD({7, 0}), mainCursor.GetPosition()); + + // Place the cursor on the 'C' + mainCursor.SetPosition({2, 0}); + + Log::Comment(NoThrowString().Format(L"before=[%s]", tbi.GetRowByOffset(0).GetText().c_str())); + + // Delete 4 chars - [C, D, E, F] + std::wstringstream ss; + ss << L"\x1b[" << 4 << L"P"; + seq = ss.str(); + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format(L"after =[%s]", tbi.GetRowByOffset(0).GetText().c_str())); + + VERIFY_ARE_EQUAL(COORD({2, 0}), mainCursor.GetPosition()); + + auto iter = tbi.GetCellDataAt({0, 0}); + VERIFY_ARE_EQUAL(L"A", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"B", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"G", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + VERIFY_ARE_EQUAL(L"\x20", iter->Chars()); + iter++; + +} + +void ScreenBufferTests::DontResetColorsAboveVirtualBottom() +{ + // Created for MSFT:19989333. + // Print some colored text, then scroll the viewport up, so the colored text + // is below the visible viewport. Change the colors, then write a character. + // Both the old chars and the new char should have different colors, the + // first character should not have been reset to the new colors. + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, {0, 1}, true)); + cursor.SetPosition({0, si.GetViewport().BottomInclusive()}); + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + const auto darkRed = gci.GetColorTableEntry(::XtermToWindowsIndex(1)); + const auto darkBlue = gci.GetColorTableEntry(::XtermToWindowsIndex(4)); + const auto darkBlack = gci.GetColorTableEntry(::XtermToWindowsIndex(0)); + const auto darkWhite = gci.GetColorTableEntry(::XtermToWindowsIndex(7)); + stateMachine.ProcessString(L"\x1b[31;44m"); + stateMachine.ProcessString(L"X"); + stateMachine.ProcessString(L"\x1b[m"); + stateMachine.ProcessString(L"X"); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + VERIFY_ARE_EQUAL(2, cursor.GetPosition().X); + { + const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + LOG_ATTR(attrA); + LOG_ATTR(attrB); + VERIFY_ARE_EQUAL(darkRed, gci.LookupForegroundColor(attrA)); + VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrA)); + + VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrB)); + VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrB)); + } + + Log::Comment(NoThrowString().Format(L"Emulate scrolling up with the mouse")); + VERIFY_SUCCESS_NTSTATUS(si.SetViewportOrigin(true, {0, 0}, false)); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_IS_GREATER_THAN(cursor.GetPosition().Y, si.GetViewport().BottomInclusive()); + + stateMachine.ProcessString(L"X"); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(3, cursor.GetPosition().X); + { + const ROW& row = tbi.GetRowByOffset(cursor.GetPosition().Y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + const auto attrC = attrs[1]; + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + VERIFY_ARE_EQUAL(darkRed, gci.LookupForegroundColor(attrA)); + VERIFY_ARE_EQUAL(darkBlue, gci.LookupBackgroundColor(attrA)); + + VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrB)); + VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrB)); + + VERIFY_ARE_EQUAL(darkWhite, gci.LookupForegroundColor(attrC)); + VERIFY_ARE_EQUAL(darkBlack, gci.LookupBackgroundColor(attrC)); + } +} + +void _CommonScrollingSetup() +{ + // Used for testing MSFT:20204600 + // Place an A on the first line, and a B on the 6th line (index 5). + // Set the scrolling region in between those lines (so scrolling won't affect them.) + // First write "1\n2\n3\n4", to put 1-4 on the lines in between the A and B. + // the viewport will look like: + // A + // 1 + // 2 + // 3 + // 4 + // B + // then write "\n5\n6\n7\n", which will cycle around the scroll region a bit. + // the viewport will look like: + // A + // 5 + // 6 + // 7 + // + // B + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + const auto oldView = si.GetViewport(); + const auto view = Viewport::FromDimensions({0, 0}, {oldView.Width(), 6}); + si.SetViewport(view, true); + cursor.SetPosition({0, 0}); + std::wstring seq = L"A"; + stateMachine.ProcessString(seq); + cursor.SetPosition({0, 5}); + seq = L"B"; + stateMachine.ProcessString(seq); + seq = L"\x1b[2;5r"; + stateMachine.ProcessString(seq); + seq = L"\x1b[2;1H"; + stateMachine.ProcessString(seq); + seq = L"1\n2\n3\n4"; + stateMachine.ProcessString(seq); + + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(1, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); + { + auto iter0 = tbi.GetCellDataAt({0, 0}); + auto iter1 = tbi.GetCellDataAt({0, 1}); + auto iter2 = tbi.GetCellDataAt({0, 2}); + auto iter3 = tbi.GetCellDataAt({0, 3}); + auto iter4 = tbi.GetCellDataAt({0, 4}); + auto iter5 = tbi.GetCellDataAt({0, 5}); + VERIFY_ARE_EQUAL(L"A" , iter0->Chars()); + VERIFY_ARE_EQUAL(L"1" , iter1->Chars()); + VERIFY_ARE_EQUAL(L"2" , iter2->Chars()); + VERIFY_ARE_EQUAL(L"3" , iter3->Chars()); + VERIFY_ARE_EQUAL(L"4" , iter4->Chars()); + VERIFY_ARE_EQUAL(L"B" , iter5->Chars()); + } + + + seq = L"\n5\n6\n7\n"; + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); + { + auto iter0 = tbi.GetCellDataAt({0, 0}); + auto iter1 = tbi.GetCellDataAt({0, 1}); + auto iter2 = tbi.GetCellDataAt({0, 2}); + auto iter3 = tbi.GetCellDataAt({0, 3}); + auto iter4 = tbi.GetCellDataAt({0, 4}); + auto iter5 = tbi.GetCellDataAt({0, 5}); + VERIFY_ARE_EQUAL(L"A" , iter0->Chars()); + VERIFY_ARE_EQUAL(L"5" , iter1->Chars()); + VERIFY_ARE_EQUAL(L"6" , iter2->Chars()); + VERIFY_ARE_EQUAL(L"7" , iter3->Chars()); + // Chars() will return a single space for an empty row. + VERIFY_ARE_EQUAL(L"\x20" , iter4->Chars()); + VERIFY_ARE_EQUAL(L"B" , iter5->Chars()); + } +} + +void ScreenBufferTests::ScrollUpInMargins() +{ + // Tests MSFT:20204600 + // Do the common scrolling setup, then executes a Scroll Up, and verifies + // the rows have what we'd expect. + + _CommonScrollingSetup(); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + // Execute a Scroll Up command + std::wstring seq = L"\x1b[S"; + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); + { + auto iter0 = tbi.GetCellDataAt({0, 0}); + auto iter1 = tbi.GetCellDataAt({0, 1}); + auto iter2 = tbi.GetCellDataAt({0, 2}); + auto iter3 = tbi.GetCellDataAt({0, 3}); + auto iter4 = tbi.GetCellDataAt({0, 4}); + auto iter5 = tbi.GetCellDataAt({0, 5}); + VERIFY_ARE_EQUAL(L"A" , iter0->Chars()); + VERIFY_ARE_EQUAL(L"6" , iter1->Chars()); + VERIFY_ARE_EQUAL(L"7" , iter2->Chars()); + VERIFY_ARE_EQUAL(L"\x20" , iter3->Chars()); + VERIFY_ARE_EQUAL(L"\x20" , iter4->Chars()); + VERIFY_ARE_EQUAL(L"B" , iter5->Chars()); + } + +} + +void ScreenBufferTests::ScrollDownInMargins() +{ + // Tests MSFT:20204600 + // Do the common scrolling setup, then executes a Scroll Down, and verifies + // the rows have what we'd expect. + + _CommonScrollingSetup(); + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& tbi = si.GetTextBuffer(); + auto& stateMachine = si.GetStateMachine(); + auto& cursor = si.GetTextBuffer().GetCursor(); + + // Execute a Scroll Down command + std::wstring seq = L"\x1b[T"; + stateMachine.ProcessString(seq); + + Log::Comment(NoThrowString().Format( + L"cursor=%s", VerifyOutputTraits::ToString(cursor.GetPosition()).GetBuffer() + )); + Log::Comment(NoThrowString().Format( + L"viewport=%s", VerifyOutputTraits::ToString(si.GetViewport().ToInclusive()).GetBuffer() + )); + + VERIFY_ARE_EQUAL(0, cursor.GetPosition().X); + VERIFY_ARE_EQUAL(4, cursor.GetPosition().Y); + { + auto iter0 = tbi.GetCellDataAt({0, 0}); + auto iter1 = tbi.GetCellDataAt({0, 1}); + auto iter2 = tbi.GetCellDataAt({0, 2}); + auto iter3 = tbi.GetCellDataAt({0, 3}); + auto iter4 = tbi.GetCellDataAt({0, 4}); + auto iter5 = tbi.GetCellDataAt({0, 5}); + VERIFY_ARE_EQUAL(L"A" , iter0->Chars()); + VERIFY_ARE_EQUAL(L"\x20", iter1->Chars()); + VERIFY_ARE_EQUAL(L"5" , iter2->Chars()); + VERIFY_ARE_EQUAL(L"6" , iter3->Chars()); + VERIFY_ARE_EQUAL(L"7" , iter4->Chars()); + VERIFY_ARE_EQUAL(L"B" , iter5->Chars()); + } +} diff --git a/src/host/ut_host/SearchTests.cpp b/src/host/ut_host/SearchTests.cpp new file mode 100644 index 000000000..bb42784c8 --- /dev/null +++ b/src/host/ut_host/SearchTests.cpp @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "search.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class SearchTests +{ + TEST_CLASS(SearchTests); + + CommonState* m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareNewTextBufferInfo(); + m_state->FillTextBuffer(); + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupNewTextBufferInfo(); + + return true; + } + + void DoFoundChecks(Search& s, COORD& coordStartExpected, SHORT lineDelta) + { + COORD coordEndExpected = coordStartExpected; + coordEndExpected.X += 1; + + VERIFY_IS_TRUE(s.FindNext()); + VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); + VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + + coordStartExpected.Y += lineDelta; + coordEndExpected.Y += lineDelta; + VERIFY_IS_TRUE(s.FindNext()); + VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); + VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + + coordStartExpected.Y += lineDelta; + coordEndExpected.Y += lineDelta; + VERIFY_IS_TRUE(s.FindNext()); + VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); + VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + + coordStartExpected.Y += lineDelta; + coordEndExpected.Y += lineDelta; + VERIFY_IS_TRUE(s.FindNext()); + VERIFY_ARE_EQUAL(coordStartExpected, s._coordSelStart); + VERIFY_ARE_EQUAL(coordEndExpected, s._coordSelEnd); + + VERIFY_IS_FALSE(s.FindNext()); + } + + TEST_METHOD(ForwardCaseSensitive) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 0 }; + Search s(outputBuffer, L"AB", Search::Direction::Forward, Search::Sensitivity::CaseSensitive); + DoFoundChecks(s, coordStartExpected, 1); + } + + TEST_METHOD(ForwardCaseSensitiveJapanese) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 2, 0 }; + Search s(outputBuffer, L"\x304b", Search::Direction::Forward, Search::Sensitivity::CaseSensitive); + DoFoundChecks(s, coordStartExpected, 1); + } + + TEST_METHOD(ForwardCaseInsensitive) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 0 }; + Search s(outputBuffer, L"ab", Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); + DoFoundChecks(s, coordStartExpected, 1); + } + + TEST_METHOD(ForwardCaseInsensitiveJapanese) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 2, 0 }; + Search s(outputBuffer, L"\x304b", Search::Direction::Forward, Search::Sensitivity::CaseInsensitive); + DoFoundChecks(s, coordStartExpected, 1); + } + + TEST_METHOD(BackwardCaseSensitive) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 0, 3 }; + Search s(outputBuffer, L"AB", Search::Direction::Backward, Search::Sensitivity::CaseSensitive); + DoFoundChecks(s, coordStartExpected, -1); + } + + TEST_METHOD(BackwardCaseSensitiveJapanese) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 2, 3 }; + Search s(outputBuffer, L"\x304b", Search::Direction::Backward, Search::Sensitivity::CaseSensitive); + DoFoundChecks(s, coordStartExpected, -1); + } + + TEST_METHOD(BackwardCaseInsensitive) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 0, 3 }; + Search s(outputBuffer, L"ab", Search::Direction::Backward, Search::Sensitivity::CaseInsensitive); + DoFoundChecks(s, coordStartExpected, -1); + } + + TEST_METHOD(BackwardCaseInsensitiveJapanese) + { + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + COORD coordStartExpected = { 2, 3 }; + Search s(outputBuffer, L"\x304b", Search::Direction::Backward, Search::Sensitivity::CaseInsensitive); + DoFoundChecks(s, coordStartExpected, -1); + } +}; diff --git a/src/host/ut_host/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp new file mode 100644 index 000000000..8bad4dc3c --- /dev/null +++ b/src/host/ut_host/SelectionTests.cpp @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" + +#include "selection.hpp" +#include "cmdline.h" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class SelectionTests +{ + TEST_CLASS(SelectionTests); + + CommonState* m_state; + Selection* m_pSelection; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + m_pSelection = &Selection::Instance(); + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_pSelection = nullptr; + + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + return true; + } + + void VerifyGetSelectionRects_BoxMode() + { + const auto selectionRects = m_pSelection->GetSelectionRects(); + const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1; + + if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size())) + { + for (auto iRect = 0; iRect < gsl::narrow(selectionRects.size()); iRect++) + { + // ensure each rectangle is exactly the width requested (block selection) + const SMALL_RECT* const psrRect = &selectionRects[iRect]; + + const short sRectangleLineNumber = (short)iRect + m_pSelection->_srSelectionRect.Top; + + VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber); + + VERIFY_ARE_EQUAL(psrRect->Left, m_pSelection->_srSelectionRect.Left); + VERIFY_ARE_EQUAL(psrRect->Right, m_pSelection->_srSelectionRect.Right); + } + } + } + + TEST_METHOD(TestGetSelectionRects_BoxMode) + { + m_pSelection->_fSelectionVisible = true; + + // set selection region + m_pSelection->_srSelectionRect.Top = 0; + m_pSelection->_srSelectionRect.Bottom = 3; + m_pSelection->_srSelectionRect.Left = 1; + m_pSelection->_srSelectionRect.Right = 10; + + // #1 top-left to bottom right selection first + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + + // A. false/false for the selection modes should mean box selection + m_pSelection->_fLineSelection = false; + m_pSelection->_fUseAlternateSelection = false; + + VerifyGetSelectionRects_BoxMode(); + + // B. true/true for the selection modes should also mean box selection + m_pSelection->_fLineSelection = true; + m_pSelection->_fUseAlternateSelection = true; + + VerifyGetSelectionRects_BoxMode(); + + // now try the other 3 configurations of box region. + // #2 top-right to bottom-left selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + + VerifyGetSelectionRects_BoxMode(); + + // #3 bottom-left to top-right selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + + VerifyGetSelectionRects_BoxMode(); + + // #4 bottom-right to top-left selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + + VerifyGetSelectionRects_BoxMode(); + } + + void VerifyGetSelectionRects_LineMode() + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + const auto selectionRects = m_pSelection->GetSelectionRects(); + const UINT cRectanglesExpected = m_pSelection->_srSelectionRect.Bottom - m_pSelection->_srSelectionRect.Top + 1; + + if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size())) + { + // RULES: + // 1. If we're only selection one line, select the entire region between the two rectangles. + // Else if we're selecting multiple lines... + // 2. Extend all lines except the last line to the right edge of the screen + // Extend all lines except the first line to the left edge of the screen + // 3. If our anchor is in the top-right or bottom-left corner of the rectangle... + // The inside portion of our rectangle on the first and last lines is invalid. + // Remove from selection (but preserve the anchors themselves). + + // RULE #1: If 1 line, entire region selected. + bool fHaveOneLine = selectionRects.size() == 1; + + if (fHaveOneLine) + { + SMALL_RECT srSelectionRect = m_pSelection->_srSelectionRect; + VERIFY_ARE_EQUAL(srSelectionRect.Top, srSelectionRect.Bottom); + + const SMALL_RECT* const psrRect = &selectionRects[0]; + + VERIFY_ARE_EQUAL(psrRect->Top, srSelectionRect.Top); + VERIFY_ARE_EQUAL(psrRect->Bottom, srSelectionRect.Bottom); + + VERIFY_ARE_EQUAL(psrRect->Left, srSelectionRect.Left); + VERIFY_ARE_EQUAL(psrRect->Right, srSelectionRect.Right); + } + else + { + // RULE #2 : Check extension to edges + for (UINT iRect = 0; iRect < selectionRects.size(); iRect++) + { + // ensure each rectangle is exactly the width requested (block selection) + const SMALL_RECT* const psrRect = &selectionRects[iRect]; + + const short sRectangleLineNumber = (short)iRect + m_pSelection->_srSelectionRect.Top; + + VERIFY_ARE_EQUAL(psrRect->Top, sRectangleLineNumber); + VERIFY_ARE_EQUAL(psrRect->Bottom, sRectangleLineNumber); + + bool fIsFirstLine = iRect == 0; + bool fIsLastLine = iRect == selectionRects.size() - 1; + + // for all lines except the last, the line should reach the right edge of the buffer + if (!fIsLastLine) + { + // buffer size = 80, then selection goes 0 to 79. Thus X - 1. + VERIFY_ARE_EQUAL(psrRect->Right, gci.GetActiveOutputBuffer().GetTextBuffer().GetSize().RightInclusive()); + } + + // for all lines except the first, the line should reach the left edge of the buffer + if (!fIsFirstLine) + { + VERIFY_ARE_EQUAL(psrRect->Left, 0); + } + } + + // RULE #3: Check first and last line have invalid regions removed, if applicable + UINT iFirst = 0; + UINT iLast = gsl::narrow(selectionRects.size() - 1u); + + const SMALL_RECT* const psrFirst = &selectionRects[iFirst]; + const SMALL_RECT* const psrLast = &selectionRects[iLast]; + + bool fRemoveRegion = false; + + SMALL_RECT srSelectionRect = m_pSelection->_srSelectionRect; + COORD coordAnchor = m_pSelection->_coordSelectionAnchor; + + // if the anchor is in the top right or bottom left corner, we must have removed a region. otherwise, it stays as is. + if (coordAnchor.Y == srSelectionRect.Top && coordAnchor.X == srSelectionRect.Right) + { + fRemoveRegion = true; + } + else if (coordAnchor.Y == srSelectionRect.Bottom && coordAnchor.X == srSelectionRect.Left) + { + fRemoveRegion = true; + } + + // now check the first row's left based on removal + if (!fRemoveRegion) + { + VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Left); + } + else + { + VERIFY_ARE_EQUAL(psrFirst->Left, srSelectionRect.Right); + } + + // and the last row's right based on removal + if (!fRemoveRegion) + { + VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Right); + } + else + { + VERIFY_ARE_EQUAL(psrLast->Right, srSelectionRect.Left); + } + } + } + } + + TEST_METHOD(TestGetSelectionRects_LineMode) + { + m_pSelection->_fSelectionVisible = true; + + // Part I: Multiple line selection + // set selection region + m_pSelection->_srSelectionRect.Top = 0; + m_pSelection->_srSelectionRect.Bottom = 3; + m_pSelection->_srSelectionRect.Left = 1; + m_pSelection->_srSelectionRect.Right = 10; + + // #1 top-left to bottom right selection first + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + + // A. true/false for the selection modes should mean line selection + m_pSelection->_fLineSelection = true; + m_pSelection->_fUseAlternateSelection = false; + + VerifyGetSelectionRects_LineMode(); + + // B. false/true for the selection modes should also mean line selection + m_pSelection->_fLineSelection = false; + m_pSelection->_fUseAlternateSelection = true; + + VerifyGetSelectionRects_LineMode(); + + // now try the other 3 configurations of box region. + // #2 top-right to bottom-left selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + + VerifyGetSelectionRects_LineMode(); + + // #3 bottom-left to top-right selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + + VerifyGetSelectionRects_LineMode(); + + // #4 bottom-right to top-left selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + + VerifyGetSelectionRects_LineMode(); + + // Part II: Single line selection + m_pSelection->_srSelectionRect.Top = 2; + m_pSelection->_srSelectionRect.Bottom = 2; + m_pSelection->_srSelectionRect.Left = 1; + m_pSelection->_srSelectionRect.Right = 10; + + // #1: left to right selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Left; + VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top); + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Bottom; + + VerifyGetSelectionRects_LineMode(); + + // #2: right to left selection + m_pSelection->_coordSelectionAnchor.X = m_pSelection->_srSelectionRect.Right; + VERIFY_IS_TRUE(m_pSelection->_srSelectionRect.Bottom == m_pSelection->_srSelectionRect.Top); + m_pSelection->_coordSelectionAnchor.Y = m_pSelection->_srSelectionRect.Top; + + VerifyGetSelectionRects_LineMode(); + } + + void TestBisectSelectionDelta(SHORT sTargetX, SHORT sTargetY, SHORT sLength, SHORT sDeltaLeft, SHORT sDeltaRight) + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + short sStringLength; + COORD coordTargetPoint; + SMALL_RECT srSelection; + SMALL_RECT srOriginal; + + sStringLength = sLength; + coordTargetPoint.X = sTargetX; + coordTargetPoint.Y = sTargetY; + + // selection area is always one row at a time so top/bottom = Y = row position + srSelection.Top = srSelection.Bottom = coordTargetPoint.Y; + + // selection rectangle starts from the target and goes for the length requested + srSelection.Left = coordTargetPoint.X; + srSelection.Right = coordTargetPoint.X + sStringLength - 1; + + // save original for comparison + srOriginal.Top = srSelection.Top; + srOriginal.Bottom = srSelection.Bottom; + srOriginal.Left = srSelection.Left; + srOriginal.Right = srSelection.Right; + + srSelection = Selection::s_BisectSelection(sStringLength, coordTargetPoint, screenInfo, srSelection); + + VERIFY_ARE_EQUAL(srOriginal.Top, srSelection.Top); + VERIFY_ARE_EQUAL(srOriginal.Bottom, srSelection.Bottom); + VERIFY_ARE_EQUAL(srOriginal.Left + sDeltaLeft, srSelection.Left); + VERIFY_ARE_EQUAL(srOriginal.Right + sDeltaRight, srSelection.Right); + } + + TEST_METHOD(TestBisectSelection) + { + m_state->FillTextBufferBisect(); + + // From CommonState, this is what rows look like: + // positions of き are at 0, 27-28, 39-40, 67-68, 79 + // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き + // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き + // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き + // きABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789ききABCDEFGHIJKLMNOPQRSTUVWXYZきき0123456789き + + // 1a. Start position is trailing half and is at beginning of row + + // start from position Column 0, Row 2 + // selection is 5 characters long + // the left edge should move one to the right (+1) to not select the trailing byte + // right edge shouldn't move + TestBisectSelectionDelta(0, 2, 5, 1, 0); + + // 1b. Start position is trailing half and is elsewhere in the row + + // start from position Column 28, Row 2, which is the position of a trailing き in the mid row + // selection is 5 characters long + // the left edge should move one to the left (-1) to select the leading byte also + // right edge shouldn't move + TestBisectSelectionDelta(28, 2, 5, -1, 0); + + // 1c. Start position is trailing half and is beginning of buffer + + // start from position Column 0, Row 0 which is a trailing byte + // selection is 5 characters long + // the left edge should move one to the right (+1) to not select the trailing byte + // right edge shouldn't move + TestBisectSelectionDelta(0, 0, 5, 1, 0); + + // 2a. End position is leading half and is at end of row + + // start from position 10 before end of row (80 length row) + // row is 2 + // selection is 10 characters long + // the left edge shouldn't move + // the right edge should move one to the left (-1) to not select the leading byte + TestBisectSelectionDelta(70, 2, 10, 0, -1); + + // 2b. End position is leading half and is elsewhere in the row + + // start from 10 before trailing き in the mid row (pos 68 - 10 = 58) + // row is 2 + // selection is 10 characters long + // the left edge shouldn't move + // the right edge should move one to the right (+1) to add the trailing byte to the selection + TestBisectSelectionDelta(58, 2, 10, 0, 1); + + // 2c. End position is leading half and is at end of buffer + // start from position 10 before end of row (80 length row) + // row is 300 (or 299 for the index) + // selection is 10 characters long + // the left edge shouldn't move + // the right edge shouldn't move + TestBisectSelectionDelta(70, 299, 10, 0, 0); + } +}; + +class SelectionInputTests +{ + TEST_CLASS(SelectionInputTests); + + CommonState* m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + TEST_METHOD(TestGetInputLineBoundaries) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // 80x80 box + const SHORT sRowWidth = 80; + + SMALL_RECT srectEdges; + srectEdges.Left = srectEdges.Top = 0; + srectEdges.Right = srectEdges.Bottom = sRowWidth - 1; + + // false when no cooked read data exists + VERIFY_IS_FALSE(gci.HasPendingCookedRead()); + + bool fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); + VERIFY_IS_FALSE(fResult); + + // prepare some read data + m_state->PrepareReadHandle(); + auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); }); + + m_state->PrepareCookedReadData(); + // set up to clean up read data later + auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); }); + + COOKED_READ_DATA& readData = gci.CookedReadData(); + + // backup text info position over remainder of text execution duration + TextBuffer& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); + COORD coordOldTextInfoPos; + coordOldTextInfoPos.X = textBuffer.GetCursor().GetPosition().X; + coordOldTextInfoPos.Y = textBuffer.GetCursor().GetPosition().Y; + + // set various cursor positions + readData.OriginalCursorPosition().X = 15; + readData.OriginalCursorPosition().Y = 3; + + readData.VisibleCharCount() = 200; + + textBuffer.GetCursor().SetXPosition(35); + textBuffer.GetCursor().SetYPosition(35); + + // try getting boundaries with no pointers. parameters should be fully optional. + fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); + VERIFY_IS_TRUE(fResult); + + // now let's get some actual data + COORD coordStart; + COORD coordEnd; + + fResult = Selection::s_GetInputLineBoundaries(&coordStart, &coordEnd); + VERIFY_IS_TRUE(fResult); + + // starting position/boundary should always be where the input line started + VERIFY_ARE_EQUAL(coordStart.X, readData.OriginalCursorPosition().X); + VERIFY_ARE_EQUAL(coordStart.Y, readData.OriginalCursorPosition().Y); + + // ending position can vary. it's in one of two spots + // 1. If the original cooked cursor was valid (which it was this first time), it's NumberOfVisibleChars ahead. + COORD coordFinalPos; + + const short cCharsToAdjust = ((short)readData.VisibleCharCount() - 1); // then -1 to be on the last piece of text, not past it + + coordFinalPos.X = (readData.OriginalCursorPosition().X + cCharsToAdjust) % sRowWidth; + coordFinalPos.Y = readData.OriginalCursorPosition().Y + ((readData.OriginalCursorPosition().X + cCharsToAdjust) / sRowWidth); + + VERIFY_ARE_EQUAL(coordEnd.X, coordFinalPos.X); + VERIFY_ARE_EQUAL(coordEnd.Y, coordFinalPos.Y); + + // 2. if the original cooked cursor is invalid, then it's the text info cursor position + readData.OriginalCursorPosition().X = -1; + readData.OriginalCursorPosition().Y = -1; + + fResult = Selection::s_GetInputLineBoundaries(nullptr, &coordEnd); + VERIFY_IS_TRUE(fResult); + + VERIFY_ARE_EQUAL(coordEnd.X, textBuffer.GetCursor().GetPosition().X - 1); // -1 to be on the last piece of text, not past it + VERIFY_ARE_EQUAL(coordEnd.Y, textBuffer.GetCursor().GetPosition().Y); + + // restore text buffer info position + textBuffer.GetCursor().SetXPosition(coordOldTextInfoPos.X); + textBuffer.GetCursor().SetYPosition(coordOldTextInfoPos.Y); + } + + TEST_METHOD(TestWordByWordPrevious) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD_PROPERTIES() + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + const std::wstring text(L"this is some test text."); + screenInfo.Write(OutputCellIterator(text)); + + // Get the left and right side of the text we inserted (right is one past the end) + const COORD left = { 0, 0 }; + const COORD right = { gsl::narrow(text.length()), 0 }; + + // Get the selection instance and buffer size + auto& sel = Selection::Instance(); + const auto bufferSize = screenInfo.GetBufferSize(); + + // The anchor is where the selection started from. + const auto anchor = right; + + // The point is the "other end" of the anchor forming the rectangle of what is covered. + // It starts at the same spot as the anchor to represent the initial 1x1 selection. + auto point = anchor; + + // Walk through the sequence in reverse extending the sequence by one word each time to the left. + // The anchor is always the end of the line and the selection just gets bigger. + do + { + // We expect the result to be left of where we started. + // It will point at the character just right of the space (or the beginning of the line). + COORD resultExpected = point; + + do + { + resultExpected.X--; + } while (resultExpected.X > 0 && text.at(resultExpected.X - 1) != UNICODE_SPACE); + + point = sel.WordByWordSelection(true, bufferSize, anchor, point); + + VERIFY_ARE_EQUAL(resultExpected, point); + + } while (point.X > left.X); + } + + TEST_METHOD(TestWordByWordNext) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method") + END_TEST_METHOD_PROPERTIES() + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); + + const std::wstring text(L"this is some test text."); + screenInfo.Write(OutputCellIterator(text)); + + // Get the left and right side of the text we inserted (right is one past the end) + const COORD left = { 0, 0 }; + const COORD right = { gsl::narrow(text.length()), 0 }; + + // Get the selection instance and buffer size + auto& sel = Selection::Instance(); + const auto bufferSize = screenInfo.GetBufferSize(); + + // The anchor is where the selection started from. + const auto anchor = left; + + // The point is the "other end" of the anchor forming the rectangle of what is covered. + // It starts at the same spot as the anchor to represent the initial 1x1 selection. + auto point = anchor; + + // Walk through the sequence forward extending the sequence by one word each time to the right. + // The anchor is always the end of the line and the selection just gets bigger. + do + { + // We expect the result to be right of where we started. + + COORD resultExpected = point; + + do + { + resultExpected.X++; + } while (resultExpected.X + 1 < right.X && text.at(resultExpected.X + 1) != UNICODE_SPACE); + resultExpected.X++; + + // when we reach the end, word by word selection will seek forward to the end of the buffer, so update + // the expected to the end in that circumstance + if (resultExpected.X >= right.X) + { + resultExpected.X = bufferSize.RightInclusive(); + resultExpected.Y = bufferSize.BottomInclusive(); + } + + point = sel.WordByWordSelection(false, bufferSize, anchor, point); + + VERIFY_ARE_EQUAL(resultExpected, point); + + } while (point.Y < bufferSize.BottomInclusive()); // stop once we've advanced to a point on the bottom row of the buffer. + } + +}; diff --git a/src/host/ut_host/TextBufferIteratorTests.cpp b/src/host/ut_host/TextBufferIteratorTests.cpp new file mode 100644 index 000000000..9251b9a33 --- /dev/null +++ b/src/host/ut_host/TextBufferIteratorTests.cpp @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" +#include "../buffer/out/textBuffer.hpp" +#include "../buffer/out/textBufferCellIterator.hpp" +#include "../buffer/out/textBufferTextIterator.hpp" +#include "../buffer/out/CharRow.hpp" + +#include "input.h" + +#include "../interactivity/inc/ServiceLocator.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class TextBufferIteratorTests +{ + CommonState* m_state; + + TEST_CLASS(TextBufferIteratorTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareNewTextBufferInfo(); + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupNewTextBufferInfo(); + + return true; + } + + template + void BoolOperatorTestHelper() + { + const auto it = GetIterator(); + VERIFY_IS_TRUE(it); + + const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); + const auto size = outputBuffer.GetBufferSize().Dimensions(); + T itInvalidPos(it); + itInvalidPos._exceeded = true; + VERIFY_IS_FALSE(itInvalidPos); + } + + TEST_METHOD(BoolOperatorText); + TEST_METHOD(BoolOperatorCell); + + template + void EqualsOperatorTestHelper() + { + const auto it = GetIterator(); + const auto it2 = GetIterator(); + + VERIFY_ARE_EQUAL(it, it2); + } + + TEST_METHOD(EqualsOperatorText); + TEST_METHOD(EqualsOperatorCell); + + template + void NotEqualsOperatorTestHelper() + { + const auto it = GetIterator(); + + COORD oneOff = it._pos; + oneOff.X++; + const auto it2 = GetIteratorAt(oneOff); + + VERIFY_ARE_NOT_EQUAL(it, it2); + } + + TEST_METHOD(NotEqualsOperatorText); + TEST_METHOD(NotEqualsOperatorCell); + + template + void PlusEqualsOperatorTestHelper() + { + auto it = GetIterator(); + + ptrdiff_t diffUnit = 3; + COORD expectedPos = it._pos; + expectedPos.X += gsl::narrow(diffUnit); + const auto itExpected = GetIteratorAt(expectedPos); + + it += diffUnit; + + VERIFY_ARE_EQUAL(itExpected, it); + } + + TEST_METHOD(PlusEqualsOperatorText); + TEST_METHOD(PlusEqualsOperatorCell); + + template + void MinusEqualsOperatorTestHelper() + { + auto itExpected = GetIteratorWithAdvance(); + + ptrdiff_t diffUnit = 3; + COORD pos = itExpected._pos; + pos.X += gsl::narrow(diffUnit); + auto itOffset = GetIteratorAt(pos); + + itOffset -= diffUnit; + + VERIFY_ARE_EQUAL(itExpected, itOffset); + } + + TEST_METHOD(MinusEqualsOperatorText); + TEST_METHOD(MinusEqualsOperatorCell); + + template + void PrefixPlusPlusOperatorTestHelper() + { + auto itActual = GetIterator(); + + COORD expectedPos = itActual._pos; + expectedPos.X++; + const auto itExpected = GetIteratorAt(expectedPos); + + ++itActual; + + VERIFY_ARE_EQUAL(itExpected, itActual); + } + + TEST_METHOD(PrefixPlusPlusOperatorText); + TEST_METHOD(PrefixPlusPlusOperatorCell); + + template + void PrefixMinusMinusOperatorTestHelper() + { + const auto itExpected = GetIteratorWithAdvance(); + + COORD pos = itExpected._pos; + pos.X++; + auto itActual = GetIteratorAt(pos); + + --itActual; + + VERIFY_ARE_EQUAL(itExpected, itActual); + } + + TEST_METHOD(PrefixMinusMinusOperatorText); + TEST_METHOD(PrefixMinusMinusOperatorCell); + + template + void PostfixPlusPlusOperatorTestHelper() + { + auto it = GetIterator(); + + COORD expectedPos = it._pos; + expectedPos.X++; + const auto itExpected = GetIteratorAt(expectedPos); + + ++it; + + VERIFY_ARE_EQUAL(itExpected, it); + } + + TEST_METHOD(PostfixPlusPlusOperatorText); + TEST_METHOD(PostfixPlusPlusOperatorCell); + + template + void PostfixMinusMinusOperatorTestHelper() + { + const auto itExpected = GetIteratorWithAdvance(); + + COORD pos = itExpected._pos; + pos.X++; + auto itActual = GetIteratorAt(pos); + + itActual--; + + VERIFY_ARE_EQUAL(itExpected, itActual); + } + + TEST_METHOD(PostfixMinusMinusOperatorText); + TEST_METHOD(PostfixMinusMinusOperatorCell); + + template + void PlusOperatorTestHelper() + { + auto it = GetIterator(); + + ptrdiff_t diffUnit = 3; + COORD expectedPos = it._pos; + expectedPos.X += gsl::narrow(diffUnit); + const auto itExpected = GetIteratorAt(expectedPos); + + const auto itActual = it + diffUnit; + + VERIFY_ARE_EQUAL(itExpected, itActual); + } + + TEST_METHOD(PlusOperatorText); + TEST_METHOD(PlusOperatorCell); + + template + void MinusOperatorTestHelper() + { + auto itExpected = GetIteratorWithAdvance(); + + ptrdiff_t diffUnit = 3; + COORD pos = itExpected._pos; + pos.X += gsl::narrow(diffUnit); + auto itOffset = GetIteratorAt(pos); + + const auto itActual = itOffset - diffUnit; + + VERIFY_ARE_EQUAL(itExpected, itActual); + } + + TEST_METHOD(MinusOperatorText); + TEST_METHOD(MinusOperatorCell); + + template + void DifferenceOperatorTestHelper() + { + const ptrdiff_t expected(3); + auto it = GetIterator(); + auto it2 = it + expected; + + const ptrdiff_t actual = it2 - it; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(DifferenceOperatorText); + TEST_METHOD(DifferenceOperatorCell); + + TEST_METHOD(AsCharInfoCell); + + TEST_METHOD(DereferenceOperatorText); + TEST_METHOD(DereferenceOperatorCell); + + TEST_METHOD(ConstructedNoLimit); + TEST_METHOD(ConstructedLimits); + +}; + +template +T GetIterator() {} + +template +T GetIteratorAt(COORD at) {} + +template +T GetIteratorWithAdvance() {} + +template<> +TextBufferCellIterator GetIteratorAt(COORD at) +{ + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + return outputBuffer.GetCellDataAt(at); +} + +template<> +TextBufferCellIterator GetIterator() +{ + return GetIteratorAt({ 0 }); +} + +template<> +TextBufferCellIterator GetIteratorWithAdvance() +{ + return GetIteratorAt({ 5, 5 }); +} + +template<> +TextBufferTextIterator GetIteratorAt(COORD at) +{ + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + return outputBuffer.GetTextDataAt(at); +} + +template<> +TextBufferTextIterator GetIterator() +{ + return GetIteratorAt({ 0 }); +} + +template<> +TextBufferTextIterator GetIteratorWithAdvance() +{ + return GetIteratorAt({ 5, 5 }); +} + +void TextBufferIteratorTests::BoolOperatorText() +{ + BoolOperatorTestHelper(); +} + +void TextBufferIteratorTests::BoolOperatorCell() +{ + BoolOperatorTestHelper(); + + Log::Comment(L"For cells, also check incrementing past the end."); + const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); + const auto size = outputBuffer.GetBufferSize().Dimensions(); + TextBufferCellIterator it(outputBuffer.GetTextBuffer(), { size.X-1, size.Y-1 }); + VERIFY_IS_TRUE(it); + it++; + VERIFY_IS_FALSE(it); +} + +void TextBufferIteratorTests::EqualsOperatorText() +{ + EqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::EqualsOperatorCell() +{ + EqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::NotEqualsOperatorText() +{ + NotEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::NotEqualsOperatorCell() +{ + NotEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::PlusEqualsOperatorText() +{ + PlusEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::PlusEqualsOperatorCell() +{ + PlusEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::MinusEqualsOperatorText() +{ + MinusEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::MinusEqualsOperatorCell() +{ + MinusEqualsOperatorTestHelper(); +} + +void TextBufferIteratorTests::PrefixPlusPlusOperatorText() +{ + PrefixPlusPlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PrefixPlusPlusOperatorCell() +{ + PrefixPlusPlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PrefixMinusMinusOperatorText() +{ + PrefixMinusMinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PrefixMinusMinusOperatorCell() +{ + PrefixMinusMinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PostfixPlusPlusOperatorText() +{ + PostfixPlusPlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PostfixPlusPlusOperatorCell() +{ + PostfixPlusPlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PostfixMinusMinusOperatorText() +{ + PostfixMinusMinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PostfixMinusMinusOperatorCell() +{ + PostfixMinusMinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PlusOperatorText() +{ + PlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::PlusOperatorCell() +{ + PlusOperatorTestHelper(); +} + +void TextBufferIteratorTests::MinusOperatorText() +{ + MinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::MinusOperatorCell() +{ + MinusOperatorTestHelper(); +} + +void TextBufferIteratorTests::DifferenceOperatorText() +{ + DifferenceOperatorTestHelper(); +} + +void TextBufferIteratorTests::DifferenceOperatorCell() +{ + DifferenceOperatorTestHelper(); +} + +void TextBufferIteratorTests::AsCharInfoCell() +{ + m_state->FillTextBuffer(); + const auto it = GetIterator(); + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + + const auto wcharExpected = *row.GetCharRow().GlyphAt(it._pos.X).begin(); + const auto attrExpected = row.GetAttrRow().GetAttrByColumn(it._pos.X); + + const auto cellActual = gci.AsCharInfo(*it); + const auto wcharActual = cellActual.Char.UnicodeChar; + const auto attrActual = it->TextAttr(); + + VERIFY_ARE_EQUAL(wcharExpected, wcharActual); + VERIFY_ARE_EQUAL(attrExpected, attrActual); +} + +void TextBufferIteratorTests::DereferenceOperatorText() +{ + m_state->FillTextBuffer(); + const auto it = GetIterator(); + + const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); + + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + + const auto wcharExpected = row.GetCharRow().GlyphAt(it._pos.X); + const auto wcharActual = *it; + + VERIFY_ARE_EQUAL(*wcharExpected.begin(), *wcharActual.begin()); +} + +void TextBufferIteratorTests::DereferenceOperatorCell() +{ + m_state->FillTextBuffer(); + const auto it = GetIterator(); + + + const auto& outputBuffer = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer(); + + const auto& row = outputBuffer._textBuffer->GetRowByOffset(it._pos.Y); + + const auto textExpected = (std::wstring_view)row.GetCharRow().GlyphAt(it._pos.X); + const auto dbcsExpected = row.GetCharRow().DbcsAttrAt(it._pos.X); + const auto attrExpected = row.GetAttrRow().GetAttrByColumn(it._pos.X).GetLegacyAttributes(); + + const auto cellActual = *it; + const auto textActual = cellActual.Chars(); + const auto dbcsActual = cellActual.DbcsAttr(); + const auto attrActual = cellActual.TextAttr(); + + VERIFY_ARE_EQUAL(String(textExpected.data(), (int)textExpected.size()), String(textActual.data(), (int)textActual.size())); + VERIFY_ARE_EQUAL(dbcsExpected, dbcsActual); + VERIFY_ARE_EQUAL(attrExpected, attrActual); +} + +void TextBufferIteratorTests::ConstructedNoLimit() +{ + m_state->FillTextBuffer(); + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + const auto& textBuffer = outputBuffer.GetTextBuffer(); + const auto& bufferSize = textBuffer.GetSize(); + + TextBufferCellIterator it(textBuffer, { 0 }); + + VERIFY_IS_TRUE(it, L"Iterator is valid."); + VERIFY_ARE_EQUAL(bufferSize, it._bounds, L"Bounds match the bounds of the text buffer."); + + const auto totalBufferDistance = bufferSize.Width() * bufferSize.Height(); + + // Advance buffer to one before the end. + it += (totalBufferDistance - 1); + VERIFY_IS_TRUE(it, L"Iterator is still valid."); + + // Advance over the end. + it++; + VERIFY_IS_FALSE(it, L"Iterator invalid now."); + + // Verify throws for out of range. + VERIFY_THROWS_SPECIFIC(TextBufferCellIterator(textBuffer, { -1, -1 }), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); +} + +void TextBufferIteratorTests::ConstructedLimits() +{ + m_state->FillTextBuffer(); + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& outputBuffer = gci.GetActiveOutputBuffer(); + const auto& textBuffer = outputBuffer.GetTextBuffer(); + + SMALL_RECT limits; + limits.Top = 1; + limits.Bottom = 1; + limits.Left = 3; + limits.Right = 5; + const auto viewport = Microsoft::Console::Types::Viewport::FromInclusive(limits); + + COORD pos; + pos.X = limits.Left; + pos.Y = limits.Top; + + TextBufferCellIterator it(textBuffer, pos, viewport); + + VERIFY_IS_TRUE(it, L"Iterator is valid."); + VERIFY_ARE_EQUAL(viewport, it._bounds, L"Bounds match the bounds given."); + + const auto totalBufferDistance = viewport.Width() * viewport.Height(); + + // Advance buffer to one before the end. + it += (totalBufferDistance - 1); + VERIFY_IS_TRUE(it, L"Iterator is still valid."); + + // Advance over the end. + it++; + VERIFY_IS_FALSE(it, L"Iterator invalid now."); + + // Verify throws for out of range. + VERIFY_THROWS_SPECIFIC(TextBufferCellIterator(textBuffer, + { 0 }, + viewport), + wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + + // Verify throws for limit not inside buffer + const auto bufferSize = textBuffer.GetSize(); + VERIFY_THROWS_SPECIFIC(TextBufferCellIterator(textBuffer, + pos, + Microsoft::Console::Types::Viewport::FromInclusive(bufferSize.ToExclusive())), + wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); + +} diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp new file mode 100644 index 000000000..a324bd4f7 --- /dev/null +++ b/src/host/ut_host/TextBufferTests.cpp @@ -0,0 +1,1966 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../inc/consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" +#include "../buffer/out/textBuffer.hpp" +#include "../buffer/out/CharRow.hpp" + +#include "input.h" +#include "_stream.h" + +#include "../interactivity/inc/ServiceLocator.hpp" +#include "../renderer/inc/DummyRenderTarget.hpp" + +using namespace Microsoft::Console::Types; +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class TextBufferTests +{ + DummyRenderTarget _renderTarget; + CommonState* m_state; + + TEST_CLASS(TextBufferTests); + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + m_state->PrepareNewTextBufferInfo(); + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + m_state->CleanupNewTextBufferInfo(); + + return true; + } + + TEST_METHOD(TestBufferCreate); + + TextBuffer& GetTbi(); + + SHORT GetBufferWidth(); + + SHORT GetBufferHeight(); + + TEST_METHOD(TestBufferRowByOffset); + + TEST_METHOD(TestWrapFlag); + + TEST_METHOD(TestDoubleBytePadFlag); + + void DoBoundaryTest(PWCHAR const pwszInputString, + short const cLength, + short const cMax, + short const cLeft, + short const cRight); + + TEST_METHOD(TestBoundaryMeasuresRegularString); + + TEST_METHOD(TestBoundaryMeasuresFloatingString); + + TEST_METHOD(TestCopyProperties); + + TEST_METHOD(TestInsertCharacter); + + TEST_METHOD(TestIncrementCursor); + + TEST_METHOD(TestNewlineCursor); + + void TestLastNonSpace(short const cursorPosY); + + TEST_METHOD(TestGetLastNonSpaceCharacter); + + TEST_METHOD(TestSetWrapOnCurrentRow); + + TEST_METHOD(TestIncrementCircularBuffer); + + TEST_METHOD(TestMixedRgbAndLegacyForeground); + TEST_METHOD(TestMixedRgbAndLegacyBackground); + TEST_METHOD(TestMixedRgbAndLegacyUnderline); + TEST_METHOD(TestMixedRgbAndLegacyBrightness); + + TEST_METHOD(TestRgbEraseLine); + + TEST_METHOD(TestUnBold); + TEST_METHOD(TestUnBoldRgb); + TEST_METHOD(TestComplexUnBold); + + TEST_METHOD(CopyAttrs); + + TEST_METHOD(EmptySgrTest); + + TEST_METHOD(TestReverseReset); + + TEST_METHOD(CopyLastAttr); + + TEST_METHOD(TestRgbThenBold); + TEST_METHOD(TestResetClearsBoldness); + + TEST_METHOD(TestBackspaceRightSideVt); + + TEST_METHOD(TestBackspaceStrings); + TEST_METHOD(TestBackspaceStringsAPI); + + TEST_METHOD(TestRepeatCharacter); + + TEST_METHOD(ResizeTraditional); + + TEST_METHOD(ResizeTraditionalRotationPreservesHighUnicode); + TEST_METHOD(ScrollBufferRotationPreservesHighUnicode); + + TEST_METHOD(ResizeTraditionalHighUnicodeRowRemoval); + TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval); + + TEST_METHOD(TestBurrito); + +}; + +void TextBufferTests::TestBufferCreate() +{ + VERIFY_SUCCESS_NTSTATUS(m_state->GetTextBufferInfoInitResult()); +} + +TextBuffer& TextBufferTests::GetTbi() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer().GetTextBuffer(); +} + +SHORT TextBufferTests::GetBufferWidth() +{ + return GetTbi().GetSize().Width(); +} + +SHORT TextBufferTests::GetBufferHeight() +{ + return GetTbi().GetSize().Height(); +} + +void TextBufferTests::TestBufferRowByOffset() +{ + TextBuffer& textBuffer = GetTbi(); + SHORT csBufferHeight = GetBufferHeight(); + + VERIFY_IS_TRUE(csBufferHeight > 20); + + short sId = csBufferHeight / 2 - 5; + + const ROW& row = textBuffer.GetRowByOffset(sId); + VERIFY_ARE_EQUAL(row.GetId(), sId); +} + +void TextBufferTests::TestWrapFlag() +{ + TextBuffer& textBuffer = GetTbi(); + + ROW& Row = textBuffer._GetFirstRow(); + + // no wrap by default + VERIFY_IS_FALSE(Row.GetCharRow().WasWrapForced()); + + // try set wrap and check + Row.GetCharRow().SetWrapForced(true); + VERIFY_IS_TRUE(Row.GetCharRow().WasWrapForced()); + + // try unset wrap and check + Row.GetCharRow().SetWrapForced(false); + VERIFY_IS_FALSE(Row.GetCharRow().WasWrapForced()); +} + +void TextBufferTests::TestDoubleBytePadFlag() +{ + TextBuffer& textBuffer = GetTbi(); + + ROW& Row = textBuffer._GetFirstRow(); + + // no padding by default + VERIFY_IS_FALSE(Row.GetCharRow().WasDoubleBytePadded()); + + // try set and check + Row.GetCharRow().SetDoubleBytePadded(true); + VERIFY_IS_TRUE(Row.GetCharRow().WasDoubleBytePadded()); + + // try unset and check + Row.GetCharRow().SetDoubleBytePadded(false); + VERIFY_IS_FALSE(Row.GetCharRow().WasDoubleBytePadded()); +} + + +void TextBufferTests::DoBoundaryTest(PWCHAR const pwszInputString, + short const cLength, + short const cMax, + short const cLeft, + short const cRight) +{ + TextBuffer& textBuffer = GetTbi(); + + CharRow& charRow = textBuffer._GetFirstRow().GetCharRow(); + + // copy string into buffer + for (size_t i = 0; i < static_cast(cLength); ++i) + { + charRow.GlyphAt(i) = { &pwszInputString[i], 1 }; + } + + // space pad the rest of the string + if (cLength < cMax) + { + for (short cStart = cLength; cStart < cMax; cStart++) + { + charRow.ClearGlyph(cStart); + } + } + + // left edge should be 0 since there are no leading spaces + VERIFY_ARE_EQUAL(charRow.MeasureLeft(), static_cast(cLeft)); + // right edge should be one past the index of the last character or the string length + VERIFY_ARE_EQUAL(charRow.MeasureRight(), static_cast(cRight)); +} + +void TextBufferTests::TestBoundaryMeasuresRegularString() +{ + SHORT csBufferWidth = GetBufferWidth(); + + // length 44, left 0, right 44 + const PWCHAR pwszLazyDog = L"The quick brown fox jumps over the lazy dog."; + DoBoundaryTest(pwszLazyDog, 44, csBufferWidth, 0, 44); +} + +void TextBufferTests::TestBoundaryMeasuresFloatingString() +{ + SHORT csBufferWidth = GetBufferWidth(); + + // length 5 spaces + 4 chars + 5 spaces = 14, left 5, right 9 + const PWCHAR pwszOffsets = L" C:\\> "; + DoBoundaryTest(pwszOffsets, 14, csBufferWidth, 5, 9); +} + +void TextBufferTests::TestCopyProperties() +{ + TextBuffer& otherTbi = GetTbi(); + + std::unique_ptr testTextBuffer = std::make_unique(otherTbi.GetSize().Dimensions(), + otherTbi._currentAttributes, + 12, + otherTbi._renderTarget); + VERIFY_IS_NOT_NULL(testTextBuffer.get()); + + // set initial mapping values + testTextBuffer->GetCursor().SetHasMoved(false); + otherTbi.GetCursor().SetHasMoved(true); + + testTextBuffer->GetCursor().SetIsVisible(false); + otherTbi.GetCursor().SetIsVisible(true); + + testTextBuffer->GetCursor().SetIsOn(false); + otherTbi.GetCursor().SetIsOn(true); + + testTextBuffer->GetCursor().SetIsDouble(false); + otherTbi.GetCursor().SetIsDouble(true); + + testTextBuffer->GetCursor().SetDelay(false); + otherTbi.GetCursor().SetDelay(true); + + // run copy + testTextBuffer->CopyProperties(otherTbi); + + // test that new now contains values from other + VERIFY_IS_TRUE(testTextBuffer->GetCursor().HasMoved()); + VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsVisible()); + VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsOn()); + VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsDouble()); + VERIFY_IS_TRUE(testTextBuffer->GetCursor().GetDelay()); +} + +void TextBufferTests::TestInsertCharacter() +{ + TextBuffer& textBuffer = GetTbi(); + + // get starting cursor position + COORD const coordCursorBefore = textBuffer.GetCursor().GetPosition(); + + // Get current row from the buffer + ROW& Row = textBuffer.GetRowByOffset(coordCursorBefore.Y); + + // create some sample test data + const auto wch = L'Z'; + const std::wstring_view wchTest(&wch, 1); + DbcsAttribute dbcsAttribute; + dbcsAttribute.SetTrailing(); + WORD const wAttrTest = BACKGROUND_INTENSITY | FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; + TextAttribute TestAttributes = TextAttribute(wAttrTest); + + CharRow& charRow = Row.GetCharRow(); + charRow.DbcsAttrAt(coordCursorBefore.X).SetLeading(); + // ensure that the buffer didn't start with these fields + VERIFY_ARE_NOT_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); + VERIFY_ARE_NOT_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); + + auto attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); + + VERIFY_ARE_NOT_EQUAL(attr, TestAttributes); + + // now apply the new data to the buffer + textBuffer.InsertCharacter(wchTest, dbcsAttribute, TestAttributes); + + // ensure that the buffer position where the cursor WAS contains the test items + VERIFY_ARE_EQUAL(charRow.GlyphAt(coordCursorBefore.X), wchTest); + VERIFY_ARE_EQUAL(charRow.DbcsAttrAt(coordCursorBefore.X), dbcsAttribute); + + attr = Row.GetAttrRow().GetAttrByColumn(coordCursorBefore.X); + VERIFY_ARE_EQUAL(attr, TestAttributes); + + // ensure that the cursor moved to a new position (X or Y or both have changed) + VERIFY_IS_TRUE((coordCursorBefore.X != textBuffer.GetCursor().GetPosition().X) || + (coordCursorBefore.Y != textBuffer.GetCursor().GetPosition().Y)); + // the proper advancement of the cursor (e.g. which position it goes to) is validated in other tests +} + +void TextBufferTests::TestIncrementCursor() +{ + TextBuffer& textBuffer = GetTbi(); + + // only checking X increments here + // Y increments are covered in the NewlineCursor test + + short const sBufferWidth = textBuffer.GetSize().Width(); + + short const sBufferHeight = textBuffer.GetSize().Height(); + VERIFY_IS_TRUE(sBufferWidth > 1 && sBufferHeight > 1); + + Log::Comment(L"Test normal case of moving once to the right within a single line"); + textBuffer.GetCursor().SetXPosition(0); + textBuffer.GetCursor().SetYPosition(0); + + COORD coordCursorBefore = textBuffer.GetCursor().GetPosition(); + + textBuffer.IncrementCursor(); + + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 1); // X should advance by 1 + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // Y shouldn't have moved + + Log::Comment(L"Test line wrap case where cursor is on the right edge of the line"); + textBuffer.GetCursor().SetXPosition(sBufferWidth - 1); + textBuffer.GetCursor().SetYPosition(0); + + coordCursorBefore = textBuffer.GetCursor().GetPosition(); + + textBuffer.IncrementCursor(); + + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // position should be reset to the left edge when passing right edge + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y - 1, coordCursorBefore.Y); // the cursor should be moved one row down from where it used to be +} + +void TextBufferTests::TestNewlineCursor() +{ + TextBuffer& textBuffer = GetTbi(); + + + const short sBufferHeight = textBuffer.GetSize().Height(); + + const short sBufferWidth = textBuffer.GetSize().Width(); + // width and height are sufficiently large for upcoming math + VERIFY_IS_TRUE(sBufferWidth > 4 && sBufferHeight > 4); + + Log::Comment(L"Verify standard row increment from somewhere in the buffer"); + + // set cursor X position to non zero, any position in buffer + textBuffer.GetCursor().SetXPosition(3); + + // set cursor Y position to not-the-final row in the buffer + textBuffer.GetCursor().SetYPosition(3); + + COORD coordCursorBefore = textBuffer.GetCursor().GetPosition(); + + // perform operation + textBuffer.NewlineCursor(); + + // verify + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge of buffer + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y + 1); // move down one row + + Log::Comment(L"Verify increment when already on last row of buffer"); + + // X position still doesn't matter + textBuffer.GetCursor().SetXPosition(3); + + // Y position needs to be on the last row of the buffer + textBuffer.GetCursor().SetYPosition(sBufferHeight - 1); + + coordCursorBefore = textBuffer.GetCursor().GetPosition(); + + // perform operation + textBuffer.NewlineCursor(); + + // verify + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().X, 0); // move to left edge + VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().Y, coordCursorBefore.Y); // cursor Y position should not have moved. stays on same logical final line of buffer + + // This is okay because the backing circular buffer changes, not the logical screen position (final visible line of the buffer) +} + +void TextBufferTests::TestLastNonSpace(short const cursorPosY) +{ + TextBuffer& textBuffer = GetTbi(); + textBuffer.GetCursor().SetYPosition(cursorPosY); + + COORD coordLastNonSpace = textBuffer.GetLastNonSpaceCharacter(); + + // We expect the last non space character to be the last printable character in the row. + // The .Right property on a row is 1 past the last printable character in the row. + // If there is one character in the row, the last character would be 0. + // If there are no characters in the row, the last character would be -1 and we need to seek backwards to find the previous row with a character. + + // start expected position from cursor + COORD coordExpected = textBuffer.GetCursor().GetPosition(); + + // Try to get the X position from the current cursor position. + coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; + + // If we went negative, this row was empty and we need to continue seeking upward... + // - As long as X is negative (empty rows) + // - As long as we have space before the top of the buffer (Y isn't the 0th/top row). + while (coordExpected.X < 0 && coordExpected.Y > 0) + { + coordExpected.Y--; + coordExpected.X = static_cast(textBuffer.GetRowByOffset(coordExpected.Y).GetCharRow().MeasureRight()) - 1; + } + + VERIFY_ARE_EQUAL(coordLastNonSpace.X, coordExpected.X); + VERIFY_ARE_EQUAL(coordLastNonSpace.Y, coordExpected.Y); +} + +void TextBufferTests::TestGetLastNonSpaceCharacter() +{ + m_state->FillTextBuffer(); // fill buffer with some text, it should be 4 rows. See CommonState for details + + Log::Comment(L"Test with cursor inside last row of text"); + TestLastNonSpace(3); + + Log::Comment(L"Test with cursor one beyond last row of text"); + TestLastNonSpace(4); + + Log::Comment(L"Test with cursor way beyond last row of text"); + TestLastNonSpace(14); +} + +void TextBufferTests::TestSetWrapOnCurrentRow() +{ + TextBuffer& textBuffer = GetTbi(); + + short sCurrentRow = textBuffer.GetCursor().GetPosition().Y; + + ROW& Row = textBuffer.GetRowByOffset(sCurrentRow); + + Log::Comment(L"Testing off to on"); + + // turn wrap status off first + Row.GetCharRow().SetWrapForced(false); + + // trigger wrap + textBuffer._SetWrapOnCurrentRow(); + + // ensure this row was flipped + VERIFY_IS_TRUE(Row.GetCharRow().WasWrapForced()); + + Log::Comment(L"Testing on stays on"); + + // make sure wrap status is on + Row.GetCharRow().SetWrapForced(true); + + // trigger wrap + textBuffer._SetWrapOnCurrentRow(); + + // ensure row is still on + VERIFY_IS_TRUE(Row.GetCharRow().WasWrapForced()); +} + +void TextBufferTests::TestIncrementCircularBuffer() +{ + TextBuffer& textBuffer = GetTbi(); + + short const sBufferHeight = textBuffer.GetSize().Height(); + + VERIFY_IS_TRUE(sBufferHeight > 4); // buffer should be sufficiently large + + Log::Comment(L"Test 1 = FirstRow of circular buffer is not the final row of the buffer"); + Log::Comment(L"Test 2 = FirstRow of circular buffer IS THE FINAL ROW of the buffer (and therefore circles)"); + short rgRowsToTest[] = { 2, sBufferHeight - 1 }; + + for (UINT iTestIndex = 0; iTestIndex < ARRAYSIZE(rgRowsToTest); iTestIndex++) + { + const short iRowToTestIndex = rgRowsToTest[iTestIndex]; + + short iNextRowIndex = iRowToTestIndex + 1; + // if we're at or crossing the height, loop back to 0 (circular buffer) + if (iNextRowIndex >= sBufferHeight) + { + iNextRowIndex = 0; + } + + textBuffer._firstRow = iRowToTestIndex; + + // fill first row with some stuff + ROW& FirstRow = textBuffer._GetFirstRow(); + CharRow& charRow = FirstRow.GetCharRow(); + const auto stuff = L'A'; + charRow.GlyphAt(0) = { &stuff, 1 }; + + // ensure it does say that it contains text + VERIFY_IS_TRUE(FirstRow.GetCharRow().ContainsText()); + + // try increment + textBuffer.IncrementCircularBuffer(); + + // 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 + + // ensure old first row has been emptied + VERIFY_IS_FALSE(FirstRow.GetCharRow().ContainsText()); + } +} + +void TextBufferTests::TestMixedRgbAndLegacyForeground() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + // Case 1 - + // Write '\E[m\E[38;2;64;128;255mX\E[49mX\E[m' + // Make sure that the second X has RGB attributes (FG and BG) + // FG = rgb(64;128;255), BG = rgb(default) + Log::Comment(L"Case 1 \"\\E[m\\E[38;2;64;128;255mX\\E[49mX\\E[m\""); + + wchar_t* sequence = L"\x1b[m\x1b[38;2;64;128;255mX\x1b[49mX\x1b[m"; + + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const short x = cursor.GetPosition().X; + const short y = cursor.GetPosition().Y; + const ROW& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), gci.LookupBackgroundColor(si.GetAttributes())); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), gci.LookupBackgroundColor(si.GetAttributes())); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); + +} + +void TextBufferTests::TestMixedRgbAndLegacyBackground() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + // Case 2 - + // \E[m\E[48;2;64;128;255mX\E[39mX\E[m + // Make sure that the second X has RGB attributes (FG and BG) + // FG = rgb(default), BG = rgb(64;128;255) + Log::Comment(L"Case 2 \"\\E[m\\E[48;2;64;128;255mX\\E[39mX\\E[m\""); + + wchar_t* sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[39mX\x1b[m"; + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), gci.LookupForegroundColor(si.GetAttributes())); + + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), gci.LookupForegroundColor(si.GetAttributes())); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); +} + +void TextBufferTests::TestMixedRgbAndLegacyUnderline() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + // Case 3 - + // '\E[m\E[48;2;64;128;255mX\E[4mX\E[m' + // Make sure that the second X has RGB attributes AND underline + Log::Comment(L"Case 3 \"\\E[m\\E[48;2;64;128;255mX\\E[4mX\\E[m\""); + wchar_t* sequence = L"\x1b[m\x1b[48;2;64;128;255mX\x1b[4mX\x1b[m"; + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), gci.LookupForegroundColor(si.GetAttributes())); + + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), RGB(64, 128, 255)); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), gci.LookupForegroundColor(si.GetAttributes())); + + VERIFY_ARE_EQUAL(attrA.GetLegacyAttributes()&COMMON_LVB_UNDERSCORE, 0); + VERIFY_ARE_EQUAL(attrB.GetLegacyAttributes()&COMMON_LVB_UNDERSCORE, COMMON_LVB_UNDERSCORE); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); + +} + +void TextBufferTests::TestMixedRgbAndLegacyBrightness() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + // Case 4 - + // '\E[m\E[32mX\E[1mX' + // Make sure that the second X is a BRIGHT green, not white. + Log::Comment(L"Case 4 ;\"\\E[m\\E[32mX\\E[1mX\""); + const auto dark_green = gci.GetColorTableEntry(2); + const auto bright_green = gci.GetColorTableEntry(10); + VERIFY_ARE_NOT_EQUAL(dark_green, bright_green); + + wchar_t* sequence = L"\x1b[m\x1b[32mX\x1b[1mX"; + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), dark_green); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), bright_green); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); +} + +void TextBufferTests::TestRgbEraseLine() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + // Case 1 - + // Write '\E[m\E[48;2;64;128;255X\E[48;2;128;128;255\E[KX' + // Make sure that all the characters after the first have the rgb attrs + // BG = rgb(128;128;255) + { + std::wstring sequence = L"\x1b[m\x1b[48;2;64;128;255m"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + sequence = L"X"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + sequence = L"\x1b[48;2;128;128;255m"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + sequence = L"\x1b[K"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + sequence = L"X"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_ARE_EQUAL(x, 2); + VERIFY_ARE_EQUAL(y, 0); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + + const auto attr0 = attrs[0]; + + VERIFY_ARE_EQUAL(attr0.IsLegacy(), false); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr0), RGB(64, 128, 255)); + + for (auto i = 1; i < len; i++) + { + const auto attr = attrs[i]; + LOG_ATTR(attr); + VERIFY_ARE_EQUAL(attr.IsLegacy(), false); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr), RGB(128, 128, 255)); + } + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); + } +} + +void TextBufferTests::TestUnBold() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + // Case 1 - + // Write '\E[1;32mX\E[22mX' + // The first X should be bright green. + // The second x should be dark green. + std::wstring sequence = L"\x1b[1;32mX\x1b[22mX"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto dark_green = gci.GetColorTableEntry(2); + const auto bright_green = gci.GetColorTableEntry(10); + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_ARE_EQUAL(x, 2); + VERIFY_ARE_EQUAL(y, 0); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), bright_green); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), dark_green); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); +} + +void TextBufferTests::TestUnBoldRgb() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + // Case 2 - + // Write '\E[1;32m\E[48;2;1;2;3mX\E[22mX' + // The first X should be bright green, and not legacy. + // The second X should be dark green, and not legacy. + // BG = rgb(1;2;3) + std::wstring sequence = L"\x1b[1;32m\x1b[48;2;1;2;3mX\x1b[22mX"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto dark_green = gci.GetColorTableEntry(2); + const auto bright_green = gci.GetColorTableEntry(10); + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_ARE_EQUAL(x, 2); + VERIFY_ARE_EQUAL(y, 0); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), bright_green); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), dark_green); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); +} + +void TextBufferTests::TestComplexUnBold() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + // Case 3 - + // Write '\E[1;32m\E[48;2;1;2;3mA\E[22mB\E[38;2;32;32;32mC\E[1mD\E[38;2;64;64;64mE\E[22mF' + // The A should be bright green, and not legacy. + // The B should be dark green, and not legacy. + // The C should be rgb(32, 32, 32), and not legacy. + // The D should be unchanged from the third. + // The E should be rgb(64, 64, 64), and not legacy. + // The F should be rgb(64, 64, 64), and not legacy. + // BG = rgb(1;2;3) + std::wstring sequence = L"\x1b[1;32m\x1b[48;2;1;2;3mA\x1b[22mB\x1b[38;2;32;32;32mC\x1b[1mD\x1b[38;2;64;64;64mE\x1b[22mF"; + Log::Comment(NoThrowString().Format(sequence.c_str())); + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto dark_green = gci.GetColorTableEntry(2); + const auto bright_green = gci.GetColorTableEntry(10); + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_ARE_EQUAL(x, 6); + VERIFY_ARE_EQUAL(y, 0); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 6]; + const auto attrB = attrs[x - 5]; + const auto attrC = attrs[x - 4]; + const auto attrD = attrs[x - 3]; + const auto attrE = attrs[x - 2]; + const auto attrF = attrs[x - 1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + Log::Comment(NoThrowString().Format( + L"attrA=%s", VerifyOutputTraits::ToString(attrA).GetBuffer() + )); + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + LOG_ATTR(attrD); + LOG_ATTR(attrE); + LOG_ATTR(attrF); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrC.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrD.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrE.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrF.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), bright_green); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), RGB(1, 2, 3)); + VERIFY_IS_TRUE(attrA.IsBold()); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), dark_green); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), RGB(1, 2, 3)); + VERIFY_IS_FALSE(attrB.IsBold()); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrC), RGB(32, 32, 32)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrC), RGB(1, 2, 3)); + VERIFY_IS_FALSE(attrC.IsBold()); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrD), gci.LookupForegroundColor(attrC)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrD), gci.LookupBackgroundColor(attrC)); + VERIFY_IS_TRUE(attrD.IsBold()); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrE), RGB(64, 64, 64)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrE), RGB(1, 2, 3)); + VERIFY_IS_TRUE(attrE.IsBold()); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrF), RGB(64, 64, 64)); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrF), RGB(1, 2, 3)); + VERIFY_IS_FALSE(attrF.IsBold()); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); +} + + +void TextBufferTests::CopyAttrs() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + // Write '\E[32mX\E[33mX\n\E[34mX\E[35mX\E[H\E[M' + // The first two X's should get deleted. + // The third X should be blue + // The fourth X should be magenta + std::wstring sequence = L"\x1b[32mX\x1b[33mX\n\x1b[34mX\x1b[35mX\x1b[H\x1b[M"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto dark_blue = gci.GetColorTableEntry(1); + const auto dark_magenta = gci.GetColorTableEntry(5); + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_ARE_EQUAL(x, 0); + VERIFY_ARE_EQUAL(y, 0); + + const auto& row = tbi.GetRowByOffset(0); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[0]; + const auto attrB = attrs[1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), dark_blue); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), dark_magenta); + +} + +void TextBufferTests::EmptySgrTest() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); + const COLORREF defaultFg = gci.LookupForegroundColor(si.GetAttributes()); + const COLORREF defaultBg = gci.LookupBackgroundColor(si.GetAttributes()); + + // Case 1 - + // Write '\x1b[0mX\x1b[31mX\x1b[31;m' + // The first X should be default colors. + // The second X should be (darkRed,default). + // The third X should be default colors. + std::wstring sequence = L"\x1b[0mX\x1b[31mX\x1b[31;mX"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const COLORREF darkRed = gci.GetColorTableEntry(4); + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_IS_TRUE(x >= 3); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 3]; + const auto attrB = attrs[x - 2]; + const auto attrC = attrs[x - 1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), defaultFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), defaultBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), darkRed); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), defaultBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrC), defaultFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrC), defaultBg); + + stateMachine.ProcessString(&reset[0], reset.length()); +} + +void TextBufferTests::TestReverseReset() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); + const COLORREF defaultFg = gci.LookupForegroundColor(si.GetAttributes()); + const COLORREF defaultBg = gci.LookupBackgroundColor(si.GetAttributes()); + + // Case 1 - + // Write '\E[42m\E[38;2;128;5;255mX\E[7mX\E[27mX' + // The first X should be (fg,bg) = (rgb(128;5;255), dark_green) + // The second X should be (fg,bg) = (dark_green, rgb(128;5;255)) + // The third X should be (fg,bg) = (rgb(128;5;255), dark_green) + std::wstring sequence = L"\x1b[42m\x1b[38;2;128;5;255mX\x1b[7mX\x1b[27mX"; + stateMachine.ProcessString(&sequence[0], sequence.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto dark_green = gci.GetColorTableEntry(2); + const COLORREF rgbColor = RGB(128, 5, 255); + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + VERIFY_IS_TRUE(x >= 3); + + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const auto len = tbi.GetSize().Width(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 3]; + const auto attrB = attrs[x - 2]; + const auto attrC = attrs[x - 1]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), rgbColor); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), dark_green); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), dark_green); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), rgbColor); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrC), rgbColor); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrC), dark_green); + + stateMachine.ProcessString(&reset[0], reset.length()); +} + +void TextBufferTests::CopyLastAttr() +{ + DisableVerifyExceptions disable; + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + std::wstring reset = L"\x1b[0m"; + stateMachine.ProcessString(&reset[0], reset.length()); + const COLORREF defaultFg = gci.LookupForegroundColor(si.GetAttributes()); + const COLORREF defaultBg = gci.LookupBackgroundColor(si.GetAttributes()); + + const COLORREF solFg = RGB(101, 123, 131); + const COLORREF solBg = RGB(0, 43, 54); + const COLORREF solCyan = RGB(42, 161, 152); + + std::wstring solFgSeq = L"\x1b[38;2;101;123;131m"; + std::wstring solBgSeq = L"\x1b[48;2;0;43;54m"; + std::wstring solCyanSeq = L"\x1b[38;2;42;161;152m"; + + // Make sure that the color table has certain values we expect + const COLORREF defaultBrightBlack = RGB(118, 118, 118); + const COLORREF defaultBrightYellow = RGB(249, 241, 165); + const COLORREF defaultBrightCyan = RGB(97, 214, 214); + + gci.SetColorTableEntry(8, defaultBrightBlack); + gci.SetColorTableEntry(14, defaultBrightYellow); + gci.SetColorTableEntry(11, defaultBrightCyan); + + // Write (solFg, solBG) X \n + // (solFg, solBG) X (solCyan, solBG) X \n + // (solFg, solBG) X (solCyan, solBG) X (solFg, solBG) X + // then go home, and insert a line. + + // Row 1 + stateMachine.ProcessString(&solFgSeq[0], solFgSeq.length()); + stateMachine.ProcessString(&solBgSeq[0], solBgSeq.length()); + stateMachine.ProcessString(L"X", 1); + stateMachine.ProcessString(L"\n", 1); + + // Row 2 + // Remember that the colors from before persist here too, so we don't need + // to emit both the FG and BG if they haven't changed. + stateMachine.ProcessString(L"X", 1); + stateMachine.ProcessString(&solCyanSeq[0], solCyanSeq.length()); + stateMachine.ProcessString(L"X", 1); + stateMachine.ProcessString(L"\n", 1); + + // Row 3 + stateMachine.ProcessString(&solFgSeq[0], solFgSeq.length()); + stateMachine.ProcessString(&solBgSeq[0], solBgSeq.length()); + stateMachine.ProcessString(L"X", 1); + stateMachine.ProcessString(&solCyanSeq[0], solCyanSeq.length()); + stateMachine.ProcessString(L"X", 1); + stateMachine.ProcessString(&solFgSeq[0], solFgSeq.length()); + stateMachine.ProcessString(L"X", 1); + + std::wstring insertLineAtHome = L"\x1b[H\x1b[L"; + stateMachine.ProcessString(&insertLineAtHome[0], insertLineAtHome.length()); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + const ROW& row1 = tbi.GetRowByOffset(y + 1); + const ROW& row2 = tbi.GetRowByOffset(y + 2); + const ROW& row3 = tbi.GetRowByOffset(y + 3); + const auto len = tbi.GetSize().Width(); + + const std::vector attrs1{ row1.GetAttrRow().begin(), row1.GetAttrRow().end() }; + const std::vector attrs2{ row2.GetAttrRow().begin(), row2.GetAttrRow().end() }; + const std::vector attrs3{ row3.GetAttrRow().begin(), row3.GetAttrRow().end() }; + + const auto attr1A = attrs1[0]; + + const auto attr2A = attrs2[0]; + const auto attr2B = attrs2[1]; + + const auto attr3A = attrs3[0]; + const auto attr3B = attrs3[1]; + const auto attr3C = attrs3[2]; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + + LOG_ATTR(attr1A); + LOG_ATTR(attr2A); + LOG_ATTR(attr2A); + LOG_ATTR(attr3A); + LOG_ATTR(attr3B); + LOG_ATTR(attr3C); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr1A), solFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr1A), solBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr2A), solFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr2A), solBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr2B), solCyan); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr2B), solBg); + + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr3A), solFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr3A), solBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr3B), solCyan); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr3B), solBg); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attr3C), solFg); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attr3C), solBg); + + stateMachine.ProcessString(&reset[0], reset.length()); +} + +void TextBufferTests::TestRgbThenBold() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + // See MSFT:16398982 + Log::Comment(NoThrowString().Format( + L"Test that a bold following a RGB color doesn't remove the RGB color" + )); + Log::Comment(L"\"\\x1b[38;2;40;40;40m\\x1b[48;2;168;153;132mX\\x1b[1mX\\x1b[m\""); + const auto foreground = RGB(40, 40, 40); + const auto background = RGB(168, 153, 132); + + const wchar_t* const sequence = L"\x1b[38;2;40;40;40m\x1b[48;2;168;153;132mX\x1b[1mX\x1b[m"; + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x - 2]; + const auto attrB = attrs[x - 1]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + Log::Comment(NoThrowString().Format( + L"attrA should be RGB, and attrB should be the same as attrA, NOT bolded" + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + + VERIFY_ARE_EQUAL(attrA.IsLegacy(), false); + VERIFY_ARE_EQUAL(attrB.IsLegacy(), false); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), foreground); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrA), background); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), foreground); + VERIFY_ARE_EQUAL(gci.LookupBackgroundColor(attrB), background); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); +} + +void TextBufferTests::TestResetClearsBoldness() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + Log::Comment(NoThrowString().Format( + L"Test that resetting bold attributes clears the boldness." + )); + const auto x0 = cursor.GetPosition().X; + + // Test assumes that the background/foreground were default attribute when it starts up, + // so set that here. + TextAttribute defaultAttribute; + si.SetAttributes(defaultAttribute); + + const COLORREF defaultFg = gci.LookupForegroundColor(si.GetAttributes()); + const COLORREF defaultBg = gci.LookupBackgroundColor(si.GetAttributes()); + const auto dark_green = gci.GetColorTableEntry(2); + const auto bright_green = gci.GetColorTableEntry(10); + + wchar_t* sequence = L"\x1b[32mA\x1b[1mB\x1b[0mC\x1b[32mD"; + Log::Comment(NoThrowString().Format(sequence)); + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + + const auto x = cursor.GetPosition().X; + const auto y = cursor.GetPosition().Y; + const auto& row = tbi.GetRowByOffset(y); + const auto attrRow = &row.GetAttrRow(); + const std::vector attrs{ attrRow->begin(), attrRow->end() }; + const auto attrA = attrs[x0]; + const auto attrB = attrs[x0 + 1]; + const auto attrC = attrs[x0 + 2]; + const auto attrD = attrs[x0 + 3]; + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x, y + )); + Log::Comment(NoThrowString().Format( + L"attrA should be RGB, and attrB should be the same as attrA, NOT bolded" + )); + + LOG_ATTR(attrA); + LOG_ATTR(attrB); + LOG_ATTR(attrC); + LOG_ATTR(attrD); + + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrA), dark_green); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrB), bright_green); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrC), defaultFg); + VERIFY_ARE_EQUAL(gci.LookupForegroundColor(attrD), dark_green); + + VERIFY_IS_FALSE(attrA.IsBold()); + VERIFY_IS_TRUE(attrB.IsBold()); + VERIFY_IS_FALSE(attrC.IsBold()); + VERIFY_IS_FALSE(attrD.IsBold()); + + wchar_t* reset = L"\x1b[0m"; + stateMachine.ProcessString(reset, std::wcslen(reset)); +} + +void TextBufferTests::TestBackspaceRightSideVt() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + Log::Comment(L"verify that backspace has the same behavior as a vt CUB sequence once " + L"we've traversed to the right side of the current row"); + + const wchar_t* const sequence = L"\033[1000Cx\by\n"; + Log::Comment(NoThrowString().Format(sequence)); + + const auto preCursorPosition = cursor.GetPosition(); + stateMachine.ProcessString(sequence, std::wcslen(sequence)); + const auto postCursorPosition = cursor.GetPosition(); + + // make sure newline was handled correctly + VERIFY_ARE_EQUAL(0, postCursorPosition.X); + VERIFY_ARE_EQUAL(preCursorPosition.Y, postCursorPosition.Y - 1); + + // make sure "yx" was written to the end of the line the cursor started on + const auto& row = tbi.GetRowByOffset(preCursorPosition.Y); + const auto rowText = row.GetText(); + auto it = rowText.crbegin(); + VERIFY_ARE_EQUAL(*it, L'x'); + ++it; + VERIFY_ARE_EQUAL(*it, L'y'); +} + +void TextBufferTests::TestBackspaceStrings() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + const Cursor& cursor = tbi.GetCursor(); + + const auto x0 = cursor.GetPosition().X; + const auto y0 = cursor.GetPosition().Y; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x0, y0 + )); + std::wstring seq = L"a\b \b"; + stateMachine.ProcessString(seq.c_str(), seq.length()); + + const auto x1 = cursor.GetPosition().X; + const auto y1 = cursor.GetPosition().Y; + + VERIFY_ARE_EQUAL(x1, x0); + VERIFY_ARE_EQUAL(y1, y0); + + seq = L"a"; + stateMachine.ProcessString(seq.c_str(), seq.length()); + seq = L"\b"; + stateMachine.ProcessString(seq.c_str(), seq.length()); + seq = L" "; + stateMachine.ProcessString(seq.c_str(), seq.length()); + seq = L"\b"; + stateMachine.ProcessString(seq.c_str(), seq.length()); + + const auto x2 = cursor.GetPosition().X; + const auto y2 = cursor.GetPosition().Y; + + VERIFY_ARE_EQUAL(x2, x0); + VERIFY_ARE_EQUAL(y2, y0); +} + +void TextBufferTests::TestBackspaceStringsAPI() +{ + // Pretty much the same as the above test, but explicitly DOESNT use the + // state machine. + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + const TextBuffer& tbi = si.GetTextBuffer(); + const Cursor& cursor = tbi.GetCursor(); + + gci.SetVirtTermLevel(0); + WI_ClearFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + const auto x0 = cursor.GetPosition().X; + const auto y0 = cursor.GetPosition().Y; + + Log::Comment(NoThrowString().Format( + L"cursor={X:%d,Y:%d}", + x0, y0 + )); + + // We're going to write an "a" to the buffer in various ways, then try + // backspacing it with "\b \b". + // Regardless of how we write those sequences of characters, the end result + // should be the same. + std::unique_ptr waiter; + + size_t aCb = 2; + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, waiter)); + + size_t seqCb = 6; + Log::Comment(NoThrowString().Format( + L"Using WriteCharsLegacy, write \\b \\b as a single string." + )); + { + wchar_t* str = L"\b \b"; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); + + Log::Comment(NoThrowString().Format( + L"Using DoWriteConsole, write \\b \\b as a single string." + )); + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, waiter)); + + VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, waiter)); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); + } + + seqCb = 2; + + Log::Comment(NoThrowString().Format( + L"Using DoWriteConsole, write \\b \\b as seperate strings." + )); + + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, waiter)); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); + + + Log::Comment(NoThrowString().Format( + L"Using WriteCharsLegacy, write \\b \\b as seperate strings." + )); + { + wchar_t* str = L"a"; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); + } + { + wchar_t* str = L"\b"; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); + } + { + wchar_t* str = L" "; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); + } + { + wchar_t* str = L"\b"; + VERIFY_SUCCESS_NTSTATUS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().X, 0, nullptr)); + } + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, y0); + +} + +void TextBufferTests::TestRepeatCharacter() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& si = gci.GetActiveOutputBuffer().GetActiveBuffer(); + TextBuffer& tbi = si.GetTextBuffer(); + StateMachine& stateMachine = si.GetStateMachine(); + Cursor& cursor = tbi.GetCursor(); + + WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + cursor.SetXPosition(0); + cursor.SetYPosition(0); + + Log::Comment( + L"Test 0: Simply repeat a single character." + ); + + std::wstring sequence = L"X"; + stateMachine.ProcessString(sequence); + + sequence = L"\x1b[b"; + stateMachine.ProcessString(sequence); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + + { + const auto& row0 = tbi.GetRowByOffset(0); + const auto row0Text = row0.GetText(); + VERIFY_ARE_EQUAL(L'X', row0Text[0]); + VERIFY_ARE_EQUAL(L'X', row0Text[1]); + VERIFY_ARE_EQUAL(L' ', row0Text[2]); + } + + Log::Comment( + L"Test 1: Try repeating characters after another VT action. It should do nothing." + ); + + stateMachine.ProcessString(L"\n"); + stateMachine.ProcessString(L"A"); + stateMachine.ProcessString(L"B"); + stateMachine.ProcessString(L"\x1b[A"); + stateMachine.ProcessString(L"\x1b[b"); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 0); + + { + const auto& row0 = tbi.GetRowByOffset(0); + const auto& row1 = tbi.GetRowByOffset(1); + const auto row0Text = row0.GetText(); + const auto row1Text = row1.GetText(); + VERIFY_ARE_EQUAL(L'X', row0Text[0]); + VERIFY_ARE_EQUAL(L'X', row0Text[1]); + VERIFY_ARE_EQUAL(L' ', row0Text[2]); + VERIFY_ARE_EQUAL(L'A', row1Text[0]); + VERIFY_ARE_EQUAL(L'B', row1Text[1]); + VERIFY_ARE_EQUAL(L' ', row1Text[2]); + } + + Log::Comment( + L"Test 2: Repeat a character lots of times" + ); + + stateMachine.ProcessString(L"\x1b[3;H"); + stateMachine.ProcessString(L"C"); + stateMachine.ProcessString(L"\x1b[5b"); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 6); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 2); + + { + const auto& row2 = tbi.GetRowByOffset(2); + const auto row2Text = row2.GetText(); + VERIFY_ARE_EQUAL(L'C', row2Text[0]); + VERIFY_ARE_EQUAL(L'C', row2Text[1]); + VERIFY_ARE_EQUAL(L'C', row2Text[2]); + VERIFY_ARE_EQUAL(L'C', row2Text[3]); + VERIFY_ARE_EQUAL(L'C', row2Text[4]); + VERIFY_ARE_EQUAL(L'C', row2Text[5]); + VERIFY_ARE_EQUAL(L' ', row2Text[6]); + } + + Log::Comment( + L"Test 3: try repeating a non-graphical character. It should do nothing." + ); + + stateMachine.ProcessString(L"\r\n"); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 3); + stateMachine.ProcessString(L"D\n"); + stateMachine.ProcessString(L"\x1b[b"); + + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 4); + + + Log::Comment( + L"Test 4: try repeating multiple times. It should do nothing." + ); + + stateMachine.ProcessString(L"\r\n"); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 0); + VERIFY_ARE_EQUAL(cursor.GetPosition().Y, 5); + stateMachine.ProcessString(L"E"); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 1); + stateMachine.ProcessString(L"\x1b[b"); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + stateMachine.ProcessString(L"\x1b[b"); + VERIFY_ARE_EQUAL(cursor.GetPosition().X, 2); + + { + const auto& row5 = tbi.GetRowByOffset(5); + const auto row5Text = row5.GetText(); + VERIFY_ARE_EQUAL(L'E', row5Text[0]); + VERIFY_ARE_EQUAL(L'E', row5Text[1]); + VERIFY_ARE_EQUAL(L' ', row5Text[2]); + } +} + +void TextBufferTests::ResizeTraditional() +{ + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:shrinkX", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:shrinkY", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + + bool shrinkX; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"shrinkX", shrinkX), L"Shrink X = true, Grow X = false"); + + bool shrinkY; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"shrinkY", shrinkY), L"Shrink Y = true, Grow Y = false"); + + const COORD smallSize = { 5, 5 }; + TextAttribute defaultAttr; + defaultAttr.SetFromLegacy(0); + + TextBuffer buffer(smallSize, defaultAttr, 12, _renderTarget); + + Log::Comment(L"Fill buffer with some data and do assorted resize operations."); + + wchar_t expectedChar = L'A'; + const std::wstring_view expectedView(&expectedChar, 1); + TextAttribute expectedAttr(FOREGROUND_RED); + OutputCellIterator it(expectedChar, expectedAttr); + const auto finalIt = buffer.Write(it); + VERIFY_ARE_EQUAL(smallSize.X * smallSize.Y, finalIt.GetCellDistance(it), L"Verify we said we filled every cell."); + + const Viewport writtenView = Viewport::FromDimensions({ 0, 0 }, smallSize); + + Log::Comment(L"Ensure every cell has our test pattern value."); + { + TextBufferCellIterator viewIt(buffer, { 0, 0 }); + while (viewIt) + { + VERIFY_ARE_EQUAL(expectedView, viewIt->Chars()); + VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); + viewIt++; + } + } + + Log::Comment(L"Resize to X and Y."); + COORD newSize = smallSize; + + if (shrinkX) + { + newSize.X -= 2; + } + else + { + newSize.X += 2; + } + + if (shrinkY) + { + newSize.Y -= 2; + } + else + { + newSize.Y += 2; + } + + // When we grow, we extend the last color. Therefore, this region covers the area colored the same as the letters but filled with a blank. + const auto widthAdjustedView = Viewport::FromDimensions(writtenView.Origin(), { newSize.X, smallSize.Y }); + + // When we resize, we expect the attributes to be unchanged, but the new cells + // to be filled with spaces + wchar_t expectedSpace = UNICODE_SPACE; + std::wstring_view expectedSpaceView(&expectedSpace, 1); + + VERIFY_SUCCEEDED(buffer.ResizeTraditional(newSize)); + + Log::Comment(L"Verify every cell in the X dimension is still the same as when filled and the new Y row is just empty default cells."); + { + TextBufferCellIterator viewIt(buffer, { 0, 0 }); + while (viewIt) + { + Log::Comment(NoThrowString().Format(L"Checking cell (Y=%d, X=%d)", viewIt._pos.Y, viewIt._pos.X)); + if (writtenView.IsInBounds(viewIt._pos)) + { + Log::Comment(L"This position is inside our original write area. It should have the original character and color."); + // If the position is in bounds with what we originally wrote, it should have that character and color. + VERIFY_ARE_EQUAL(expectedView, viewIt->Chars()); + VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); + } + else if (widthAdjustedView.IsInBounds(viewIt._pos)) + { + Log::Comment(L"This position is right of our original write area. It should have extended the color rightward and filled with a space."); + // If we missed the original fill, but we're still in region defined by the adjusted width, then + // the color was extended outward but without the character value. + VERIFY_ARE_EQUAL(expectedSpaceView, viewIt->Chars()); + VERIFY_ARE_EQUAL(expectedAttr, viewIt->TextAttr()); + } + else + { + Log::Comment(L"This position is below our ouriginal write area. It should have filled blank lines (space lines) with the default fill color."); + // Otherwise, we use the default. + VERIFY_ARE_EQUAL(expectedSpaceView, viewIt->Chars()); + VERIFY_ARE_EQUAL(defaultAttr, viewIt->TextAttr()); + } + viewIt++; + } + } + +} + +// This tests that when buffer storage rows are rotated around during a resize traditional operation, +// that the Unicode Storage-held high unicode items like emoji rotate properly with it. +void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() +{ + // Set up a text buffer for us + const COORD bufferSize{ 80, 10 }; + const UINT cursorSize = 12; + const TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Get a position inside the buffer + const COORD pos{ 2, 1 }; + auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + + // Fill it up with a sequence that will have to hit the high unicode storage. + // 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"; + position = bbutton; + + // Read back the text at that position and ensure that it matches what we wrote. + const auto readBack = _buffer->GetTextDataAt(pos); + const auto readBackText = *readBack; + VERIFY_ARE_EQUAL(String(bbutton), String(readBackText.data(), gsl::narrow(readBackText.size()))); + + // Make it the first row in the buffer so it will rotate around when we resize and cause renumbering + const SHORT delta = _buffer->GetFirstRowIndex() - pos.Y; + const COORD newPos{ pos.X, pos.Y + delta }; + + _buffer->_SetFirstRowIndex(pos.Y); + + // Perform resize to rotate the rows around + VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(bufferSize)); + + // Retrieve the text at the old and new positions. + const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos); + const auto shouldBeEmojiText = *_buffer->GetTextDataAt(newPos); + + VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow(shouldBeEmptyText.size()))); + VERIFY_ARE_EQUAL(String(bbutton), String(shouldBeEmojiText.data(), gsl::narrow(shouldBeEmojiText.size()))); +} + +// This tests that when buffer storage rows are rotated around during a scroll buffer operation, +// that the Unicode Storage-held high unicode items like emoji rotate properly with it. +void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() +{ + // Set up a text buffer for us + const COORD bufferSize{ 80, 10 }; + const UINT cursorSize = 12; + const TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Get a position inside the buffer + const COORD pos{ 2, 1 }; + auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + + // Fill it up with a sequence that will have to hit the high unicode storage. + // This is the fire emoji: 🔥 + // It's encoded in UTF-16, as needed by the buffer. + const auto fire = L"\xD83D\xDD25"; + position = fire; + + // Read back the text at that position and ensure that it matches what we wrote. + const auto readBack = _buffer->GetTextDataAt(pos); + const auto readBackText = *readBack; + VERIFY_ARE_EQUAL(String(fire), String(readBackText.data(), gsl::narrow(readBackText.size()))); + + // Prepare a delta and the new position we expect the symbol to be moved into. + const SHORT delta = 5; + const COORD newPos{ pos.X, pos.Y + delta }; + + // 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()))); +} + +// This tests that rows removed from the buffer while resizing traditionally will also drop the high unicode +// characters from the Unicode Storage buffer +void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() +{ + // Set up a text buffer for us + const COORD bufferSize{ 80, 10 }; + const UINT cursorSize = 12; + const TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Get a position inside the buffer in the bottom row + const COORD pos{ 0, bufferSize.Y - 1 }; + auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + + // Fill it up with a sequence that will have to hit the high unicode storage. + // This is the eggplant emoji: 🍆 + // It's encoded in UTF-16, as needed by the buffer. + const auto emoji = L"\xD83C\xDF46"; + position = emoji; + + // Read back the text at that position and ensure that it matches what we wrote. + const auto readBack = _buffer->GetTextDataAt(pos); + const auto readBackText = *readBack; + VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); + + VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); + + // Perform resize to trim off the row of the buffer that included the emoji + COORD trimmedBufferSize{ bufferSize.X, bufferSize.Y - 1 }; + + VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + + VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); +} + +// This tests that columns removed from the buffer while resizing traditionally will also drop the high unicode +// characters from the Unicode Storage buffer +void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() +{ + // Set up a text buffer for us + const COORD bufferSize{ 80, 10 }; + const UINT cursorSize = 12; + const TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Get a position inside the buffer in the last column + const COORD pos{ bufferSize.X - 1, 0 }; + auto position = _buffer->_storage[pos.Y].GetCharRow().GlyphAt(pos.X); + + // Fill it up with a sequence that will have to hit the high unicode storage. + // This is the peach emoji: 🍑 + // It's encoded in UTF-16, as needed by the buffer. + const auto emoji = L"\xD83C\xDF51"; + position = emoji; + + // Read back the text at that position and ensure that it matches what we wrote. + const auto readBack = _buffer->GetTextDataAt(pos); + const auto readBackText = *readBack; + VERIFY_ARE_EQUAL(String(emoji), String(readBackText.data(), gsl::narrow(readBackText.size()))); + + VERIFY_ARE_EQUAL(1u, _buffer->GetUnicodeStorage()._map.size(), L"There should be one item in the map."); + + // Perform resize to trim off the column of the buffer that included the emoji + COORD trimmedBufferSize{ bufferSize.X - 1, bufferSize.Y}; + + VERIFY_NT_SUCCESS(_buffer->ResizeTraditional(trimmedBufferSize)); + + VERIFY_IS_TRUE(_buffer->GetUnicodeStorage()._map.empty(), L"The map should now be empty."); +} + +void TextBufferTests::TestBurrito() +{ + COORD bufferSize{ 80, 9001 }; + UINT cursorSize = 12; + TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // This is the burrito emoji: 🌯 + // It's encoded in UTF-16, as needed by the buffer. + const auto burrito = L"\xD83C\xDF2F"; + OutputCellIterator burriter{ burrito }; + + auto afterFIter = _buffer->Write({ L"F" }); + _buffer->IncrementCursor(); + + auto afterBurritoIter = _buffer->Write(burriter); + _buffer->IncrementCursor(); + _buffer->IncrementCursor(); + VERIFY_IS_FALSE(afterBurritoIter); +} diff --git a/src/host/ut_host/TitleTests.cpp b/src/host/ut_host/TitleTests.cpp new file mode 100644 index 000000000..ec5166505 --- /dev/null +++ b/src/host/ut_host/TitleTests.cpp @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "srvinit.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class TitleTests +{ + TEST_CLASS(TitleTests); + + TEST_CLASS_SETUP(ClassSetup) + { + // This class assumes that %SystemRoot% == c:\windows + WCHAR szSystemRoot[MAX_PATH]; + if (0 != GetWindowsDirectoryW(szSystemRoot, ARRAYSIZE(szSystemRoot))) + { + String strSystemRoot(szSystemRoot); + String strExpectedSystemRoot(L"c:\\windows"); + return (strSystemRoot.ToLower() == strExpectedSystemRoot.ToLower()); + } + + return false; + } + + + TEST_METHOD(TestTranslateConsoleTitle) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:consoleTitle", L"{foo\\bar, c:\\windows\\system32\\cmd.exe, x:\\file\\path}") + TEST_METHOD_PROPERTY(L"Data:unexpand", L"{true, false}") + TEST_METHOD_PROPERTY(L"Data:substitute", L"{true, false}") + END_TEST_METHOD_PROPERTIES(); + + String strConsoleTitle; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"consoleTitle", strConsoleTitle)); + + bool fUnexpand; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"unexpand", fUnexpand)); + + bool fSubstitute; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"substitute", fSubstitute)); + + PWSTR pszTranslated = TranslateConsoleTitle(strConsoleTitle, fUnexpand, fSubstitute); + VERIFY_IS_NOT_NULL(pszTranslated); + Log::Comment(String().Format(L"Translated title: %s", pszTranslated)); + + String strTranslatedTitle(pszTranslated); + if (strConsoleTitle.Find(L"foo") == 0) + { + // dealing with non-filesystem parameter + if (fSubstitute) + { + // shouldn't have a backslash -- just an underscore + VERIFY_ARE_EQUAL(strTranslatedTitle, String(L"foo_bar")); + } + else + { + // string shouldn't be modified + VERIFY_ARE_EQUAL(strConsoleTitle, strTranslatedTitle); + } + } + else + { + // dealing with filesystem parameter + if (strConsoleTitle.Find(L"c") == 0) + { + // dealing with c:\windows\system32\cmd.exe + if (fUnexpand) + { + if (fSubstitute) + { + VERIFY_ARE_EQUAL(strTranslatedTitle, String(L"%SystemRoot%_system32_cmd.exe")); + } + else + { + VERIFY_ARE_EQUAL(strTranslatedTitle, String(L"%SystemRoot%\\system32\\cmd.exe")); + } + } + else + { + if (fSubstitute) + { + VERIFY_ARE_EQUAL(strTranslatedTitle, String(L"c:_windows_system32_cmd.exe")); + } + else + { + VERIFY_ARE_EQUAL(strConsoleTitle, strTranslatedTitle); + } + } + } + else + { + // dealing with x:\file\path + if (fSubstitute) + { + VERIFY_ARE_EQUAL(strTranslatedTitle, String(L"x:_file_path")); + } + else + { + VERIFY_ARE_EQUAL(strConsoleTitle, strTranslatedTitle); + } + } + } + + delete[] pszTranslated; + pszTranslated = nullptr; + } +}; diff --git a/src/host/ut_host/UnicodeLiteral.hpp b/src/host/ut_host/UnicodeLiteral.hpp new file mode 100644 index 000000000..0aaac84b4 --- /dev/null +++ b/src/host/ut_host/UnicodeLiteral.hpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define REMOTE_STRING L"remote npipe:pipe=foo,server=bar\t" +#define EXPECTED_REMOTE_STRING L"-remote \"npipe:pipe=foo,server=bar\"" diff --git a/src/host/ut_host/Utf16ParserTests.cpp b/src/host/ut_host/Utf16ParserTests.cpp new file mode 100644 index 000000000..310ef7203 --- /dev/null +++ b/src/host/ut_host/Utf16ParserTests.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "../../types/inc/Utf16Parser.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + + +static const std::vector CyrillicChar = { 0x0431 }; // lowercase be +static const std::vector LatinChar = { 0x0061 }; // uppercase A +static const std::vector FullWidthChar = { 0xFF2D }; // fullwidth latin small letter m +static const std::vector GaelicChar = { 0x1E41 }; // latin small letter m with dot above +static const std::vector HiraganaChar = { 0x3059 }; // hiragana su +static const std::vector SunglassesEmoji = { 0xD83D, 0xDE0E }; // smiling face with sunglasses emoji + +class Utf16ParserTests +{ + TEST_CLASS(Utf16ParserTests); + + TEST_METHOD(CanParseNonSurrogateText) + { + const std::vector> expected = { CyrillicChar, LatinChar, FullWidthChar, GaelicChar, HiraganaChar }; + + std::wstring wstr; + for (const auto& charData : expected) + { + wstr.push_back(charData.at(0)); + } + + const std::vector> result = Utf16Parser::Parse(wstr); + + VERIFY_ARE_EQUAL(expected.size(), result.size()); + for (size_t i = 0; i < result.size(); ++i) + { + const auto& sequence = result.at(i); + VERIFY_ARE_EQUAL(sequence, expected.at(i)); + } + } + + TEST_METHOD(CanParseSurrogatePairs) + { + const std::wstring wstr{ SunglassesEmoji.begin(), SunglassesEmoji.end() }; + const std::vector> result = Utf16Parser::Parse(wstr); + + VERIFY_ARE_EQUAL(result.size(), 1u); + VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); + for (size_t i = 0; i < SunglassesEmoji.size(); ++i) + { + VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); + } + } + + TEST_METHOD(WillDropBadSurrogateCombinations) + { + // test dropping of invalid leading surrogates + std::wstring wstr{ SunglassesEmoji.begin(), SunglassesEmoji.end() }; + wstr += wstr; + wstr.at(1) = SunglassesEmoji.at(0); // wstr contains 3 leading, 1 trailing surrogate sequence + + std::vector> result = Utf16Parser::Parse(wstr); + + VERIFY_ARE_EQUAL(result.size(), 1u); + VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); + for (size_t i = 0; i < SunglassesEmoji.size(); ++i) + { + VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); + } + + // test dropping of invalid trailing surrogates + wstr = { SunglassesEmoji.begin(), SunglassesEmoji.end() }; + wstr += wstr; + wstr.at(0) = SunglassesEmoji.at(1); // wstr contains 2 trailing, 1 leading, 1 trailing surrogate sequence + + result = Utf16Parser::Parse(wstr); + + VERIFY_ARE_EQUAL(result.size(), 1u); + VERIFY_ARE_EQUAL(result.at(0).size(), SunglassesEmoji.size()); + for (size_t i = 0; i < SunglassesEmoji.size(); ++i) + { + VERIFY_ARE_EQUAL(result.at(0).at(i), SunglassesEmoji.at(i)); + } + } +}; diff --git a/src/host/ut_host/Utf8ToWideCharParserTests.cpp b/src/host/ut_host/Utf8ToWideCharParserTests.cpp new file mode 100644 index 000000000..72e488d6a --- /dev/null +++ b/src/host/ut_host/Utf8ToWideCharParserTests.cpp @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "utf8ToWideCharParser.hpp" + +#define IsBitSet WI_IsFlagSet + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace std; + +class Utf8ToWideCharParserTests +{ + static const unsigned int utf8CodePage = 65001; + static const unsigned int USACodePage = 1252; + + + TEST_CLASS(Utf8ToWideCharParserTests); + + TEST_METHOD(ConvertsAsciiTest) + { + Log::Comment(L"Testing that ASCII chars are correctly converted to wide chars"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + // ascii "hello" + const unsigned char hello[5] = { 0x48, 0x65, 0x6c, 0x6c, 0x6f }; + const unsigned char wideHello[10] = { 0x48, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00 }; + unsigned int count = 5; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + + VERIFY_SUCCEEDED(parser.Parse(hello, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)5); + VERIFY_ARE_EQUAL(generated, (unsigned int)5); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for (int i = 0; i < ARRAYSIZE(wideHello); ++i) + { + VERIFY_ARE_EQUAL(wideHello[i], pReturnedBytes[i]); + } + } + + TEST_METHOD(ConvertSimpleUtf8Test) + { + Log::Comment(L"Testing that a simple UTF8 sequence can be converted"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + // U+3059, U+3057 (hiragana sushi) + const unsigned char sushi[6] = { 0xe3, 0x81, 0x99, 0xe3, 0x81, 0x97}; + const unsigned char wideSushi[4] = { 0x59, 0x30, 0x57, 0x30 }; + unsigned int count = 6; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + + VERIFY_SUCCEEDED(parser.Parse(sushi, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)6); + VERIFY_ARE_EQUAL(generated, (unsigned int)2); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for (int i = 0; i < ARRAYSIZE(wideSushi); ++i) + { + VERIFY_ARE_EQUAL(wideSushi[i], pReturnedBytes[i]); + } + } + + TEST_METHOD(WaitsForAdditionalInputAfterPartialSequenceTest) + { + Log::Comment(L"Testing that nothing is returned when parsing a partial sequence until the sequence is complete"); + // U+3057 (hiragana shi) + unsigned char shi[3] = { 0xe3, 0x81, 0x97 }; + unsigned char wideShi[2] = { 0x57, 0x30 }; + auto parser = Utf8ToWideCharParser { utf8CodePage }; + unsigned int count = 1; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + + for (int i = 0; i < 2; ++i) + { + VERIFY_SUCCEEDED(parser.Parse(shi + i, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)1); + VERIFY_ARE_EQUAL(generated, (unsigned int)0); + VERIFY_ARE_EQUAL(output.get(), nullptr); + count = 1; + } + + VERIFY_SUCCEEDED(parser.Parse(shi + 2, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)1); + VERIFY_ARE_EQUAL(generated, (unsigned int)1); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for (int i = 0; i < ARRAYSIZE(wideShi); ++i) + { + VERIFY_ARE_EQUAL(wideShi[i], pReturnedBytes[i]); + } + } + + TEST_METHOD(ReturnsInitialPartOfSequenceThatEndsWithPartialTest) + { + Log::Comment(L"Testing that a valid portion of a sequence is returned when it ends with a partial sequence"); + // U+3059, U+3057 (hiragana sushi) + const unsigned char sushi[6] = { 0xe3, 0x81, 0x99, 0xe3, 0x81, 0x97 }; + const unsigned char wideSushi[4] = { 0x59, 0x30, 0x57, 0x30 }; + unsigned int count = 4; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + auto parser = Utf8ToWideCharParser { utf8CodePage }; + + VERIFY_SUCCEEDED(parser.Parse(sushi, count, consumed, output, generated)); + // check that we got the first wide char back + VERIFY_ARE_EQUAL(consumed, (unsigned int)4); + VERIFY_ARE_EQUAL(generated, (unsigned int)1); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for (int i = 0; i < 2; ++i) + { + VERIFY_ARE_EQUAL(wideSushi[i], pReturnedBytes[i]); + } + + // add byte 2 of 3 to parser + count = 1; + consumed = 0; + generated = 0; + output.reset(nullptr); + VERIFY_SUCCEEDED(parser.Parse(sushi + 4, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)1); + VERIFY_ARE_EQUAL(generated, (unsigned int)0); + VERIFY_ARE_EQUAL(output.get(), nullptr); + + // add last byte + count = 1; + consumed = 0; + generated = 0; + output.reset(nullptr); + VERIFY_SUCCEEDED(parser.Parse(sushi + 5, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)1); + VERIFY_ARE_EQUAL(generated, (unsigned int)1); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + pReturnedBytes = reinterpret_cast(output.get()); + for (int i = 0; i < 2; ++i) + { + VERIFY_ARE_EQUAL(wideSushi[i + 2], pReturnedBytes[i]); + } + } + + TEST_METHOD(MergesMultiplePartialSequencesTest) + { + Log::Comment(L"Testing that partial sequences sent individually will be merged together"); + // (hiragana doomo arigatoo) + const unsigned char doomoArigatoo[24] = { + 0xe3, 0x81, 0xa9, // U+3069 + 0xe3, 0x81, 0x86, // U+3046 + 0xe3, 0x82, 0x82, // U+3082 + 0xe3, 0x81, 0x82, // U+3042 + 0xe3, 0x82, 0x8a, // U+308A + 0xe3, 0x81, 0x8c, // U+304C + 0xe3, 0x81, 0xa8, // U+3068 + 0xe3, 0x81, 0x86 // U+3046 + }; + const unsigned char wideDoomoArigatoo[16] = { + 0x69, 0x30, + 0x46, 0x30, + 0x82, 0x30, + 0x42, 0x30, + 0x8a, 0x30, + 0x4c, 0x30, + 0x68, 0x30, + 0x46, 0x30 + }; + // send first 4 bytes + unsigned int count = 4; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + auto parser = Utf8ToWideCharParser { utf8CodePage }; + + VERIFY_SUCCEEDED(parser.Parse(doomoArigatoo, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)4); + VERIFY_ARE_EQUAL(generated, (unsigned int)1); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for(int i = 0; i < 2; ++i) + { + VERIFY_ARE_EQUAL(wideDoomoArigatoo[i], pReturnedBytes[i]); + } + + // send next 16 bytes + count = 16; + consumed = 0; + generated = 0; + output.reset(nullptr); + VERIFY_SUCCEEDED(parser.Parse(doomoArigatoo + 4, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)16); + VERIFY_ARE_EQUAL(generated, (unsigned int)5); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + pReturnedBytes = reinterpret_cast(output.get()); + for(int i = 0; i < 10; ++i) + { + VERIFY_ARE_EQUAL(wideDoomoArigatoo[i + 2], pReturnedBytes[i]); + } + + // send last 4 bytes + count = 4; + consumed = 0; + generated = 0; + output.reset(nullptr); + VERIFY_SUCCEEDED(parser.Parse(doomoArigatoo + 20, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)4); + VERIFY_ARE_EQUAL(generated, (unsigned int)2); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + pReturnedBytes = reinterpret_cast(output.get()); + for(int i = 0; i < 4; ++i) + { + VERIFY_ARE_EQUAL(wideDoomoArigatoo[i + 12], pReturnedBytes[i]); + } + } + + TEST_METHOD(RemovesInvalidSequencesTest) + { + Log::Comment(L"Testing that invalid sequences are removed and don't stop the parsing of the rest"); + // hiragana sushi with junk between japanese characters + const unsigned char sushi[9] = { + 0xe3, 0x81, 0x99, // U+3059 + 0x80, 0x81, 0x82, // junk continuation bytes + 0xe3, 0x81, 0x97 // U+3057 + }; + const unsigned char wideSushi[4] = { 0x59, 0x30, 0x57, 0x30 }; + unsigned int count = 9; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + auto parser = Utf8ToWideCharParser { utf8CodePage }; + + VERIFY_SUCCEEDED(parser.Parse(sushi, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(consumed, (unsigned int)9); + VERIFY_ARE_EQUAL(generated, (unsigned int)2); + VERIFY_ARE_NOT_EQUAL(output.get(), nullptr); + + unsigned char* pReturnedBytes = reinterpret_cast(output.get()); + for(int i = 0; i < ARRAYSIZE(wideSushi); ++i) + { + VERIFY_ARE_EQUAL(wideSushi[i], pReturnedBytes[i]); + } + } + + TEST_METHOD(PartialBytesAreDroppedOnCodePageChangeTest) + { + Log::Comment(L"Testing that a saved partial sequence is cleared when the codepage changes"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + // 2 bytes of a 4 byte sequence + const unsigned int inputSize = 2; + const unsigned char partialSequence[inputSize] = { 0xF0, 0x80 }; + unsigned int count = inputSize; + unsigned int consumed = 0; + unsigned int generated = 0; + unique_ptr output { nullptr }; + VERIFY_SUCCEEDED(parser.Parse(partialSequence, count, consumed, output, generated)); + VERIFY_ARE_EQUAL(parser._currentState, Utf8ToWideCharParser::_State::BeginPartialParse); + VERIFY_ARE_EQUAL(parser._bytesStored, inputSize); + // set the codepage to the same one it currently is, ensure + // that nothing changes + parser.SetCodePage(utf8CodePage); + VERIFY_ARE_EQUAL(parser._currentState, Utf8ToWideCharParser::_State::BeginPartialParse); + VERIFY_ARE_EQUAL(parser._bytesStored, inputSize); + // change to a different codepage, ensure parser is reset + parser.SetCodePage(USACodePage); + VERIFY_ARE_EQUAL(parser._currentState, Utf8ToWideCharParser::_State::Ready); + VERIFY_ARE_EQUAL(parser._bytesStored, (unsigned int)0); + } + + TEST_METHOD(_IsLeadByteTest) + { + Log::Comment(L"Testing that _IsLeadByte properly differentiates correct from incorrect sequences"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + VERIFY_IS_TRUE(parser._IsLeadByte(0xC0)); // 2 byte sequence + VERIFY_IS_TRUE(parser._IsLeadByte(0xE0)); // 3 byte sequence + VERIFY_IS_TRUE(parser._IsLeadByte(0xF0)); // 4 byte sequence + VERIFY_IS_FALSE(parser._IsLeadByte(0x00)); // ASCII char NUL + VERIFY_IS_FALSE(parser._IsLeadByte(0x80)); // continuation byte + VERIFY_IS_FALSE(parser._IsLeadByte(0x83)); // continuation byte + VERIFY_IS_FALSE(parser._IsLeadByte(0x7E)); // ASCII char '~' + VERIFY_IS_FALSE(parser._IsLeadByte(0x21)); // ASCII char '!' + VERIFY_IS_FALSE(parser._IsLeadByte(0xF8)); // invalid 5 byte sequence + VERIFY_IS_FALSE(parser._IsLeadByte(0xFC)); // invalid 6 byte sequence + VERIFY_IS_FALSE(parser._IsLeadByte(0xFE)); // invalid 7 byte sequence + VERIFY_IS_FALSE(parser._IsLeadByte(0xFF)); // all 1's + } + + + TEST_METHOD(_IsContinuationByteTest) + { + Log::Comment(L"Testing that _IsContinuationByte properly differentiates correct from incorrect sequences"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + for (BYTE i = 0x00; i < 0xFF; ++i) + { + if (IsBitSet(i, 0x80) && !IsBitSet(i, 0x40)) + { + VERIFY_IS_TRUE(parser._IsContinuationByte(i), NoThrowString().Format(L"Byte is 0x%02x", i)); + } + else + { + VERIFY_IS_FALSE(parser._IsContinuationByte(i), NoThrowString().Format(L"Byte is 0x%02x", i)); + } + } + VERIFY_IS_FALSE(parser._IsContinuationByte(0xFF)); + } + + TEST_METHOD(_IsAsciiByteTest) + { + Log::Comment(L"Testing that _IsAsciiByte properly differentiates correct from incorrect sequences"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + for (BYTE i = 0x00; i < 0x80; ++i) + { + VERIFY_IS_TRUE(parser._IsAsciiByte(i), NoThrowString().Format(L"Byte is 0x%02x", i)); + } + for (BYTE i = 0xFF; i > 0x7F; --i) + { + VERIFY_IS_FALSE(parser._IsAsciiByte(i), NoThrowString().Format(L"Byte is 0x%02x", i)); + } + } + + TEST_METHOD(_Utf8SequenceSizeTest) + { + Log::Comment(L"Testing that _Utf8SequenceSize correctly counts the number of MSB 1's"); + auto parser = Utf8ToWideCharParser { utf8CodePage }; + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0x00), (unsigned int)0); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0x80), (unsigned int)1); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xC2), (unsigned int)2); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xE3), (unsigned int)3); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xF0), (unsigned int)4); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xF3), (unsigned int)4); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xF8), (unsigned int)5); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xFC), (unsigned int)6); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xFD), (unsigned int)6); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xFE), (unsigned int)7); + VERIFY_ARE_EQUAL(parser._Utf8SequenceSize(0xFF), (unsigned int)8); + } + +}; diff --git a/src/host/ut_host/UtilsTests.cpp b/src/host/ut_host/UtilsTests.cpp new file mode 100644 index 000000000..4e74d93c2 --- /dev/null +++ b/src/host/ut_host/UtilsTests.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "globals.h" + +#include + +#include "utils.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class UtilsTests +{ + TEST_CLASS(UtilsTests); + + CommonState* m_state; + + TEST_CLASS_SETUP(ClassSetup) + { + m_state = new CommonState(); + + m_state->PrepareGlobalFont(); + m_state->PrepareGlobalScreenBuffer(); + + UINT const seed = (UINT)time(NULL); + Log::Comment(String().Format(L"Setting random seed to : %d", seed)); + srand(seed); + + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + m_state->CleanupGlobalScreenBuffer(); + m_state->CleanupGlobalFont(); + + delete m_state; + + return true; + } + + SHORT RandomShort() + { + SHORT s; + + do + { + s = (SHORT)rand() % SHORT_MAX; + } while (s == 0i16); + + return s; + } + + void FillBothCoordsSameRandom(COORD* pcoordA, COORD* pcoordB) + { + pcoordA->X = pcoordB->X = RandomShort(); + pcoordA->Y = pcoordB->Y = RandomShort(); + } + + void LogCoordinates(const COORD coordA, const COORD coordB) + { + Log::Comment(String().Format(L"Coordinates - A: (%d, %d) B: (%d, %d)", coordA.X, coordA.Y, coordB.X, coordB.Y)); + } + + void SubtractRandom(short &psValue) + { + SHORT const sRand = RandomShort(); + psValue -= gsl::narrow(std::max(sRand % psValue, 1)); + } + + TEST_METHOD(TestCompareCoords) + { + int result = 5; // not 1, 0, or -1 + COORD coordA; + COORD coordB; + + // Set the buffer size to be able to accomodate large values. + COORD coordMaxBuffer; + coordMaxBuffer.X = SHORT_MAX; + coordMaxBuffer.Y = SHORT_MAX; + + Log::Comment(L"#1: 0 case. Coords equal"); + FillBothCoordsSameRandom(&coordA, &coordB); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_ARE_EQUAL(result, 0); + + Log::Comment(L"#2: -1 case. A comes before B"); + Log::Comment(L"A. A left of B, same line"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordA.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_LESS_THAN(result, 0); + + Log::Comment(L"B. A above B, same column"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordA.Y); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_LESS_THAN(result, 0); + + Log::Comment(L"C. A up and to the left of B."); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordA.Y); + SubtractRandom(coordA.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_LESS_THAN(result, 0); + + Log::Comment(L"D. A up and to the right of B."); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordA.Y); + SubtractRandom(coordB.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_LESS_THAN(result, 0); + + Log::Comment(L"#3: 1 case. A comes after B"); + Log::Comment(L"A. A right of B, same line"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordB.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_GREATER_THAN(result, 0); + + Log::Comment(L"B. A below B, same column"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordB.Y); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_GREATER_THAN(result, 0); + + Log::Comment(L"C. A down and to the left of B"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordB.Y); + SubtractRandom(coordA.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_GREATER_THAN(result, 0); + + Log::Comment(L"D. A down and to the right of B"); + FillBothCoordsSameRandom(&coordA, &coordB); + SubtractRandom(coordB.Y); + SubtractRandom(coordB.X); + LogCoordinates(coordA, coordB); + result = Utils::s_CompareCoords(coordMaxBuffer, coordA, coordB); + VERIFY_IS_GREATER_THAN(result, 0); + } +}; diff --git a/src/host/ut_host/ViewportTests.cpp b/src/host/ut_host/ViewportTests.cpp new file mode 100644 index 000000000..af6c482ad --- /dev/null +++ b/src/host/ut_host/ViewportTests.cpp @@ -0,0 +1,1058 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "CommonState.hpp" + +#include "../../types/inc/Viewport.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using Viewport = Microsoft::Console::Types::Viewport; + +class ViewportTests +{ + TEST_CLASS(ViewportTests); + + TEST_METHOD(CreateEmpty) + { + const auto v = Viewport::Empty(); + + VERIFY_ARE_EQUAL(0, v.Left()); + VERIFY_ARE_EQUAL(-1, v.RightInclusive()); + VERIFY_ARE_EQUAL(0, v.RightExclusive()); + VERIFY_ARE_EQUAL(0, v.Top()); + VERIFY_ARE_EQUAL(-1, v.BottomInclusive()); + VERIFY_ARE_EQUAL(0, v.BottomExclusive()); + VERIFY_ARE_EQUAL(0, v.Height()); + VERIFY_ARE_EQUAL(0, v.Width()); + VERIFY_ARE_EQUAL(COORD({ 0 }), v.Origin()); + VERIFY_ARE_EQUAL(COORD({ 0 }), v.Dimensions()); + } + + TEST_METHOD(CreateFromInclusive) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + COORD origin; + origin.X = rect.Left; + origin.Y = rect.Top; + + COORD dimensions; + dimensions.X = rect.Right - rect.Left + 1; + dimensions.Y = rect.Bottom - rect.Top + 1; + + const auto v = Viewport::FromInclusive(rect); + + VERIFY_ARE_EQUAL(rect.Left, v.Left()); + VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.Top, v.Top()); + VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); + VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); + } + + TEST_METHOD(CreateFromExclusive) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + COORD origin; + origin.X = rect.Left; + origin.Y = rect.Top; + + COORD dimensions; + dimensions.X = rect.Right - rect.Left; + dimensions.Y = rect.Bottom - rect.Top; + + const auto v = Viewport::FromExclusive(rect); + + VERIFY_ARE_EQUAL(rect.Left, v.Left()); + VERIFY_ARE_EQUAL(rect.Right - 1, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.Right, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.Top, v.Top()); + VERIFY_ARE_EQUAL(rect.Bottom - 1, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.Bottom, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); + VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); + } + + TEST_METHOD(CreateFromDimensionsWidthHeight) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + COORD origin; + origin.X = rect.Left; + origin.Y = rect.Top; + + COORD dimensions; + dimensions.X = rect.Right - rect.Left + 1; + dimensions.Y = rect.Bottom - rect.Top + 1; + + const auto v = Viewport::FromDimensions(origin, dimensions.X, dimensions.Y); + + VERIFY_ARE_EQUAL(rect.Left, v.Left()); + VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.Top, v.Top()); + VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); + VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); + } + + TEST_METHOD(CreateFromDimensions) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + COORD origin; + origin.X = rect.Left; + origin.Y = rect.Top; + + COORD dimensions; + dimensions.X = rect.Right - rect.Left + 1; + dimensions.Y = rect.Bottom - rect.Top + 1; + + const auto v = Viewport::FromDimensions(origin, dimensions); + + VERIFY_ARE_EQUAL(rect.Left, v.Left()); + VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.Top, v.Top()); + VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); + VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); + } + + TEST_METHOD(CreateFromDimensionsNoOrigin) + { + SMALL_RECT rect; + rect.Top = 0; + rect.Left = 0; + rect.Bottom = 5; + rect.Right = 20; + + COORD origin; + origin.X = rect.Left; + origin.Y = rect.Top; + + COORD dimensions; + dimensions.X = rect.Right - rect.Left + 1; + dimensions.Y = rect.Bottom - rect.Top + 1; + + const auto v = Viewport::FromDimensions(dimensions); + + VERIFY_ARE_EQUAL(rect.Left, v.Left()); + VERIFY_ARE_EQUAL(rect.Right, v.RightInclusive()); + VERIFY_ARE_EQUAL(rect.Right + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(rect.Top, v.Top()); + VERIFY_ARE_EQUAL(rect.Bottom, v.BottomInclusive()); + VERIFY_ARE_EQUAL(rect.Bottom + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(dimensions.Y, v.Height()); + VERIFY_ARE_EQUAL(dimensions.X, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(dimensions, v.Dimensions()); + } + + TEST_METHOD(CreateFromCoord) + { + COORD origin; + origin.X = 12; + origin.Y = 24; + + const auto v = Viewport::FromCoord(origin); + + VERIFY_ARE_EQUAL(origin.X, v.Left()); + VERIFY_ARE_EQUAL(origin.X, v.RightInclusive()); + VERIFY_ARE_EQUAL(origin.X + 1, v.RightExclusive()); + VERIFY_ARE_EQUAL(origin.Y, v.Top()); + VERIFY_ARE_EQUAL(origin.Y, v.BottomInclusive()); + VERIFY_ARE_EQUAL(origin.Y + 1, v.BottomExclusive()); + VERIFY_ARE_EQUAL(1, v.Height()); + VERIFY_ARE_EQUAL(1, v.Width()); + VERIFY_ARE_EQUAL(origin, v.Origin()); + VERIFY_ARE_EQUAL(COORD({ 1, 1, }), v.Dimensions()); + } + + TEST_METHOD(ToRect) + { + COORD origin; + origin.X = 2; + origin.Y = 4; + + COORD dimensions; + dimensions.X = 10; + dimensions.Y = 20; + + const auto v = Viewport::FromDimensions(origin, dimensions); + + const RECT rc = v.ToRect(); + const SMALL_RECT exclusive = v.ToExclusive(); + + VERIFY_ARE_EQUAL(exclusive.Left, v.Left()); + VERIFY_ARE_EQUAL(rc.left, v.Left()); + + VERIFY_ARE_EQUAL(exclusive.Top, v.Top()); + VERIFY_ARE_EQUAL(rc.top, v.Top()); + + VERIFY_ARE_EQUAL(exclusive.Right, v.RightExclusive()); + VERIFY_ARE_EQUAL(rc.right, v.RightExclusive()); + + VERIFY_ARE_EQUAL(exclusive.Bottom, v.BottomExclusive()); + VERIFY_ARE_EQUAL(rc.bottom, v.BottomExclusive()); + } + + TEST_METHOD(IsInBoundsCoord) + { + SMALL_RECT r; + r.Top = 3; + r.Bottom = 5; + r.Left = 10; + r.Right = 20; + + const auto v = Viewport::FromInclusive(r); + + COORD c; + c.X = r.Left; + c.Y = r.Top; + VERIFY_IS_TRUE(v.IsInBounds(c), L"Top left corner in bounds."); + + c.Y = r.Bottom; + VERIFY_IS_TRUE(v.IsInBounds(c), L"Bottom left corner in bounds."); + + c.X = r.Right; + VERIFY_IS_TRUE(v.IsInBounds(c), L"Bottom right corner in bounds."); + + c.Y = r.Top; + VERIFY_IS_TRUE(v.IsInBounds(c), L"Top right corner in bounds."); + + c.X++; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One right out the top right is out of bounds."); + + c.X--; + c.Y--; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One up out the top right is out of bounds."); + + c.X = r.Left; + c.Y = r.Top; + c.X--; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One left out the top left is out of bounds."); + + c.X++; + c.Y--; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One up out the top left is out of bounds."); + + c.X = r.Left; + c.Y = r.Bottom; + c.X--; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One left out the bottom left is out of bounds."); + + c.X++; + c.Y++; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One down out the bottom left is out of bounds."); + + c.X = r.Right; + c.Y = r.Bottom; + c.X++; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One right out the bottom right is out of bounds."); + + c.X--; + c.Y++; + VERIFY_IS_FALSE(v.IsInBounds(c), L"One down out the bottom right is out of bounds."); + } + + TEST_METHOD(IsInBoundsViewport) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + const auto original = rect; + + const auto view = Viewport::FromInclusive(rect); + + auto test = Viewport::FromInclusive(rect); + VERIFY_IS_TRUE(view.IsInBounds(test), L"Same size/position viewport is in bounds."); + + rect.Top++; + rect.Bottom--; + rect.Left++; + rect.Right--; + test = Viewport::FromInclusive(rect); + VERIFY_IS_TRUE(view.IsInBounds(test), L"Viewport inscribed inside viewport is in bounds."); + + rect = original; + rect.Top--; + test = Viewport::FromInclusive(rect); + VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one taller upwards is out of bounds."); + + rect = original; + rect.Bottom++; + test = Viewport::FromInclusive(rect); + VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one taller downwards is out of bounds."); + + rect = original; + rect.Left--; + test = Viewport::FromInclusive(rect); + VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one wider leftwards is out of bounds."); + + rect = original; + rect.Right++; + test = Viewport::FromInclusive(rect); + VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport that is one wider rightwards is out of bounds."); + + rect = original; + rect.Left++; + rect.Right++; + rect.Top++; + rect.Bottom++; + test = Viewport::FromInclusive(rect); + VERIFY_IS_FALSE(view.IsInBounds(test), L"Viewport offset at the origin but same size is out of bounds."); + } + + TEST_METHOD(ClampCoord) + { + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + const auto view = Viewport::FromInclusive(rect); + + COORD pos; + pos.X = rect.Left; + pos.Y = rect.Top; + + auto before = pos; + view.Clamp(pos); + VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in top left corner."); + + pos.X = rect.Left; + pos.Y = rect.Bottom; + before = pos; + view.Clamp(pos); + VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in bottom left corner."); + + pos.X = rect.Right; + pos.Y = rect.Bottom; + before = pos; + view.Clamp(pos); + VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in bottom right corner."); + + pos.X = rect.Right; + pos.Y = rect.Top; + before = pos; + view.Clamp(pos); + VERIFY_ARE_EQUAL(before, pos, L"Verify clamp did nothing for position in top right corner."); + + + COORD expected; + expected.X = rect.Right; + expected.Y = rect.Top; + + pos = expected; + pos.X++; + pos.Y--; + before = pos; + + view.Clamp(pos); + VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the top right corner back."); + VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the top right corner."); + + expected.X = rect.Left; + expected.Y = rect.Top; + + pos = expected; + pos.X--; + pos.Y--; + before = pos; + + view.Clamp(pos); + VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the top left corner back."); + VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the top left corner."); + + expected.X = rect.Left; + expected.Y = rect.Bottom; + + pos = expected; + pos.X--; + pos.Y++; + before = pos; + + view.Clamp(pos); + VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the bottom left corner back."); + VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the bottom left corner."); + + expected.X = rect.Right; + expected.Y = rect.Bottom; + + pos = expected; + pos.X++; + pos.Y++; + before = pos; + + view.Clamp(pos); + VERIFY_ARE_NOT_EQUAL(before, pos, L"Verify clamp modified position out the bottom right corner back."); + VERIFY_ARE_EQUAL(expected, pos, L"Verify position was clamped into the bottom right corner."); + + Viewport invalidView = Viewport::Empty(); + VERIFY_THROWS_SPECIFIC(invalidView.Clamp(pos), + wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_NOT_VALID_STATE; }); + } + + TEST_METHOD(ClampViewport) + { + // Create the rectangle/view we will clamp to. + SMALL_RECT rect; + rect.Top = 3; + rect.Bottom = 5; + rect.Left = 10; + rect.Right = 20; + + const auto view = Viewport::FromInclusive(rect); + + Log::Comment(L"Make a rectangle that is larger than and fully encompasses our clamping rectangle."); + SMALL_RECT testRect; + testRect.Top = rect.Top - 3; + testRect.Bottom = rect.Bottom + 3; + testRect.Left = rect.Left - 3; + testRect.Right = rect.Right + 3; + + auto testView = Viewport::FromInclusive(testRect); + + auto actual = view.Clamp(testView); + VERIFY_ARE_EQUAL(view, actual, L"All sides should get reduced down to the size of the given rect."); + + Log::Comment(L"Make a rectangle that is fully inscribed inside our clamping rectangle."); + testRect.Top = rect.Top + 1; + testRect.Bottom = rect.Bottom - 1; + testRect.Left = rect.Left + 1; + testRect.Right = rect.Right - 1; + testView = Viewport::FromInclusive(testRect); + + actual = view.Clamp(testView); + VERIFY_ARE_EQUAL(testView, actual, L"Verify that nothing changed because this rectangle already sat fully inside the clamping rectangle."); + + Log::Comment(L"Craft a rectangle where the left is outside the right, right is outside the left, top is outside the bottom, and bottom is outside the top."); + testRect.Top = rect.Bottom + 10; + testRect.Bottom = rect.Top - 10; + testRect.Left = rect.Right + 10; + testRect.Right = rect.Left - 10; + testView = Viewport::FromInclusive(testRect); + + Log::Comment(L"We expect it to be pulled back so each coordinate is in bounds, but the rectangle is still invalid (since left will be > right)."); + SMALL_RECT expected; + expected.Top = rect.Bottom; + expected.Bottom = rect.Top; + expected.Left = rect.Right; + expected.Right = rect.Left; + const auto expectedView = Viewport::FromInclusive(expected); + + actual = view.Clamp(testView); + VERIFY_ARE_EQUAL(expectedView, actual, L"Every dimension should be pulled just inside the clamping rectangle."); + } + + TEST_METHOD(IncrementInBounds) + { + bool success = false; + + SMALL_RECT edges; + edges.Left = 10; + edges.Right = 19; + edges.Top = 20; + edges.Bottom = 29; + + const auto v = Viewport::FromInclusive(edges); + COORD original; + COORD screen; + + // #1 coord inside region + original.X = screen.X = 15; + original.Y = screen.Y = 25; + + success = v.IncrementInBounds(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, original.X + 1); + VERIFY_ARE_EQUAL(screen.Y, original.Y); + + // #2 coord right edge, not bottom + original.X = screen.X = edges.Right; + original.Y = screen.Y = 25; + + success = v.IncrementInBounds(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Left); + VERIFY_ARE_EQUAL(screen.Y, original.Y + 1); + + // #3 coord right edge, bottom + original.X = screen.X = edges.Right; + original.Y = screen.Y = edges.Bottom; + + success = v.IncrementInBounds(screen); + + VERIFY_IS_FALSE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Right); + VERIFY_ARE_EQUAL(screen.Y, edges.Bottom); + } + + TEST_METHOD(IncrementInBoundsCircular) + { + bool success = false; + + SMALL_RECT edges; + edges.Left = 10; + edges.Right = 19; + edges.Top = 20; + edges.Bottom = 29; + + const auto v = Viewport::FromInclusive(edges); + COORD original; + COORD screen; + + // #1 coord inside region + original.X = screen.X = 15; + original.Y = screen.Y = 25; + + success = v.IncrementInBoundsCircular(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, original.X + 1); + VERIFY_ARE_EQUAL(screen.Y, original.Y); + + // #2 coord right edge, not bottom + original.X = screen.X = edges.Right; + original.Y = screen.Y = 25; + + success = v.IncrementInBoundsCircular(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Left); + VERIFY_ARE_EQUAL(screen.Y, original.Y + 1); + + // #3 coord right edge, bottom + original.X = screen.X = edges.Right; + original.Y = screen.Y = edges.Bottom; + + success = v.IncrementInBoundsCircular(screen); + + VERIFY_IS_FALSE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Left); + VERIFY_ARE_EQUAL(screen.Y, edges.Top); + } + + TEST_METHOD(DecrementInBounds) + { + bool success = false; + + SMALL_RECT edges; + edges.Left = 10; + edges.Right = 19; + edges.Top = 20; + edges.Bottom = 29; + + const auto v = Viewport::FromInclusive(edges); + COORD original; + COORD screen; + + // #1 coord inside region + original.X = screen.X = 15; + original.Y = screen.Y = 25; + + success = v.DecrementInBounds(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, original.X - 1); + VERIFY_ARE_EQUAL(screen.Y, original.Y); + + // #2 coord left edge, not top + original.X = screen.X = edges.Left; + original.Y = screen.Y = 25; + + success = v.DecrementInBounds(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Right); + VERIFY_ARE_EQUAL(screen.Y, original.Y - 1); + + // #3 coord left edge, top + original.X = screen.X = edges.Left; + original.Y = screen.Y = edges.Top; + + success = v.DecrementInBounds(screen); + + VERIFY_IS_FALSE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Left); + VERIFY_ARE_EQUAL(screen.Y, edges.Top); + } + + TEST_METHOD(DecrementInBoundsCircular) + { + bool success = false; + + SMALL_RECT edges; + edges.Left = 10; + edges.Right = 19; + edges.Top = 20; + edges.Bottom = 29; + + const auto v = Viewport::FromInclusive(edges); + COORD original; + COORD screen; + + // #1 coord inside region + original.X = screen.X = 15; + original.Y = screen.Y = 25; + + success = v.DecrementInBoundsCircular(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, original.X - 1); + VERIFY_ARE_EQUAL(screen.Y, original.Y); + + // #2 coord left edge, not top + original.X = screen.X = edges.Left; + original.Y = screen.Y = 25; + + success = v.DecrementInBoundsCircular(screen); + + VERIFY_IS_TRUE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Right); + VERIFY_ARE_EQUAL(screen.Y, original.Y - 1); + + // #3 coord left edge, top + original.X = screen.X = edges.Left; + original.Y = screen.Y = edges.Top; + + success = v.DecrementInBoundsCircular(screen); + + VERIFY_IS_FALSE(success); + VERIFY_ARE_EQUAL(screen.X, edges.Right); + VERIFY_ARE_EQUAL(screen.Y, edges.Bottom); + } + + SHORT RandomShort() + { + SHORT s; + + do + { + s = (SHORT)rand() % SHORT_MAX; + } while (s == 0i16); + + return s; + } + + TEST_METHOD(MoveInBounds) + { + const UINT cTestLoopInstances = 100; + + const SHORT sRowWidth = 20; + VERIFY_IS_TRUE(sRowWidth > 0); + + // 20x20 box + SMALL_RECT srectEdges; + srectEdges.Top = srectEdges.Left = 0; + srectEdges.Bottom = srectEdges.Right = sRowWidth - 1; + + const auto v = Viewport::FromInclusive(srectEdges); + + // repeat test + for (UINT i = 0; i < cTestLoopInstances; i++) + { + COORD coordPos; + coordPos.X = RandomShort() % 20; + coordPos.Y = RandomShort() % 20; + + SHORT sAddAmount = RandomShort() % (sRowWidth * sRowWidth); + + COORD coordFinal; + coordFinal.X = (coordPos.X + sAddAmount) % sRowWidth; + coordFinal.Y = coordPos.Y + ((coordPos.X + sAddAmount) / sRowWidth); + + Log::Comment(String().Format(L"Add To Position: (%d, %d) Amount to add: %d", coordPos.Y, coordPos.X, sAddAmount)); + + // Movement result is expected to be true, unless there's an error. + bool fExpectedResult = true; + + // if we've calculated past the final row, then the function will reset to the original position and the output will be false. + if (coordFinal.Y >= sRowWidth) + { + coordFinal = coordPos; + fExpectedResult = false; + } + + bool const fActualResult = v.MoveInBounds(sAddAmount, coordPos); + + VERIFY_ARE_EQUAL(fExpectedResult, fActualResult); + VERIFY_ARE_EQUAL(coordPos.X, coordFinal.X); + VERIFY_ARE_EQUAL(coordPos.Y, coordFinal.Y); + + Log::Comment(String().Format(L"Actual: (%d, %d) Expected: (%d, %d)", coordPos.Y, coordPos.X, coordFinal.Y, coordFinal.X)); + } + } + + TEST_METHOD(CompareInBounds) + { + SMALL_RECT edges; + edges.Left = 10; + edges.Right = 19; + edges.Top = 20; + edges.Bottom = 29; + + const auto v = Viewport::FromInclusive(edges); + + COORD first, second; + first.X = 12; + first.Y = 24; + second = first; + second.X += 2; + + VERIFY_ARE_EQUAL(-2, v.CompareInBounds(first, second), L"Second and first on same row. Second is right of first."); + VERIFY_ARE_EQUAL(2, v.CompareInBounds(second, first), L"Reverse params, should get opposite direction, same magnitude."); + + first.X = edges.Left; + first.Y = 24; + + second.X = edges.Right; + second.Y = first.Y - 1; + + VERIFY_ARE_EQUAL(1, v.CompareInBounds(first, second), L"Second is up a line at the right edge from first at the line below on the left edge."); + VERIFY_ARE_EQUAL(-1, v.CompareInBounds(second, first), L"Reverse params, should get opposite direction, same magnitude."); + } + + TEST_METHOD(Offset) + { + SMALL_RECT edges; + edges.Top = 0; + edges.Left = 0; + edges.Right = 10; + edges.Bottom = 10; + + const auto original = Viewport::FromInclusive(edges); + + Log::Comment(L"Move down and to the right first."); + COORD adjust = { 7, 2 }; + SMALL_RECT expectedEdges; + expectedEdges.Top = edges.Top + adjust.Y; + expectedEdges.Bottom = edges.Bottom + adjust.Y; + expectedEdges.Left = edges.Left + adjust.X; + expectedEdges.Right = edges.Right + adjust.X; + + Viewport expected = Viewport::FromInclusive(expectedEdges); + + Viewport actual = Viewport::Offset(original, adjust); + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Now try moving up and to the left."); + adjust = { -3, -5 }; + + expectedEdges.Top = edges.Top + adjust.Y; + expectedEdges.Bottom = edges.Bottom + adjust.Y; + expectedEdges.Left = edges.Left + adjust.X; + expectedEdges.Right = edges.Right + adjust.X; + + expected = Viewport::FromInclusive(expectedEdges); + actual = Viewport::Offset(original, adjust); + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Now try adding way too much to cause an overflow."); + adjust = { SHORT_MAX, SHORT_MAX }; + + VERIFY_THROWS_SPECIFIC(const auto vp = Viewport::Offset(original, adjust), + wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW); }); + } + + TEST_METHOD(Union) + { + SMALL_RECT srOne; + srOne.Left = 4; + srOne.Right = 10; + srOne.Top = 6; + srOne.Bottom = 14; + const auto one = Viewport::FromInclusive(srOne); + + SMALL_RECT srTwo; + srTwo.Left = 5; + srTwo.Right = 13; + srTwo.Top = 2; + srTwo.Bottom = 10; + const auto two = Viewport::FromInclusive(srTwo); + + SMALL_RECT srExpected; + srExpected.Left = srOne.Left < srTwo.Left ? srOne.Left : srTwo.Left; + srExpected.Right = srOne.Right > srTwo.Right ? srOne.Right : srTwo.Right; + srExpected.Top = srOne.Top < srTwo.Top ? srOne.Top : srTwo.Top; + srExpected.Bottom = srOne.Bottom > srTwo.Bottom ? srOne.Bottom : srTwo.Bottom; + + const auto expected = Viewport::FromInclusive(srExpected); + + const auto actual = Viewport::Union(one, two); + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(Intersect) + { + SMALL_RECT srOne; + srOne.Left = 4; + srOne.Right = 10; + srOne.Top = 6; + srOne.Bottom = 14; + const auto one = Viewport::FromInclusive(srOne); + + SMALL_RECT srTwo; + srTwo.Left = 5; + srTwo.Right = 13; + srTwo.Top = 2; + srTwo.Bottom = 10; + const auto two = Viewport::FromInclusive(srTwo); + + SMALL_RECT srExpected; + srExpected.Left = srOne.Left > srTwo.Left ? srOne.Left : srTwo.Left; + srExpected.Right = srOne.Right < srTwo.Right ? srOne.Right : srTwo.Right; + srExpected.Top = srOne.Top > srTwo.Top ? srOne.Top : srTwo.Top; + srExpected.Bottom = srOne.Bottom < srTwo.Bottom ? srOne.Bottom : srTwo.Bottom; + + const auto expected = Viewport::FromInclusive(srExpected); + + const auto actual = Viewport::Intersect(one, two); + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SubtractFour) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + + SMALL_RECT srRemove; + srRemove.Top = 3; + srRemove.Left = 3; + srRemove.Bottom = 6; + srRemove.Right = 6; + const auto remove = Viewport::FromInclusive(srRemove); + + std::vector expected; + // SMALL_RECT constructed as: Left, Top, Right, Bottom + // Top View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + // Bottom View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Bottom + 1, srOriginal.Right, srOriginal.Bottom })); + // Left View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srRemove.Bottom })); + // Right View + expected.emplace_back(Viewport::FromInclusive({ srRemove.Right + 1, srRemove.Top, srOriginal.Right, srRemove.Bottom })); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } + + TEST_METHOD(SubtractThree) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + + SMALL_RECT srRemove; + srRemove.Top = 3; + srRemove.Left = 3; + srRemove.Bottom = 6; + srRemove.Right = 15; + const auto remove = Viewport::FromInclusive(srRemove); + + std::vector expected; + // SMALL_RECT constructed as: Left, Top, Right, Bottom + // Top View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + // Bottom View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Bottom + 1, srOriginal.Right, srOriginal.Bottom })); + // Left View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srRemove.Bottom })); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } + + TEST_METHOD(SubtractTwo) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + + SMALL_RECT srRemove; + srRemove.Top = 3; + srRemove.Left = 3; + srRemove.Bottom = 15; + srRemove.Right = 15; + const auto remove = Viewport::FromInclusive(srRemove); + + std::vector expected; + // SMALL_RECT constructed as: Left, Top, Right, Bottom + // Top View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + // Left View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srRemove.Top, srRemove.Left - 1, srOriginal.Bottom })); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } + + TEST_METHOD(SubtractOne) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + + SMALL_RECT srRemove; + srRemove.Top = 3; + srRemove.Left = -12; + srRemove.Bottom = 15; + srRemove.Right = 15; + const auto remove = Viewport::FromInclusive(srRemove); + + std::vector expected; + // SMALL_RECT constructed as: Left, Top, Right, Bottom + // Top View + expected.emplace_back(Viewport::FromInclusive({ srOriginal.Left, srOriginal.Top, srOriginal.Right, srRemove.Top - 1 })); + + const auto foo = expected.cbegin(); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } + + TEST_METHOD(SubtractZero) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + + SMALL_RECT srRemove; + srRemove.Top = 12; + srRemove.Left = 12; + srRemove.Bottom = 15; + srRemove.Right = 15; + const auto remove = Viewport::FromInclusive(srRemove); + + std::vector expected; + expected.emplace_back(original); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } + + TEST_METHOD(SubtractSame) + { + SMALL_RECT srOriginal; + srOriginal.Top = 0; + srOriginal.Left = 0; + srOriginal.Bottom = 10; + srOriginal.Right = 10; + const auto original = Viewport::FromInclusive(srOriginal); + const auto remove = original; + + std::vector expected; + expected.emplace_back(Viewport::FromDimensions(original.Origin(), { 0, 0 })); + + const auto actual = Viewport::Subtract(original, remove); + + VERIFY_ARE_EQUAL(expected.size(), actual.size(), L"Same number of viewports in expected and actual"); + Log::Comment(L"Now validate that each viewport has the expected area."); + for (size_t i = 0; i < expected.size(); i++) + { + const auto& exp = expected.at(i); + const auto& act = actual.at(i); + VERIFY_ARE_EQUAL(exp, act); + } + } +}; diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp new file mode 100644 index 000000000..77610004c --- /dev/null +++ b/src/host/ut_host/VtIoTests.cpp @@ -0,0 +1,444 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "..\..\inc\consoletaeftemplates.hpp" +#include "..\..\types\inc\Viewport.hpp" + +#include "..\..\renderer\vt\Xterm256Engine.hpp" +#include "..\..\renderer\vt\XtermEngine.hpp" +#include "..\..\renderer\vt\WinTelnetEngine.hpp" +#include "..\..\renderer\dx\DxRenderer.hpp" +#include "..\..\renderer\base\Renderer.hpp" +#include "..\Settings.hpp" +#include "..\VtIo.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace std; + + +class Microsoft::Console::VirtualTerminal::VtIoTests +{ + TEST_CLASS(VtIoTests); + + // General Tests: + TEST_METHOD(NoOpStartTest); + TEST_METHOD(ModeParsingTest); + + TEST_METHOD(DtorTestJustEngine); + TEST_METHOD(DtorTestDeleteVtio); + TEST_METHOD(DtorTestStackAlloc); + TEST_METHOD(DtorTestStackAllocMany); + + TEST_METHOD(RendererDtorAndThread); + TEST_METHOD(RendererDtorAndThreadAndDx); + + TEST_METHOD(BasicAnonymousPipeOpeningWithSignalChannelTest); +}; + +class VtIoTestColorProvider : public Microsoft::Console::IDefaultColorProvider +{ +public: + virtual ~VtIoTestColorProvider() = default; + COLORREF GetDefaultForeground() const + { + return RGB(0xff, 0xff, 0xff); + } + COLORREF GetDefaultBackground() const + { + return RGB(0, 0, 0); + } +}; + +using namespace Microsoft::Console; +using namespace Microsoft::Console::VirtualTerminal; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +void VtIoTests::NoOpStartTest() +{ + VtIo vtio; + VERIFY_IS_FALSE(vtio.IsUsingVt()); + + Log::Comment(L"Verify we succeed at StartIfNeeded even if we weren't initialized"); + VERIFY_SUCCEEDED(vtio.StartIfNeeded()); +} + +void VtIoTests::ModeParsingTest() +{ + VtIoMode mode; + VERIFY_SUCCEEDED(VtIo::ParseIoMode(L"xterm", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::XTERM); + + VERIFY_SUCCEEDED(VtIo::ParseIoMode(L"xterm-256color", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::XTERM_256); + + VERIFY_SUCCEEDED(VtIo::ParseIoMode(L"win-telnet", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::WIN_TELNET); + + VERIFY_SUCCEEDED(VtIo::ParseIoMode(L"xterm-ascii", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::XTERM_ASCII); + + VERIFY_SUCCEEDED(VtIo::ParseIoMode(L"", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::XTERM_256); + + VERIFY_FAILED(VtIo::ParseIoMode(L"garbage", mode)); + VERIFY_ARE_EQUAL(mode, VtIoMode::INVALID); +} + +Viewport SetUpViewport() +{ + SMALL_RECT view = {}; + view.Top = view.Left = 0; + view.Bottom = 31; + view.Right = 79; + + return Viewport::FromInclusive(view); +} + +void VtIoTests::DtorTestJustEngine() +{ + Log::Comment(NoThrowString().Format( + L"This test is going to instantiate a bunch of VtIos in different \n" + L"scenarios to see if something causes a weird cleanup.\n" + L"It's here because of the strange nature of VtEngine having members\n" + L"that are only defined in UNIT_TESTING" + )); + + const WORD colorTableSize = 16; + COLORREF colorTable[colorTableSize]; + VtIoTestColorProvider p; + + Log::Comment(NoThrowString().Format( + L"New some engines and delete them" + )); + for (int i = 0; i < 25; ++i) + { + Log::Comment(NoThrowString().Format( + L"New/Delete loop #%d", i + )); + + wil::unique_hfile hOutputFile; + hOutputFile.reset(INVALID_HANDLE_VALUE); + auto pRenderer256 = new Xterm256Engine(std::move(hOutputFile), p, SetUpViewport(), colorTable, colorTableSize); + Log::Comment(NoThrowString().Format(L"Made Xterm256Engine")); + delete pRenderer256; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + + auto pRenderEngineXterm = new XtermEngine(std::move(hOutputFile), p, SetUpViewport(), colorTable, colorTableSize, false); + Log::Comment(NoThrowString().Format(L"Made XtermEngine")); + delete pRenderEngineXterm; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + + auto pRenderEngineXtermAscii = new XtermEngine(std::move(hOutputFile), p, SetUpViewport(), colorTable, colorTableSize, true); + Log::Comment(NoThrowString().Format(L"Made XtermEngine")); + delete pRenderEngineXtermAscii; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + + auto pRenderEngineWinTelnet = new WinTelnetEngine(std::move(hOutputFile), p, SetUpViewport(), colorTable, colorTableSize); + Log::Comment(NoThrowString().Format(L"Made WinTelnetEngine")); + delete pRenderEngineWinTelnet; + Log::Comment(NoThrowString().Format(L"Deleted.")); + } + +} + +void VtIoTests::DtorTestDeleteVtio() +{ + Log::Comment(NoThrowString().Format( + L"This test is going to instantiate a bunch of VtIos in different \n" + L"scenarios to see if something causes a weird cleanup.\n" + L"It's here because of the strange nature of VtEngine having members\n" + L"that are only defined in UNIT_TESTING" + )); + + const WORD colorTableSize = 16; + COLORREF colorTable[colorTableSize]; + VtIoTestColorProvider p; + + Log::Comment(NoThrowString().Format( + L"New some engines and delete them" + )); + for (int i = 0; i < 25; ++i) + { + Log::Comment(NoThrowString().Format( + L"New/Delete loop #%d", i + )); + + wil::unique_hfile hOutputFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + + VtIo* vtio = new VtIo(); + Log::Comment(NoThrowString().Format(L"Made VtIo")); + vtio->_pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + Log::Comment(NoThrowString().Format(L"Made Xterm256Engine")); + delete vtio; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + vtio = new VtIo(); + Log::Comment(NoThrowString().Format(L"Made VtIo")); + vtio->_pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + false); + Log::Comment(NoThrowString().Format(L"Made XtermEngine")); + delete vtio; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + vtio = new VtIo(); + Log::Comment(NoThrowString().Format(L"Made VtIo")); + vtio->_pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + true); + Log::Comment(NoThrowString().Format(L"Made XtermEngine")); + delete vtio; + Log::Comment(NoThrowString().Format(L"Deleted.")); + + hOutputFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + vtio = new VtIo(); + Log::Comment(NoThrowString().Format(L"Made VtIo")); + vtio->_pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + Log::Comment(NoThrowString().Format(L"Made WinTelnetEngine")); + delete vtio; + Log::Comment(NoThrowString().Format(L"Deleted.")); + } + +} + +void VtIoTests::DtorTestStackAlloc() +{ + Log::Comment(NoThrowString().Format( + L"This test is going to instantiate a bunch of VtIos in different \n" + L"scenarios to see if something causes a weird cleanup.\n" + L"It's here because of the strange nature of VtEngine having members\n" + L"that are only defined in UNIT_TESTING" + )); + + const WORD colorTableSize = 16; + COLORREF colorTable[colorTableSize]; + VtIoTestColorProvider p; + + Log::Comment(NoThrowString().Format( + L"make some engines and let them fall out of scope" + )); + for (int i = 0; i < 25; ++i) + { + Log::Comment(NoThrowString().Format( + L"Scope Exit Auto cleanup #%d", i + )); + + wil::unique_hfile hOutputFile; + + hOutputFile.reset(INVALID_HANDLE_VALUE); + { + VtIo vtio; + vtio._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + } + + hOutputFile.reset(INVALID_HANDLE_VALUE); + { + VtIo vtio; + vtio._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + false); + } + + hOutputFile.reset(INVALID_HANDLE_VALUE); + { + VtIo vtio; + vtio._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + true); + } + + hOutputFile.reset(INVALID_HANDLE_VALUE); + { + VtIo vtio; + vtio._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + } + } + +} + +void VtIoTests::DtorTestStackAllocMany() +{ + Log::Comment(NoThrowString().Format( + L"This test is going to instantiate a bunch of VtIos in different \n" + L"scenarios to see if something causes a weird cleanup.\n" + L"It's here because of the strange nature of VtEngine having members\n" + L"that are only defined in UNIT_TESTING" + )); + + const WORD colorTableSize = 16; + COLORREF colorTable[colorTableSize]; + VtIoTestColorProvider p; + + Log::Comment(NoThrowString().Format( + L"Try an make a whole bunch all at once, and have them all fall out of scope at once." + )); + for (int i = 0; i < 25; ++i) + { + Log::Comment(NoThrowString().Format( + L"Multiple engines, one scope loop #%d", i + )); + + wil::unique_hfile hOutputFile; + { + hOutputFile.reset(INVALID_HANDLE_VALUE); + VtIo vtio1; + vtio1._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + VtIo vtio2; + vtio2._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + false); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + VtIo vtio3; + vtio3._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize, + true); + + hOutputFile.reset(INVALID_HANDLE_VALUE); + VtIo vtio4; + vtio4._pVtRenderEngine = std::make_unique(std::move(hOutputFile), + p, + SetUpViewport(), + colorTable, + colorTableSize); + } + } + +} + +void VtIoTests::RendererDtorAndThread() +{ + Log::Comment(NoThrowString().Format( + L"Test deleting a Renderer a bunch of times" + )); + + for (int i = 0; i < 16; ++i) + { + auto thread = std::make_unique(); + auto* pThread = thread.get(); + auto pRenderer = std::make_unique(nullptr, nullptr, 0, std::move(thread)); + VERIFY_SUCCEEDED(pThread->Initialize(pRenderer.get())); + // Sleep for a hot sec to make sure the thread starts before we enable painting + // If you don't, the thread might wait on the paint enabled event AFTER + // EnablePainting gets called, and if that happens, then the thread will + // never get destructed. This will only ever happen in the vstest test runner, + // which is what CI uses. + Sleep(500); + + pThread->EnablePainting(); + pRenderer->TriggerTeardown(); + pRenderer.reset(); + } +} + +void VtIoTests::RendererDtorAndThreadAndDx() +{ + Log::Comment(NoThrowString().Format( + L"Test deleting a Renderer a bunch of times" + )); + + for (int i = 0; i < 16; ++i) + { + auto thread = std::make_unique(); + auto* pThread = thread.get(); + auto pRenderer = std::make_unique(nullptr, nullptr, 0, std::move(thread)); + VERIFY_SUCCEEDED(pThread->Initialize(pRenderer.get())); + + auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); + pRenderer->AddRenderEngine(dxEngine.get()); + // Sleep for a hot sec to make sure the thread starts before we enable painting + // If you don't, the thread might wait on the paint enabled event AFTER + // EnablePainting gets called, and if that happens, then the thread will + // never get destructed. This will only ever happen in the vstest test runner, + // which is what CI uses. + Sleep(500); + + pThread->EnablePainting(); + pRenderer->TriggerTeardown(); + pRenderer.reset(); + } +} + +void VtIoTests::BasicAnonymousPipeOpeningWithSignalChannelTest() +{ + Log::Comment(L"Test using anonymous pipes for the input and adding a signal channel."); + + Log::Comment(L"\tcreating pipes"); + + wil::unique_handle inPipeReadSide; + wil::unique_handle inPipeWriteSide; + wil::unique_handle outPipeReadSide; + wil::unique_handle outPipeWriteSide; + wil::unique_handle signalPipeReadSide; + wil::unique_handle signalPipeWriteSide; + + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&inPipeReadSide, &inPipeWriteSide, nullptr, 0), L"Create anonymous in pipe."); + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&outPipeReadSide, &outPipeWriteSide, nullptr, 0), L"Create anonymous out pipe."); + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&signalPipeReadSide, &signalPipeWriteSide, nullptr, 0), L"Create anonymous signal pipe."); + + Log::Comment(L"\tinitializing vtio"); + + VtIo vtio; + VERIFY_IS_FALSE(vtio.IsUsingVt()); + VERIFY_ARE_EQUAL(nullptr, vtio._pPtySignalInputThread); + VERIFY_SUCCEEDED(vtio._Initialize(inPipeReadSide.release(), outPipeWriteSide.release(), L"", signalPipeReadSide.release())); + VERIFY_SUCCEEDED(vtio.CreateAndStartSignalThread()); + VERIFY_SUCCEEDED(vtio.CreateIoHandlers()); + VERIFY_IS_TRUE(vtio.IsUsingVt()); + VERIFY_ARE_NOT_EQUAL(nullptr, vtio._pPtySignalInputThread); +} diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp new file mode 100644 index 000000000..99cd16f0f --- /dev/null +++ b/src/host/ut_host/VtRendererTests.cpp @@ -0,0 +1,1340 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "../../inc/consoletaeftemplates.hpp" +#include "../../types/inc/Viewport.hpp" + +#include "../../renderer/vt/Xterm256Engine.hpp" +#include "../../renderer/vt/XtermEngine.hpp" +#include "../../renderer/vt/WinTelnetEngine.hpp" +#include "../Settings.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace Render + { + class VtRendererTest; + }; + }; +}; +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +COLORREF g_ColorTable[COLOR_TABLE_SIZE]; +static const std::string CLEAR_SCREEN = "\x1b[2J"; +static const std::string CURSOR_HOME = "\x1b[H"; +// Sometimes when we're expecting the renderengine to not write anything, +// we'll add this to the expected input, and manually write this to the callback +// to make sure nothing else gets written. +// We don't use null because that will confuse the VERIFY macros re: string length. +const char* const EMPTY_CALLBACK_SENTINEL = "\xff"; + + +class VtRenderTestColorProvider : public Microsoft::Console::IDefaultColorProvider +{ +public: + virtual ~VtRenderTestColorProvider() = default; + + COLORREF GetDefaultForeground() const + { + return g_ColorTable[15]; + } + COLORREF GetDefaultBackground() const + { + return g_ColorTable[0]; + } +}; + +VtRenderTestColorProvider p; + +class Microsoft::Console::Render::VtRendererTest +{ + TEST_CLASS(VtRendererTest); + + TEST_CLASS_SETUP(ClassSetup) + { + g_ColorTable[0] = RGB( 12, 12, 12); // Black + g_ColorTable[1] = RGB( 0, 55, 218); // Dark Blue + g_ColorTable[2] = RGB( 19, 161, 14); // Dark Green + g_ColorTable[3] = RGB( 58, 150, 221); // Dark Cyan + g_ColorTable[4] = RGB(197, 15, 31); // Dark Red + g_ColorTable[5] = RGB(136, 23, 152); // Dark Magenta + g_ColorTable[6] = RGB(193, 156, 0); // Dark Yellow + g_ColorTable[7] = RGB(204, 204, 204); // Dark White + g_ColorTable[8] = RGB(118, 118, 118); // Bright Black + g_ColorTable[9] = RGB( 59, 120, 255); // Bright Blue + g_ColorTable[10] = RGB( 22, 198, 12); // Bright Green + g_ColorTable[11] = RGB( 97, 214, 214); // Bright Cyan + g_ColorTable[12] = RGB(231, 72, 86); // Bright Red + g_ColorTable[13] = RGB(180, 0, 158); // Bright Magenta + g_ColorTable[14] = RGB(249, 241, 165); // Bright Yellow + g_ColorTable[15] = RGB(242, 242, 242); // White + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + return true; + } + + // Defining a TEST_METHOD_CLEANUP seemed to break x86 test pass. Not sure why, + // something about the clipboard tests and + // YOU_CAN_ONLY_DESIGNATE_ONE_CLASS_METHOD_TO_BE_A_TEST_METHOD_SETUP_METHOD + // It's probably more correct to leave it out anyways. + + TEST_METHOD(VtSequenceHelperTests); + + TEST_METHOD(Xterm256TestInvalidate); + TEST_METHOD(Xterm256TestColors); + TEST_METHOD(Xterm256TestCursor); + + TEST_METHOD(XtermTestInvalidate); + TEST_METHOD(XtermTestColors); + TEST_METHOD(XtermTestCursor); + + TEST_METHOD(WinTelnetTestInvalidate); + TEST_METHOD(WinTelnetTestColors); + TEST_METHOD(WinTelnetTestCursor); + + TEST_METHOD(TestWrapping); + + TEST_METHOD(TestResize); + + void Test16Colors(VtEngine* engine); + + std::deque qExpectedInput; + bool WriteCallback(const char* const pch, size_t const cch); + void TestPaint(VtEngine& engine, std::function pfn); + void TestPaintXterm(XtermEngine& engine, std::function pfn); + Viewport SetUpViewport(); +}; + +Viewport VtRendererTest::SetUpViewport() +{ + SMALL_RECT view = {}; + view.Top = view.Left = 0; + view.Bottom = 31; + view.Right = 79; + + return Viewport::FromInclusive(view); +} + +bool VtRendererTest::WriteCallback(const char* const pch, size_t const cch) +{ + std::string actualString = std::string(pch, cch); + VERIFY_IS_GREATER_THAN(qExpectedInput.size(), static_cast(0), + NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", actualString.c_str(), qExpectedInput.size())); + + std::string first = qExpectedInput.front(); + qExpectedInput.pop_front(); + + Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", first.c_str())); + Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", actualString.c_str())); + + VERIFY_ARE_EQUAL(first.length(), cch); + VERIFY_ARE_EQUAL(first, actualString); + + return true; +} + +// Function Description: +// - Small helper to do a series of testing wrapped by StartPaint/EndPaint calls +// Arguments: +// - engine: the engine to operate on +// - pfn: A function pointer to some test code to run. +// Return Value: +// - +void VtRendererTest::TestPaint(VtEngine& engine, std::function pfn) +{ + VERIFY_SUCCEEDED(engine.StartPaint()); + pfn(); + VERIFY_SUCCEEDED(engine.EndPaint()); +} + +// Function Description: +// - Small helper to do a series of testing wrapped by StartPaint/EndPaint calls +// Also expects \x1b[?25l and \x1b[?25h on start/stop, for cursor visibility +// Arguments: +// - engine: the engine to operate on +// - pfn: A function pointer to some test code to run. +// Return Value: +// - +void VtRendererTest::TestPaintXterm(XtermEngine& engine, std::function pfn) +{ + + HRESULT hr = engine.StartPaint(); + pfn(); + // If we didn't have anything to do on this frame, still execute our + // callback, but don't check for the following ?25h + if (hr != S_FALSE) + { + // If the engine has decided that it needs to disble the cursor, it'll + // insert ?25l to the front of the buffer (which won't hit this + // callback) and write ?25h to the end of the frame + if (engine._needToDisableCursor) + { + qExpectedInput.push_back("\x1b[?25h"); + } + } + + VERIFY_SUCCEEDED(engine.EndPaint()); + + VERIFY_ARE_EQUAL(qExpectedInput.size(), static_cast(0), + L"Done painting, there shouldn't be any output we're still expecting"); +} + +void VtRendererTest::VtSequenceHelperTests() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + + engine->SetTestCallback(pfn); + + qExpectedInput.push_back("\x1b[?12l"); + VERIFY_SUCCEEDED(engine->_StopCursorBlinking()); + + qExpectedInput.push_back("\x1b[?12h"); + VERIFY_SUCCEEDED(engine->_StartCursorBlinking()); + + qExpectedInput.push_back("\x1b[?25l"); + VERIFY_SUCCEEDED(engine->_HideCursor()); + + qExpectedInput.push_back("\x1b[?25h"); + VERIFY_SUCCEEDED(engine->_ShowCursor()); + + qExpectedInput.push_back("\x1b[K"); + VERIFY_SUCCEEDED(engine->_EraseLine()); + + qExpectedInput.push_back("\x1b[M"); + VERIFY_SUCCEEDED(engine->_DeleteLine(1)); + + qExpectedInput.push_back("\x1b[2M"); + VERIFY_SUCCEEDED(engine->_DeleteLine(2)); + + qExpectedInput.push_back("\x1b[L"); + VERIFY_SUCCEEDED(engine->_InsertLine(1)); + + qExpectedInput.push_back("\x1b[2L"); + VERIFY_SUCCEEDED(engine->_InsertLine(2)); + + qExpectedInput.push_back("\x1b[2X"); + VERIFY_SUCCEEDED(engine->_EraseCharacter(2)); + + qExpectedInput.push_back("\x1b[2;3H"); + VERIFY_SUCCEEDED(engine->_CursorPosition({2, 1})); + + qExpectedInput.push_back("\x1b[1;1H"); + VERIFY_SUCCEEDED(engine->_CursorPosition({0, 0})); + + qExpectedInput.push_back("\x1b[H"); + VERIFY_SUCCEEDED(engine->_CursorHome()); + + qExpectedInput.push_back("\x1b[8;32;80t"); + VERIFY_SUCCEEDED(engine->_ResizeWindow(80, 32)); + + qExpectedInput.push_back("\x1b[2J"); + VERIFY_SUCCEEDED(engine->_ClearScreen()); + + qExpectedInput.push_back("\x1b[10C"); + VERIFY_SUCCEEDED(engine->_CursorForward(10)); +} + +void VtRendererTest::Xterm256TestInvalidate() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating all invalidates the whole viewport." + )); + VERIFY_SUCCEEDED(engine->InvalidateAll()); + qExpectedInput.push_back("\x1b[2J"); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating anything only invalidates that portion" + )); + SMALL_RECT invalid = {1, 1, 1, 1}; + VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences" + )); + COORD scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one down, only top line is invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 1; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + qExpectedInput.push_back("\x1b[H"); // Go Home + qExpectedInput.push_back("\x1b[L"); // insert a line + + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, 3}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three down, only top 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // We would expect a CUP here, but the cursor is already at the home position + qExpectedInput.push_back("\x1b[3L"); // insert 3 lines + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one up, only bottom line is invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Top = invalid.Bottom - 1; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer + qExpectedInput.push_back("\n"); // Scroll down once + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, -3}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three up, only bottom 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Top = invalid.Bottom - 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + // We would expect a CUP here, but we're already at the bottom from the last call. + qExpectedInput.push_back("\n\n\n"); // Scroll down three times + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + Log::Comment(NoThrowString().Format( + L"Multiple scrolls are coalesced" + )); + + scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + scrollDelta = {0, 2}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three down, only top 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + qExpectedInput.push_back("\x1b[H"); // Go to home + qExpectedInput.push_back("\x1b[3L"); // insert 3 lines + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + Log::Comment(NoThrowString().Format( + VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()) + )); + + scrollDelta = {0, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + Log::Comment(NoThrowString().Format( + VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()) + )); + + qExpectedInput.push_back("\x1b[2J"); + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one down and one up, nothing should change ----" + L" But it still does for now MSFT:14169294" + )); + invalid = view.ToExclusive(); + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); +} + +void VtRendererTest::Xterm256TestColors() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test changing the text attributes" + )); + + Log::Comment(NoThrowString().Format( + L"Begin by setting some test values - FG,BG = (1,2,3), (4,5,6) to start" + L"These values were picked for ease of formatting raw COLORREF values." + )); + qExpectedInput.push_back("\x1b[38;2;1;2;3m"); + qExpectedInput.push_back("\x1b[48;2;5;6;7m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201, 0x00070605, 0, false, false)); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"----Change only the BG----" + )); + qExpectedInput.push_back("\x1b[48;2;7;8;9m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x00030201, 0x00090807, 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the FG----" + )); + qExpectedInput.push_back("\x1b[38;2;10;11;12m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a, 0x00090807, 0, false, false)); + + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(0x000c0b0a, 0x00090807, 0, false, false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + + }); + + // Now also do the body of the 16color test as well. + // The only change is that the "Change only the BG to something not in the table" + // test actually uses an RGB value instead of the closest match. + + Log::Comment(NoThrowString().Format( + L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK" + )); + + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"----Change only the BG----" + )); + qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the FG----" + )); + qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to something not in the table----" + )); + qExpectedInput.push_back("\x1b[48;2;1;1;1m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to the 'Default' background----" + )); + qExpectedInput.push_back("\x1b[49m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[0], 0, false, false)); + + + Log::Comment(NoThrowString().Format( + L"----Back to defaults----" + )); + + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + + }); +} + +void VtRendererTest::Xterm256TestCursor() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test moving the cursor around. Every sequence should have both params to CUP explicitly." + )); + TestPaint(*engine, [&]() + { + qExpectedInput.push_back("\x1b[2;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 1})); + + Log::Comment(NoThrowString().Format( + L"----Only move Y coord----" + )); + qExpectedInput.push_back("\x1b[31;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 30})); + + Log::Comment(NoThrowString().Format( + L"----Only move X coord----" + )); + qExpectedInput.push_back("\x1b[29C"); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + + Log::Comment(NoThrowString().Format( + L"----Sending the same move sends nothing----" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + Log::Comment(NoThrowString().Format( + L"----moving home sends a simple sequence----" + )); + qExpectedInput.push_back("\x1b[H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 0})); + + Log::Comment(NoThrowString().Format( + L"----move into the line to test some other sequences----" + )); + qExpectedInput.push_back("\x1b[7C"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 0})); + + Log::Comment(NoThrowString().Format( + L"----move down one line (x stays the same)----" + )); + qExpectedInput.push_back("\n"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 1})); + + Log::Comment(NoThrowString().Format( + L"----move to the start of the next line----" + )); + qExpectedInput.push_back("\r\n"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 2})); + + Log::Comment(NoThrowString().Format( + L"----move into the line to test some other sequnces----" + )); + qExpectedInput.push_back("\x1b[2;8H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 1})); + + Log::Comment(NoThrowString().Format( + L"----move to the start of this line (y stays the same)----" + )); + qExpectedInput.push_back("\r"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 1})); + + qExpectedInput.push_back("\x1b[?25h"); + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + L"The cursor's last \"real\" position was 0,0" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + Log::Comment(NoThrowString().Format( + L"Paint some text at 0,0, then try moving the cursor to where it currently is." + )); + qExpectedInput.push_back("\x1b[1C"); + qExpectedInput.push_back("asdfghjkl"); + + const wchar_t* const line = L"asdfghjkl"; + const unsigned char rgWidths[] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + + std::vector clusters; + for (size_t i = 0; i < wcslen(line); i++) + { + clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast(rgWidths[i])); + } + + VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false)); + + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + }); + + // Note that only PaintBufferLine updates the "Real" cursor position, which + // the cursor is moved back to at the end of each paint + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + }); +} + +void VtRendererTest::XtermTestInvalidate() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE), false); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating all invalidates the whole viewport." + )); + VERIFY_SUCCEEDED(engine->InvalidateAll()); + qExpectedInput.push_back("\x1b[2J"); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating anything only invalidates that portion" + )); + SMALL_RECT invalid = {1, 1, 1, 1}; + VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); + TestPaintXterm(*engine, [&]() + { + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences" + )); + COORD scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one down, only top line is invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 1; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + qExpectedInput.push_back("\x1b[H"); // Go Home + qExpectedInput.push_back("\x1b[L"); // insert a line + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, 3}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three down, only top 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // We would expect a CUP here, but the cursor is already at the home position + qExpectedInput.push_back("\x1b[3L"); // insert 3 lines + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one up, only bottom line is invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Top = invalid.Bottom - 1; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer + qExpectedInput.push_back("\n"); // Scroll down once + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, -3}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three up, only bottom 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Top = invalid.Bottom - 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + // We would expect a CUP here, but we're already at the bottom from the last call. + qExpectedInput.push_back("\n\n\n"); // Scroll down three times + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + Log::Comment(NoThrowString().Format( + L"Multiple scrolls are coalesced" + )); + + scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + scrollDelta = {0, 2}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled three down, only top 3 lines are invalid. ----" + )); + invalid = view.ToExclusive(); + invalid.Bottom = 3; + + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + qExpectedInput.push_back("\x1b[H"); // Go to home + qExpectedInput.push_back("\x1b[3L"); // insert 3 lines + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); + + scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + Log::Comment(NoThrowString().Format( + VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()) + )); + + scrollDelta = {0, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + Log::Comment(NoThrowString().Format( + VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()) + )); + + qExpectedInput.push_back("\x1b[2J"); + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"---- Scrolled one down and one up, nothing should change ----" + L" But it still does for now MSFT:14169294" + )); + invalid = view.ToExclusive(); + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + + VERIFY_SUCCEEDED(engine->ScrollFrame()); + }); +} + +void VtRendererTest::XtermTestColors() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE), false); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test changing the text attributes" + )); + + Log::Comment(NoThrowString().Format( + L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK" + )); + + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"----Change only the BG----" + )); + qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the FG----" + )); + qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to something not in the table----" + )); + qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to the 'Default' background----" + )); + qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[0], 0, false, false)); + + + Log::Comment(NoThrowString().Format( + L"----Back to defaults----" + )); + + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + +} + +void VtRendererTest::XtermTestCursor() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE), false); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test moving the cursor around. Every sequence should have both params to CUP explicitly." + )); + TestPaint(*engine, [&]() + { + qExpectedInput.push_back("\x1b[2;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 1})); + + Log::Comment(NoThrowString().Format( + L"----Only move Y coord----" + )); + qExpectedInput.push_back("\x1b[31;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 30})); + + Log::Comment(NoThrowString().Format( + L"----Only move X coord----" + )); + qExpectedInput.push_back("\x1b[29C"); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + + Log::Comment(NoThrowString().Format( + L"----Sending the same move sends nothing----" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + Log::Comment(NoThrowString().Format( + L"----moving home sends a simple sequence----" + )); + qExpectedInput.push_back("\x1b[H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 0})); + + Log::Comment(NoThrowString().Format( + L"----move into the line to test some other sequences----" + )); + qExpectedInput.push_back("\x1b[7C"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 0})); + + Log::Comment(NoThrowString().Format( + L"----move down one line (x stays the same)----" + )); + qExpectedInput.push_back("\n"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 1})); + + Log::Comment(NoThrowString().Format( + L"----move to the start of the next line----" + )); + qExpectedInput.push_back("\r\n"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 2})); + + Log::Comment(NoThrowString().Format( + L"----move into the line to test some other sequnces----" + )); + qExpectedInput.push_back("\x1b[2;8H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({7, 1})); + + Log::Comment(NoThrowString().Format( + L"----move to the start of this line (y stays the same)----" + )); + qExpectedInput.push_back("\r"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 1})); + + qExpectedInput.push_back("\x1b[?25h"); + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + L"The cursor's last \"real\" position was 0,0" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({0,1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + Log::Comment(NoThrowString().Format( + L"Paint some text at 0,0, then try moving the cursor to where it currently is." + )); + qExpectedInput.push_back("\x1b[1C"); + qExpectedInput.push_back("asdfghjkl"); + + const wchar_t* const line = L"asdfghjkl"; + const unsigned char rgWidths[] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + + std::vector clusters; + for (size_t i = 0; i < wcslen(line); i++) + { + clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast(rgWidths[i])); + } + + VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false)); + + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + }); + + // Note that only PaintBufferLine updates the "Real" cursor position, which + // the cursor is moved back to at the end of each paint + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + }); + +} + +void VtRendererTest::WinTelnetTestInvalidate() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating all invalidates the whole viewport." + )); + VERIFY_SUCCEEDED(engine->InvalidateAll()); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that invalidating anything only invalidates that portion" + )); + SMALL_RECT invalid = {1, 1, 1, 1}; + VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + }); + + Log::Comment(NoThrowString().Format( + L"Make sure that scrolling invalidates the whole viewport, and sends no VT sequences" + )); + COORD scrollDelta = {0, 1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); // sentinel + VERIFY_SUCCEEDED(engine->ScrollFrame()); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + + scrollDelta = {0, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->ScrollFrame()); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + + scrollDelta = {1, 0}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->ScrollFrame()); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + + scrollDelta = {-1, 0}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->ScrollFrame()); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + + scrollDelta = {1, -1}; + VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + TestPaint(*engine, [&]() + { + VERIFY_ARE_EQUAL(view, engine->_invalidRect); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->ScrollFrame()); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + }); + +} + +void VtRendererTest::WinTelnetTestColors() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test changing the text attributes" + )); + + Log::Comment(NoThrowString().Format( + L"Begin by setting the default colors - FG,BG = BRIGHT_WHITE,DARK_BLACK" + )); + + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"----Change only the BG----" + )); + qExpectedInput.push_back("\x1b[41m"); // Background DARK_RED + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the FG----" + )); + qExpectedInput.push_back("\x1b[37m"); // Foreground DARK_WHITE + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[4], 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to something not in the table----" + )); + qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], 0x010101, 0, false, false)); + + Log::Comment(NoThrowString().Format( + L"----Change only the BG to the 'Default' background----" + )); + qExpectedInput.push_back("\x1b[40m"); // Background DARK_BLACK + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[7], g_ColorTable[0], 0, false, false)); + + + Log::Comment(NoThrowString().Format( + L"----Back to defaults----" + )); + qExpectedInput.push_back("\x1b[m"); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Make sure that color setting persists across EndPaint/StartPaint" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->UpdateDrawingBrushes(g_ColorTable[15], g_ColorTable[0], 0, false, false)); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback + + }); +} + +void VtRendererTest::WinTelnetTestCursor() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + Viewport view = SetUpViewport(); + + Log::Comment(NoThrowString().Format( + L"Test moving the cursor around. Every sequence should have both params to CUP explicitly." + )); + TestPaint(*engine, [&]() + { + qExpectedInput.push_back("\x1b[2;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 1})); + + Log::Comment(NoThrowString().Format( + L"----Only move X coord----" + )); + qExpectedInput.push_back("\x1b[31;2H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({1, 30})); + + Log::Comment(NoThrowString().Format( + L"----Only move Y coord----" + )); + qExpectedInput.push_back("\x1b[31;31H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + + Log::Comment(NoThrowString().Format( + L"----Sending the same move sends nothing----" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({30, 30})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + // The "real" location is the last place the cursor was moved to not + // during the course of VT operations - eg the last place text was written, + // or the cursor was manually painted at (MSFT 13310327) + Log::Comment(NoThrowString().Format( + L"Make sure the cursor gets moved back to the last real location it was at" + )); + qExpectedInput.push_back("\x1b[1;1H"); + // EndPaint will send this sequence for us. + }); + + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + L"The cursor's last \"real\" position was 0,0" + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 0})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + Log::Comment(NoThrowString().Format( + L"Paint some text at 0,0, then try moving the cursor to where it currently is." + )); + qExpectedInput.push_back("\x1b[2;2H"); + qExpectedInput.push_back("asdfghjkl"); + + const wchar_t* const line = L"asdfghjkl"; + const unsigned char rgWidths[] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + + std::vector clusters; + for (size_t i = 0; i < wcslen(line); i++) + { + clusters.emplace_back(std::wstring_view{ &line[i], 1 }, static_cast(rgWidths[i])); + } + + VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters.data(), clusters.size() }, { 1, 1 }, false)); + + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + + }); + + // Note that only PaintBufferLine updates the "Real" cursor position, which + // the cursor is moved back to at the end of each paint + TestPaint(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Sending the same move across paint calls sends nothing." + )); + qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); + VERIFY_SUCCEEDED(engine->_MoveCursor({10, 1})); + WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); + }); +} + +void VtRendererTest::TestWrapping() +{ + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + std::unique_ptr engine = std::make_unique(std::move(hFile), p, SetUpViewport(), g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + }); + + Viewport view = SetUpViewport(); + + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Make sure the cursor is at 0,0" + )); + qExpectedInput.push_back("\x1b[H"); + VERIFY_SUCCEEDED(engine->_MoveCursor({0, 0})); + }); + + TestPaintXterm(*engine, [&]() + { + Log::Comment(NoThrowString().Format( + L"Painting a line that wrapped, then painting another line, and " + L"making sure we don't manually move the cursor between those paints." + )); + qExpectedInput.push_back("asdfghjkl"); + // TODO: Undoing this behavior due to 18123777. Will come back in MSFT:16485846 + qExpectedInput.push_back("\r\n"); + qExpectedInput.push_back("zxcvbnm,."); + + const wchar_t* const line1 = L"asdfghjkl"; + const wchar_t* const line2 = L"zxcvbnm,."; + const unsigned char rgWidths[] = {1, 1, 1, 1, 1, 1, 1, 1, 1}; + + std::vector clusters1; + for (size_t i = 0; i < wcslen(line1); i++) + { + clusters1.emplace_back(std::wstring_view{ &line1[i], 1 }, static_cast(rgWidths[i])); + } + std::vector clusters2; + for (size_t i = 0; i < wcslen(line2); i++) + { + clusters2.emplace_back(std::wstring_view{ &line2[i], 1 }, static_cast(rgWidths[i])); + } + + VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters1.data(), clusters1.size() }, { 0, 0 }, false)); + VERIFY_SUCCEEDED(engine->PaintBufferLine({ clusters2.data(), clusters2.size() }, { 0, 1 }, false)); + + }); +} + +void VtRendererTest::TestResize() +{ + Viewport view = SetUpViewport(); + wil::unique_hfile hFile = wil::unique_hfile(INVALID_HANDLE_VALUE); + auto engine = std::make_unique(std::move(hFile), p, view, g_ColorTable, static_cast(COLOR_TABLE_SIZE)); + auto pfn = std::bind(&VtRendererTest::WriteCallback, this, std::placeholders::_1, std::placeholders::_2); + engine->SetTestCallback(pfn); + + // Verify the first paint emits a clear and go home + qExpectedInput.push_back("\x1b[2J"); + VERIFY_IS_TRUE(engine->_firstPaint); + VERIFY_IS_TRUE(engine->_suppressResizeRepaint); + + // The renderer (in Renderer@_PaintFrameForEngine..._CheckViewportAndScroll) + // will manually call UpdateViewport once before actually painting the + // first frame. Replicate that behavior here + VERIFY_SUCCEEDED(engine->UpdateViewport(view.ToInclusive())); + + TestPaint(*engine, [&]() { + VERIFY_IS_FALSE(engine->_firstPaint); + VERIFY_IS_FALSE(engine->_suppressResizeRepaint); + }); + + // Resize the viewport to 120x30 + // Everything should be invalidated, and a resize message sent. + const auto newView = Viewport::FromDimensions({0, 0}, {120, 30}); + qExpectedInput.push_back("\x1b[8;30;120t"); + + VERIFY_SUCCEEDED(engine->UpdateViewport(newView.ToInclusive())); + + TestPaintXterm(*engine, [&]() { + VERIFY_ARE_EQUAL(newView, engine->_invalidRect); + VERIFY_IS_FALSE(engine->_firstPaint); + VERIFY_IS_FALSE(engine->_suppressResizeRepaint); + }); + + +} diff --git a/src/host/ut_host/product.pbxproj b/src/host/ut_host/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/host/ut_host/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/host/ut_host/sources b/src/host/ut_host/sources new file mode 100644 index 000000000..416c5ee6a --- /dev/null +++ b/src/host/ut_host/sources @@ -0,0 +1,72 @@ +!include ..\sources.test.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.Host.UnitTests +TARGETTYPE = DYNLINK +DLLDEF = + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + ApiRoutinesTests.cpp \ + AliasTests.cpp \ + SearchTests.cpp \ + HistoryTests.cpp \ + UtilsTests.cpp \ + AttrRowTests.cpp \ + ConsoleArgumentsTests.cpp \ + CodepointWidthDetectorTests.cpp \ + DbcsTests.cpp \ + ScreenBufferTests.cpp \ + TextBufferIteratorTests.cpp \ + TextBufferTests.cpp \ + ClipboardTests.cpp \ + SelectionTests.cpp \ + Utf8ToWideCharParserTests.cpp \ + Utf16ParserTests.cpp \ + OutputCellIteratorTests.cpp \ + InitTests.cpp \ + TitleTests.cpp \ + InputBufferTests.cpp \ + VtIoTests.cpp \ + VtRendererTests.cpp \ + ViewportTests.cpp \ + ConsoleArgumentsTests.cpp \ + CommandLineTests.cpp \ + CommandListPopupTests.cpp \ + CommandNumberPopupTests.cpp \ + CopyFromCharPopupTests.cpp \ + CopyToCharPopupTests.cpp \ + DefaultResource.rc \ + + +INCLUDES = \ + $(INCLUDES); \ + ..\..\inc\test; \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + +# prepend the ConRenderVt.Unittest.lib, so that it's linked before the non-ut version. + +TARGETLIBS = \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\vt\ut_lib\$(O)\ConRenderVt.Unittest.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\host\ut_lib\$(O)\ConhostV2.Unittest.lib \ + $(TARGETLIBS) \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ + + + +# ------------------------------------- +# Localization +# ------------------------------------- + +# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe. +C_DEFINES = $(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)""" +MUI_VERIFY_NO_LOC_RESOURCE = 1 diff --git a/src/host/ut_host/sources.dep b/src/host/ut_host/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/host/ut_host/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/host/ut_host/testmd.definition b/src/host/ut_host/testmd.definition new file mode 100644 index 000000000..0c2cb74a8 --- /dev/null +++ b/src/host/ut_host/testmd.definition @@ -0,0 +1,18 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "Host-UnitTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ ] + }, + "Logs": [ ], + "Plugins": [ ] +} \ No newline at end of file diff --git a/src/host/ut_lib/host.unittest.vcxproj b/src/host/ut_lib/host.unittest.vcxproj new file mode 100644 index 000000000..bc9994c39 --- /dev/null +++ b/src/host/ut_lib/host.unittest.vcxproj @@ -0,0 +1,25 @@ + + + + + + + + INLINE_TEST_METHOD_MARKUP;UNIT_TESTING;%(PreprocessorDefinitions) + + + + StaticLibrary + + + {06EC74CB-9A12-429C-B551-8562EC954747} + Win32Proj + hostlib.unittest + Host.unittest + ConhostV2Lib.unittest + + + + + + diff --git a/src/host/ut_lib/sources b/src/host/ut_lib/sources new file mode 100644 index 000000000..026819d83 --- /dev/null +++ b/src/host/ut_lib/sources @@ -0,0 +1,23 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConhostV2.Unittest +TARGETTYPE = LIBRARY +UMTYPE = windows + + +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +INCLUDES = \ + $(INCLUDES); \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + diff --git a/src/host/utf8ToWideCharParser.cpp b/src/host/utf8ToWideCharParser.cpp new file mode 100644 index 000000000..3518300af --- /dev/null +++ b/src/host/utf8ToWideCharParser.cpp @@ -0,0 +1,509 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "utf8ToWideCharParser.hpp" +#include + +#ifndef WIL_ENABLE_EXCEPTIONS +#error WIL exception helpers must be enabled +#endif + +#define IsBitSet WI_IsFlagSet + +const byte NonAsciiBytePrefix = 0x80; + +const byte ContinuationByteMask = 0xC0; +const byte ContinuationBytePrefix = 0x80; + +const byte MostSignificantBitMask = 0x80; + +// Routine Description: +// - Constructs an instance of the parser. +// Arguments: +// - codePage - Starting code page to interpret input with. +// Return Value: +// - A new instance of the parser. +Utf8ToWideCharParser::Utf8ToWideCharParser(const unsigned int codePage) : + _currentCodePage { codePage }, + _bytesStored { 0 }, + _currentState { _State::Ready }, + _convertedWideChars { nullptr } +{ + std::fill_n(_utf8CodePointPieces, _UTF8_BYTE_SEQUENCE_MAX, 0ui8); +} + +// Routine Description: +// - Set the code page that input sequences will correspond to. Clears +// any saved partial multi-byte sequences if the code page changes +// from the code page the partial sequence is associated with. +// Arguments: +// - codePage - the code page to set to. +// Return Value: +// - +void Utf8ToWideCharParser::SetCodePage(const unsigned int codePage) +{ + if (_currentCodePage != codePage) + { + _currentCodePage = codePage; + // we can't be making any assumptions about the partial + // sequence we were storing now that the codepage has changed + _bytesStored = 0; + _currentState = _State::Ready; + } +} + +// Routine Description: +// - Parses the input multi-byte sequence. +// Arguments: +// - pBytes - The byte sequence to parse. +// - cchBuffer - The amount of bytes in pBytes. This will contain the +// number of wide chars contained by converted after this function is +// run, or 0 if an error occurs (or if pBytes is 0). +// - converted - a valid unique_ptr to store the parsed wide chars +// in. On error this will contain nullptr instead of an array. +// Return Value: +// - +[[nodiscard]] +HRESULT Utf8ToWideCharParser::Parse(_In_reads_(cchBuffer) const byte* const pBytes, + _In_ unsigned int const cchBuffer, + _Out_ unsigned int& cchConsumed, + _Inout_ std::unique_ptr& converted, + _Out_ unsigned int& cchConverted) +{ + cchConsumed = 0; + cchConverted = 0; + + // we can't parse anything if we weren't given any data to parse + if (cchBuffer == 0) + { + return S_OK; + } + // we shouldn't be parsing if the current codepage isn't UTF8 + if (_currentCodePage != CP_UTF8) + { + _currentState = _State::Error; + } + HRESULT hr = S_OK; + try + { + bool loop = true; + unsigned int wideCharCount = 0; + _convertedWideChars.reset(nullptr); + while (loop) + { + switch(_currentState) + { + case _State::Ready: + wideCharCount = _ParseFullRange(pBytes, cchBuffer); + break; + case _State::BeginPartialParse: + wideCharCount = _InvolvedParse(pBytes, cchBuffer); + break; + case _State::Error: + hr = E_FAIL; + _Reset(); + wideCharCount = 0; + loop = false; + break; + case _State::Finished: + _currentState = _State::Ready; + cchConsumed = cchBuffer; + loop = false; + break; + case _State::AwaitingMoreBytes: + _currentState = _State::BeginPartialParse; + cchConsumed = cchBuffer; + loop = false; + break; + default: + _currentState = _State::Error; + break; + } + } + converted.swap(_convertedWideChars); + cchConverted = wideCharCount; + } + catch (...) + { + _Reset(); + hr = wil::ResultFromCaughtException(); + } + return hr; +} + +// Routine Description: +// - Determines if ch is a UTF8 lead byte. See _Utf8SequenceSize() for a +// description of how a lead byte is specified. +// Arguments: +// - ch - The byte to test. +// Return Value: +// - True if ch is a lead byte, false otherwise. +bool Utf8ToWideCharParser::_IsLeadByte(_In_ byte ch) +{ + unsigned int sequenceSize = _Utf8SequenceSize(ch); + return !_IsContinuationByte(ch) && + !_IsAsciiByte(ch) && + sequenceSize > 1 && + sequenceSize <= _UTF8_BYTE_SEQUENCE_MAX; +} + +// Routine Description: +// - Determines if ch is a UTF8 continuation byte. A continuation byte +// takes the form 10xx xxxx, so we need to check that the two most +// significant bits are a 1 followed by a 0. +// Arguments: +// - ch - The byte to test +// Return Value: +// - True if ch is a continuation byte, false otherwise. +bool Utf8ToWideCharParser::_IsContinuationByte(_In_ byte ch) +{ + return (ch & ContinuationByteMask) == ContinuationBytePrefix; +} + +// Routine Description: +// - Determines if ch is an ASCII compatible UTF8 byte. A byte is +// ASCII compatible if the most significant bit is a 0. +// Arguments: +// - ch - The byte to test. +// Return Value: +// - True if ch is an ASCII compatible byte, false otherwise. +bool Utf8ToWideCharParser::_IsAsciiByte(_In_ byte ch) +{ + return !IsBitSet(ch, NonAsciiBytePrefix); +} + +// Routine Description: +// - Determines if the sequence starting at pLeadByte is a valid UTF8 +// multi-byte sequence. Note that a single ASCII byte does not count +// as a valid MULTI-byte sequence. +// Arguments: +// - pLeadByte - The start of a possible sequence. +// - cb - The amount of remaining chars in the array that +// pLeadByte points to. +// Return Value: +// - true if the sequence starting at pLeadByte is a multi-byte +// sequence and uses all of the remaining chars, false otherwise. +bool Utf8ToWideCharParser::_IsValidMultiByteSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb) +{ + if (!_IsLeadByte(*pLeadByte)) + { + return false; + } + const unsigned int sequenceSize = _Utf8SequenceSize(*pLeadByte); + if (sequenceSize > cb) + { + return false; + } + // i starts at 1 so that we skip the lead byte + for (unsigned int i = 1; i < sequenceSize; ++i) + { + const byte ch = *(pLeadByte + i); + if (!_IsContinuationByte(ch)) + { + return false; + } + } + return true; +} + +// Routine Description: +// - Checks if the sequence starting at pLeadByte is a portion of a +// single valid multi-byte sequence. A new sequence must not be +// started within the range provided in order for it to be considered +// a valid partial sequence. +// Arguments: +// - pLeadByte - The start of the possible partial sequence. +// - cb - The amount of remaining chars in the array that +// pLeadByte points to. +// Return Value: +// - true if the sequence is a single partial multi-byte sequence, +// false otherwise. +bool Utf8ToWideCharParser::_IsPartialMultiByteSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb) +{ + if (!_IsLeadByte(*pLeadByte)) + { + return false; + } + const unsigned int sequenceSize = _Utf8SequenceSize(*pLeadByte); + if (sequenceSize <= cb) + { + return false; + } + // i starts at 1 so that we skip the lead byte + for (unsigned int i = 1; i < cb; ++i) + { + const byte ch = *(pLeadByte + i); + if (!_IsContinuationByte(ch)) + { + return false; + } + } + return true; +} + +// Routine Description: +// - Determines the number of bytes in the UTF8 multi-byte sequence. +// Does not perform any verification that ch is a valid lead byte. A +// lead byte indicates how many bytes are in a sequence by repeating a +// 1 for each byte in the sequence, starting with the most significant +// bit, then a 0 directly after. Ex: +// - 110x xxxx = a two byte sequence +// - 1110 xxxx = a three byte sequence +// +// Note that a byte that has a pattern 10xx xxxx is a continuation +// byte and will be reported as a sequence of one by this function. +// +// A sequence is currently a maximum of four bytes but this function +// will just count the number of consecutive 1 bits (starting with the +// most significant bit) so if the byte is malformed (ex. 1111 110x) a +// number larger than the maximum utf8 byte sequence may be +// returned. It is the responsibility of the calling function to check +// this (and the continuation byte scenario) because we don't do any +// verification here. +// Arguments: +// - ch - the lead byte of a UTF8 multi-byte sequence. +// Return Value: +// - The number of bytes (including the lead byte) that ch indicates +// are in the sequence. +unsigned int Utf8ToWideCharParser::_Utf8SequenceSize(_In_ byte ch) +{ + unsigned int msbOnes = 0; + while (IsBitSet(ch, MostSignificantBitMask)) + { + ++msbOnes; + ch <<= 1; + } + return msbOnes; +} + +// Routine Description: +// - Attempts to parse pInputChars by themselves in wide chars, +// without using any saved partial byte sequences. On success, +// _convertedWideChars will contain the converted wide char sequence +// and _currentState will be set to _State::Finished. On failure, +// _currentState will be set to either _State::Error or +// _State::BeginPartialParse. +// Arguments: +// - pInputChars - The byte sequence to convert to wide chars. +// - cb - The amount of bytes in pInputChars. +// Return Value: +// - The amount of wide chars that are stored in _convertedWideChars, +// or 0 if pInputChars cannot be successfully converted. +unsigned int Utf8ToWideCharParser::_ParseFullRange(_In_reads_(cb) const byte* const pInputChars, const unsigned int cb) +{ + int bufferSize = MultiByteToWideChar(_currentCodePage, + MB_ERR_INVALID_CHARS, + reinterpret_cast(pInputChars), + cb, + nullptr, + 0); + if (bufferSize == 0) + { + DWORD err = GetLastError(); + LOG_WIN32(err); + if (err == ERROR_NO_UNICODE_TRANSLATION) + { + _currentState = _State::BeginPartialParse; + } + else + { + _currentState = _State::Error; + } + } + else + { + _convertedWideChars = std::make_unique(bufferSize); + bufferSize = MultiByteToWideChar(_currentCodePage, + 0, + reinterpret_cast(pInputChars), + cb, + _convertedWideChars.get(), + bufferSize); + if (bufferSize == 0) + { + LOG_LAST_ERROR(); + _currentState = _State::Error; + } + _currentState = _State::Finished; + } + return bufferSize; +} + +// Routine Description: +// - Attempts to parse pInputChars in a more complex manner, taking +// into account any previously saved partial byte sequences while +// removing any invalid byte sequences. Will also save a partial byte +// sequence from the end of the sequence if necessary. If the sequence +// can be successfully parsed, _currentState will be set to +// _State::Finished. If more bytes are necessary to form a wide char, +// then _currentState will be set to +// _State::AwaitingMoreBytes. Otherwise, _currentState will be set to +// _State::Error. +// Arguments: +// - pInputChars - The byte sequence to convert to wide chars. +// - cb - The amount of bytes in pInputChars. +// Return Value: +// - The amount of wide chars that are stored in _convertedWideChars, +// or 0 if pInputChars cannot be successfully converted or if the +// parser requires additional bytes before returning a valid wide +// char. +unsigned int Utf8ToWideCharParser::_InvolvedParse(_In_reads_(cb) const byte* const pInputChars, const unsigned int cb) +{ + // Do safe math to add up the count and error if it won't fit. + unsigned int count; + const HRESULT hr = UIntAdd(cb, _bytesStored, &count); + if (FAILED(hr)) + { + LOG_HR(hr); + _currentState = _State::Error; + return 0; + } + + // Allocate space and copy. + std::unique_ptr combinedInputBytes = std::make_unique(count); + std::copy(_utf8CodePointPieces, _utf8CodePointPieces + _bytesStored, combinedInputBytes.get()); + std::copy(pInputChars, pInputChars + cb, combinedInputBytes.get() + _bytesStored); + _bytesStored = 0; + std::pair, unsigned int> validSequence = _RemoveInvalidSequences(combinedInputBytes.get(), count); + // the input may have only been a partial sequence so we need to + // check that there are actually any bytes that we can convert + // right now + if (validSequence.second == 0 && _bytesStored > 0) + { + _currentState = _State::AwaitingMoreBytes; + return 0; + } + int bufferSize = MultiByteToWideChar(_currentCodePage, + MB_ERR_INVALID_CHARS, + reinterpret_cast(validSequence.first.get()), + validSequence.second, + nullptr, + 0); + if (bufferSize == 0) + { + LOG_LAST_ERROR(); + _currentState = _State::Error; + } + else + { + _convertedWideChars = std::make_unique(bufferSize); + bufferSize = MultiByteToWideChar(_currentCodePage, + 0, + reinterpret_cast(validSequence.first.get()), + validSequence.second, + _convertedWideChars.get(), + bufferSize); + if (bufferSize == 0) + { + LOG_LAST_ERROR(); + _currentState = _State::Error; + } + else if (_bytesStored > 0) + { + _currentState = _State::AwaitingMoreBytes; + } + else + { + _currentState = _State::Finished; + } + } + return bufferSize; +} + +// Routine Description: +// - Reads pInputChars byte by byte, removing any invalid UTF8 +// multi-byte sequences. +// Arguments: +// - pInputChars - The byte sequence to fix. +// - cb - The amount of bytes in pInputChars. +// Return Value: +// - A std::pair containing the corrected byte sequence and the number +// of bytes in the sequence. +std::pair, unsigned int> Utf8ToWideCharParser::_RemoveInvalidSequences(_In_reads_(cb) const byte* const pInputChars, const unsigned int cb) +{ + std::unique_ptr validSequence = std::make_unique(cb); + unsigned int validSequenceLocation = 0; // index into validSequence + unsigned int currentByteInput = 0; // index into pInputChars + while (currentByteInput < cb) + { + if (_IsAsciiByte(pInputChars[currentByteInput])) + { + validSequence[validSequenceLocation] = pInputChars[currentByteInput]; + ++validSequenceLocation; + ++currentByteInput; + } + else if (_IsContinuationByte(pInputChars[currentByteInput])) + { + while (currentByteInput < cb && _IsContinuationByte(pInputChars[currentByteInput])) + { + ++currentByteInput; + } + } + else if (_IsLeadByte(pInputChars[currentByteInput])) + { + if (_IsValidMultiByteSequence(&pInputChars[currentByteInput], cb - currentByteInput)) + { + const unsigned int sequenceSize = _Utf8SequenceSize(pInputChars[currentByteInput]); + // min is to guard against static analyis possible buffer overflow + const unsigned int limit = std::min(sequenceSize, cb - currentByteInput); + for (unsigned int i = 0; i < limit; ++i) + { + validSequence[validSequenceLocation] = pInputChars[currentByteInput]; + ++validSequenceLocation; + ++currentByteInput; + } + } + else if (_IsPartialMultiByteSequence(&pInputChars[currentByteInput], cb - currentByteInput)) + { + _StorePartialSequence(&pInputChars[currentByteInput], cb - currentByteInput); + break; + } + else + { + ++currentByteInput; + while (currentByteInput < cb && _IsContinuationByte(pInputChars[currentByteInput])) + { + ++currentByteInput; + } + } + } + else + { + // invalid byte, skip it. + ++currentByteInput; + } + } + return std::make_pair, unsigned int>(std::move(validSequence), std::move(validSequenceLocation)); +} + +// Routine Description: +// - Stores a partial byte sequence for later use. Will overwrite any +// previously saved sequence. Will only store bytes up to the limit +// Utf8ToWideCharParser::_UTF8_BYTE_SEQUENCE_MAX. +// Arguments: +// - pLeadByte - The beginning of the sequence to save. +// - cb - The amount of bytes to save. +// Return Value: +// - +void Utf8ToWideCharParser::_StorePartialSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb) +{ + const unsigned int maxLength = std::min(cb, _UTF8_BYTE_SEQUENCE_MAX); + std::copy(pLeadByte, pLeadByte + maxLength, _utf8CodePointPieces); + _bytesStored = maxLength; +} + +// Routine Description: +// - Resets the state of the parser to that of a newly initialized +// instance. _currentCodePage is not affected. +// Arguments: +// - +// Return Value: +// - +void Utf8ToWideCharParser::_Reset() +{ + _currentState = _State::Ready; + _bytesStored = 0; + _convertedWideChars.release(); +} diff --git a/src/host/utf8ToWideCharParser.hpp b/src/host/utf8ToWideCharParser.hpp new file mode 100644 index 000000000..87669b052 --- /dev/null +++ b/src/host/utf8ToWideCharParser.hpp @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- utf8ToWideCharParser.hpp + +Abstract: +- This transforms a multi-byte character sequence into wide chars +- It will attempt to work around invalid byte sequences +- Partial byte sequences are supported + +Author(s): +- Austin Diviness (AustDi) 16-August-2016 +--*/ + +#pragma once + +class Utf8ToWideCharParser final +{ +public: + Utf8ToWideCharParser(const unsigned int codePage); + void SetCodePage(const unsigned int codePage); + [[nodiscard]] + HRESULT Parse(_In_reads_(cchBuffer) const byte* const pBytes, + _In_ unsigned int const cchBuffer, + _Out_ unsigned int& cchConsumed, + _Inout_ std::unique_ptr& converted, + _Out_ unsigned int& cchConverted); + +private: + enum class _State + { + Ready, // ready for input, no partially parsed code points + Error, // error in parsing given bytes + BeginPartialParse, // not a clean byte sequence, needs involved parsing + AwaitingMoreBytes, // have a partial sequence saved, waiting for the rest of it + Finished // ready to return a wide char sequence + }; + + bool _IsLeadByte(_In_ byte ch); + bool _IsContinuationByte(_In_ byte ch); + bool _IsAsciiByte(_In_ byte ch); + bool _IsValidMultiByteSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb); + bool _IsPartialMultiByteSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb); + unsigned int _Utf8SequenceSize(_In_ byte ch); + unsigned int _ParseFullRange(_In_reads_(cb) const byte* const _InputChars, const unsigned int cb); + unsigned int _InvolvedParse(_In_reads_(cb) const byte* const pInputChars, const unsigned int cb); + std::pair, unsigned int> _RemoveInvalidSequences(_In_reads_(cb) const byte* const pInputChars, + const unsigned int cb); + void _StorePartialSequence(_In_reads_(cb) const byte* const pLeadByte, const unsigned int cb); + void _Reset(); + + static const unsigned int _UTF8_BYTE_SEQUENCE_MAX = 4; + + byte _utf8CodePointPieces[_UTF8_BYTE_SEQUENCE_MAX]; + unsigned int _bytesStored; // bytes stored in utf8CodePointPieces + unsigned int _currentCodePage; + std::unique_ptr _convertedWideChars; + _State _currentState; + +#ifdef UNIT_TESTING + friend class Utf8ToWideCharParserTests; +#endif +}; diff --git a/src/host/utils.cpp b/src/host/utils.cpp new file mode 100644 index 000000000..a9bf93fd0 --- /dev/null +++ b/src/host/utils.cpp @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "utils.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +#include "srvinit.h" + +short CalcWindowSizeX(const SMALL_RECT& rect) noexcept +{ + return rect.Right - rect.Left + 1; +} + +short CalcWindowSizeY(const SMALL_RECT& rect) noexcept +{ + return rect.Bottom - rect.Top + 1; +} + +short CalcCursorYOffsetInPixels(const short sFontSizeY, const ULONG ulSize) noexcept +{ + // TODO: MSFT 10229700 - Note, we want to likely enforce that this isn't negative. + // Pretty sure there's not a valid case for negative offsets here. + return (short)((sFontSizeY)-(ulSize)); +} + +WORD ConvertStringToDec(_In_ PCWSTR pwchToConvert, _Out_opt_ PCWSTR * const ppwchEnd) noexcept +{ + WORD val = 0; + + while (*pwchToConvert != L'\0') + { + WCHAR ch = *pwchToConvert; + if (L'0' <= ch && ch <= L'9') + { + val = (val * 10) + (ch - L'0'); + } + else + { + break; + } + + pwchToConvert++; + } + + if (nullptr != ppwchEnd) + { + *ppwchEnd = pwchToConvert; + } + + return val; +} + + +// Routine Description: +// - Retrieves string resources from our resource files. +// Arguments: +// - id - Resource id from resource.h to the string we need to load. +// Return Value: +// - The string resource +std::wstring _LoadString(const UINT id) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WCHAR ItemString[70]; + size_t ItemLength = 0; + LANGID LangId; + + const NTSTATUS Status = GetConsoleLangId(gci.OutputCP, &LangId); + if (NT_SUCCESS(Status)) + { + ItemLength = s_LoadStringEx(ServiceLocator::LocateGlobals().hInstance, id, ItemString, ARRAYSIZE(ItemString), LangId); + } + if (!NT_SUCCESS(Status) || ItemLength == 0) + { + ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, id, ItemString, ARRAYSIZE(ItemString)); + } + + return std::wstring(ItemString, ItemLength); +} + +// Routine Description: +// - Helper to retrieve string resources from a MUI with a particular LANGID. +// Arguments: +// - hModule - The module related to loading the resource +// - wID - The resource ID number +// - lpBuffer - Buffer to place string data when read. +// - cchBufferMax - Size of buffer +// - wLangId - Language ID of resources that we should retrieve. +UINT s_LoadStringEx(_In_ HINSTANCE hModule, _In_ UINT wID, _Out_writes_(cchBufferMax) LPWSTR lpBuffer, _In_ UINT cchBufferMax, _In_ WORD wLangId) +{ + // Make sure the parms are valid. + if (lpBuffer == nullptr) + { + return 0; + } + + UINT cch = 0; + + // String Tables are broken up into 16 string segments. Find the segment containing the string we are interested in. + HANDLE const hResInfo = FindResourceEx(hModule, RT_STRING, (LPTSTR)((LONG_PTR)(((USHORT)wID >> 4) + 1)), wLangId); + if (hResInfo != nullptr) + { + // Load that segment. + HANDLE const hStringSeg = (HRSRC)LoadResource(hModule, (HRSRC)hResInfo); + + // Lock the resource. + LPTSTR lpsz; + if (hStringSeg != nullptr && (lpsz = (LPTSTR)LockResource(hStringSeg)) != nullptr) + { + // Move past the other strings in this segment. (16 strings in a segment -> & 0x0F) + wID &= 0x0F; + for (;;) + { + cch = *((WCHAR *)lpsz++); // PASCAL like string count + // first WCHAR is count of WCHARs + if (wID-- == 0) + { + break; + } + + lpsz += cch; // Step to start if next string + } + + // chhBufferMax == 0 means return a pointer to the read-only resource buffer. + if (cchBufferMax == 0) + { + *(LPTSTR *)lpBuffer = lpsz; + } + else + { + // Account for the nullptr + cchBufferMax--; + + // Don't copy more than the max allowed. + if (cch > cchBufferMax) + cch = cchBufferMax; + + // Copy the string into the buffer. + memmove(lpBuffer, lpsz, cch * sizeof(WCHAR)); + } + } + } + + // Append a nullptr. + if (cchBufferMax != 0) + { + lpBuffer[cch] = 0; + } + + return cch; +} + +// Routine Description: +// - Compares two coordinate positions to determine whether they're the same, left, or right within the given buffer size +// Arguments: +// - bufferSize - The size of the buffer to use for measurements. +// - coordFirst - The first coordinate position +// - coordSecond - The second coordinate position +// Return Value: +// - Negative if First is to the left of the Second. +// - 0 if First and Second are the same coordinate. +// - Positive if First is to the right of the Second. +// - This is so you can do s_CompareCoords(first, second) <= 0 for "first is left or the same as second". +// (the < looks like a left arrow :D) +// - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom) +int Utils::s_CompareCoords(const COORD bufferSize, const COORD coordFirst, const COORD coordSecond) noexcept +{ + const short cRowWidth = bufferSize.X; + + // Assert that our coordinates are within the expected boundaries + const short cRowHeight = bufferSize.Y; + FAIL_FAST_IF(!(coordFirst.X >= 0 && coordFirst.X < cRowWidth)); + FAIL_FAST_IF(!(coordSecond.X >= 0 && coordSecond.X < cRowWidth)); + FAIL_FAST_IF(!(coordFirst.Y >= 0 && coordFirst.Y < cRowHeight)); + FAIL_FAST_IF(!(coordSecond.Y >= 0 && coordSecond.Y < cRowHeight)); + + // First set the distance vertically + // If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160. + // For the same row, it'll be 0 rows * 80 character width = 0 difference. + int retVal = (coordFirst.Y - coordSecond.Y) * cRowWidth; + + // Now adjust for horizontal differences + // If first is in position 15 and second is in position 30, first is -15 left in relation to 30. + retVal += (coordFirst.X - coordSecond.X); + + // Further notes: + // If we already moved behind one row, this will help correct for when first is right of second. + // For example, with row 4, col 79 and row 5, col 0 as first and second respectively, the distance is -1. + // Assume the row width is 80. + // Step one will set the retVal as -80 as first is one row behind the second. + // Step two will then see that first is 79 - 0 = +79 right of second and add 79 + // The total is -80 + 79 = -1. + return retVal; +} + +// Routine Description: +// - Compares two coordinate positions to determine whether they're the same, left, or right +// Arguments: +// - coordFirst - The first coordinate position +// - coordSecond - The second coordinate position +// Return Value: +// - Negative if First is to the left of the Second. +// - 0 if First and Second are the same coordinate. +// - Positive if First is to the right of the Second. +// - This is so you can do s_CompareCoords(first, second) <= 0 for "first is left or the same as second". +// (the < looks like a left arrow :D) +// - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom) +int Utils::s_CompareCoords(const COORD coordFirst, const COORD coordSecond) noexcept +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // find the width of one row + const COORD coordScreenBufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); + return s_CompareCoords(coordScreenBufferSize, coordFirst, coordSecond); +} + +// Routine Description: +// - Finds the opposite corner given a rectangle and one of its corners. +// - For example, finds the bottom right corner given a rectangle and its top left corner. +// Arguments: +// - srRectangle - The rectangle to check +// - coordCorner - One of the corners of the given rectangle +// Return Value: +// - The opposite corner of the one given. +COORD Utils::s_GetOppositeCorner(const SMALL_RECT srRectangle, const COORD coordCorner) noexcept +{ + // Assert we were given coordinates that are indeed one of the corners of the rectangle. + FAIL_FAST_IF(!(coordCorner.X == srRectangle.Left || coordCorner.X == srRectangle.Right)); + FAIL_FAST_IF(!(coordCorner.Y == srRectangle.Top || coordCorner.Y == srRectangle.Bottom)); + + return { (srRectangle.Left == coordCorner.X) ? srRectangle.Right : srRectangle.Left, + (srRectangle.Top == coordCorner.Y) ? srRectangle.Bottom : srRectangle.Top }; +} diff --git a/src/host/utils.hpp b/src/host/utils.hpp new file mode 100644 index 000000000..79dc82b44 --- /dev/null +++ b/src/host/utils.hpp @@ -0,0 +1,44 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- utils.hpp + +Abstract: +- This moduile contains utility math functions that help perform calculations elsewhere in the console + +Author(s): +- Paul Campbell (PaulCam) 2014 +- Michael Niksa (MiNiksa) 2014 +--*/ +#pragma once + +#include "conapi.h" +#include "server.h" + +#include "..\server\ObjectHandle.h" + +#define RECT_WIDTH(x) ((x)->right - (x)->left) +#define RECT_HEIGHT(x) ((x)->bottom - (x)->top) + +short CalcWindowSizeX(const SMALL_RECT& rect) noexcept; +short CalcWindowSizeY(const SMALL_RECT& rect) noexcept; +short CalcCursorYOffsetInPixels(const short sFontSizeY, const ULONG ulSize) noexcept; +WORD ConvertStringToDec(_In_ PCWSTR pwchToConvert, _Out_opt_ PCWSTR * const ppwchEnd) noexcept; + +std::wstring _LoadString(const UINT id); +static UINT s_LoadStringEx(_In_ HINSTANCE hModule, + _In_ UINT wID, + _Out_writes_(cchBufferMax) LPWSTR lpBuffer, + _In_ UINT cchBufferMax, + _In_ WORD wLangId); + +class Utils +{ +public: + static int s_CompareCoords(const COORD bufferSize, const COORD first, const COORD second) noexcept; + static int s_CompareCoords(const COORD coordFirst, const COORD coordSecond) noexcept; + + static COORD s_GetOppositeCorner(const SMALL_RECT srRectangle, const COORD coordCorner) noexcept; +}; diff --git a/src/host/writeData.cpp b/src/host/writeData.cpp new file mode 100644 index 000000000..2542c7fd8 --- /dev/null +++ b/src/host/writeData.cpp @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "writeData.hpp" + +#include "_stream.h" +#include "..\types\inc\convert.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Routine Description: +// - Creates a new write data object for used in servicing write console requests +// Arguments: +// - siContext - The output buffer to write text data to +// - pwchContext - The string information that the client application sent us to be written +// - cbContext - Byte count of the string above +// - uiOutputCodepage - When the wait is completed, we *might* have to convert the byte count +// back into a specific codepage if the initial call was an A call. +// We need to remember what output codepage was set at the moment in time +// when the write was delayed as it might change by the time it is serviced. +// Return Value: +// - THROW: Throws if space cannot be allocated to copy the given string +WriteData::WriteData(SCREEN_INFORMATION& siContext, + _In_reads_bytes_(cbContext) wchar_t* const pwchContext, + const size_t cbContext, + const UINT uiOutputCodepage) : + IWaitRoutine(ReplyDataType::Write), + _siContext(siContext), + _pwchContext(THROW_IF_NULL_ALLOC(reinterpret_cast(new byte[cbContext]))), + _cbContext(cbContext), + _uiOutputCodepage(uiOutputCodepage), + _fLeadByteCaptured(false), + _fLeadByteConsumed(false), + _cchUtf8Consumed(0) +{ + memmove(_pwchContext, pwchContext, _cbContext); +} + +// Routine Description: +// - Destroys the write data object +// - Frees the string copy we made on creation +WriteData::~WriteData() +{ + if (nullptr != _pwchContext) + { + delete[] _pwchContext; + } +} + +// Routine Description: +// - Stores some additional information about lead byte adjustments from the conversion +// in WriteConsoleA before the real WriteConsole processing (always W) is reached +// so we can restore an accurate A byte count at the very end when the wait is serviced. +// Arguments: +// - fLeadByteCaptured - A lead byte was removed from the string before converted it and saved it. +// We need to report to the original caller that we "wrote" the byte +// even though it is held in escrow for the next call because it was +// the last character in the stream. +// - fLeadByteConsumed - We had a lead byte in escrow from the previous call that we stitched onto the +// front of the input string even though the caller didn't write it in this call. +// We need to report the byte count back to the caller without including this byte +// in the calculation as it wasn't a part of what was given in this exact call. +// Return Value: +// - +void WriteData::SetLeadByteAdjustmentStatus(const bool fLeadByteCaptured, + const bool fLeadByteConsumed) +{ + _fLeadByteCaptured = fLeadByteCaptured; + _fLeadByteConsumed = fLeadByteConsumed; +} + +// Routine Description: +// - For UTF-8 codepages, remembers how many bytes that the UTF-8 parser said it consumed from the input stream. +// This will allow us to give back the correct value after the wait routine Notify services the data later. +// Arguments: +// - cchUtf8Consumed - Count of characters consumed by the UTF-8 parser off the input stream to generate the +// wide character string that is stowed in this object for consumption in the notify routine later. +// Return Value: +// - +void WriteData::SetUtf8ConsumedCharacters(const size_t cchUtf8Consumed) +{ + _cchUtf8Consumed = cchUtf8Consumed; +} + +// Routine Description: +// - Called back at a later time to resume the writing operation when the output object becomes unblocked. +// Arguments: +// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument +// contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0. +// - fIsUnicode - Input data was in UCS-2 unicode or it needs to be converted with the current Output Codepage +// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait) +// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process +// - pControlKeyState - Unused for write operations. Set to 0. +// - pOutputData - not used. +// - true if the wait is done and result buffer/status code can be sent back to the client. +// - false if we need to continue to wait because the output object blocked again +bool WriteData::Notify(const WaitTerminationReason TerminationReason, + const bool fIsUnicode, + _Out_ NTSTATUS* const pReplyStatus, + _Out_ size_t* const pNumBytes, + _Out_ DWORD* const pControlKeyState, + _Out_ void* const /*pOutputData*/) +{ + *pNumBytes = _cbContext; + *pControlKeyState = 0; + + if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying)) + { + *pReplyStatus = STATUS_THREAD_IS_TERMINATING; + return true; + } + + // if we get to here, this routine was called by the input + // thread, which grabs the current console lock. + + // This routine should be called by a thread owning the same lock on the + // same console as we're reading from. + + FAIL_FAST_IF(!(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked())); + + std::unique_ptr waiter; + size_t cbContext = _cbContext; + NTSTATUS Status = DoWriteConsole(_pwchContext, + &cbContext, + _siContext, + waiter); + + if (Status == CONSOLE_STATUS_WAIT) + { + // an extra waiter will be created by DoWriteConsole, but we're already a waiter so discard it. + waiter.reset(); + return false; + } + + // There's extra work to do to correct the byte counts if the original call was an A-version call. + // We always process and hold text in the waiter as W-version text, but the A call is expecting + // a byte value in its own codepage of how much we have written in that codepage. + if (!fIsUnicode) + { + if (CP_UTF8 != _uiOutputCodepage) + { + // At this level with WriteConsole, everything is byte counts, so change back to char counts for + // GetALengthFromW to work correctly. + const size_t cchContext = cbContext / sizeof(wchar_t); + + // For non-UTF-8 codepages, we need to back convert the amount consumed and then + // correlate that with any lead bytes we may have kept for later or reintroduced + // from previous calls. + size_t cchTextBufferRead = 0; + + // Start by counting the number of A bytes we used in printing our W string to the screen. + try + { + cchTextBufferRead = GetALengthFromW(_uiOutputCodepage, { _pwchContext, cchContext }); + } + CATCH_LOG(); + + // If we captured a byte off the string this time around up above, it means we didn't feed + // it into the WriteConsoleW above, and therefore its consumption isn't accounted for + // in the count we just made. Add +1 to compensate. + if (_fLeadByteCaptured) + { + cchTextBufferRead++; + } + + // If we consumed an internally-stored lead byte this time around up above, it means that we + // fed a byte into WriteConsoleW that wasn't a part of this particular call's request. + // We need to -1 to compensate and tell the caller the right number of bytes consumed this request. + if (_fLeadByteConsumed) + { + cchTextBufferRead--; + } + + cbContext = cchTextBufferRead; + } + else + { + // For UTF-8, we were told exactly how many valid bytes were consumed before we got into the wait state. + // Just give that value back now. + cbContext = _cchUtf8Consumed; + } + } + + *pNumBytes = cbContext; + *pReplyStatus = Status; + return true; +} diff --git a/src/host/writeData.hpp b/src/host/writeData.hpp new file mode 100644 index 000000000..0d6871efc --- /dev/null +++ b/src/host/writeData.hpp @@ -0,0 +1,53 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- writeData.hpp + +Abstract: +- This file defines the interface for write data structures. +- This is used not only within the write call, but also to hold context in case a wait condition is required + because writing to the buffer is blocked for some reason. + +Author: +- Michael Niksa (MiNiksa) 9-Mar-2017 + +Revision History: +--*/ + +#pragma once + +#include "../server/IWaitRoutine.h" +#include "../server/WaitTerminationReason.h" + +class WriteData : public IWaitRoutine +{ +public: + WriteData(SCREEN_INFORMATION& siContext, + _In_reads_bytes_(cbContext) wchar_t* const pwchContext, + const size_t cbContext, + const UINT uiOutputCodepage); + ~WriteData(); + + void SetLeadByteAdjustmentStatus(const bool fLeadByteCaptured, + const bool fLeadByteConsumed); + + void SetUtf8ConsumedCharacters(const size_t cchUtf8Consumed); + + 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); + +private: + SCREEN_INFORMATION& _siContext; + wchar_t* const _pwchContext; + size_t const _cbContext; + UINT const _uiOutputCodepage; + bool _fLeadByteCaptured; + bool _fLeadByteConsumed; + size_t _cchUtf8Consumed; +}; diff --git a/src/inc/CppCoreCheck/warnings.h b/src/inc/CppCoreCheck/warnings.h new file mode 100644 index 000000000..5ce8f6ff3 --- /dev/null +++ b/src/inc/CppCoreCheck/warnings.h @@ -0,0 +1,113 @@ +// +// This is a GENERATED file. It should *not* be edited directly. +// Changes should be made to the defectdefs.xml file +// This file contains symbolic constants for warning numbers. +// + + +#pragma once +enum ECppCoreCheckWarningCodes +{ + WARNING_NO_RAW_POINTER_ASSIGNMENT = 26400, // Do not assign the result of an allocation or a function call with an owner return value to a raw pointer, use owner instead (i.11: http://go.microsoft.com/fwlink/?linkid=845474). + WARNING_DONT_DELETE_NON_OWNER = 26401, // Do not delete a raw pointer that is not an owner (i.11: http://go.microsoft.com/fwlink/?linkid=845474). + WARNING_DONT_HEAP_ALLOCATE_MOVABLE_RESULT = 26402, // Return a scoped object instead of a heap-allocated if it has a move constructor (r.3: http://go.microsoft.com/fwlink/?linkid=845471). + WARNING_RESET_OR_DELETE_OWNER = 26403, // Reset or explicitly delete an owner pointer '%1$s' (r.3: http://go.microsoft.com/fwlink/?linkid=845471). + WARNING_DONT_DELETE_INVALID = 26404, // Do not delete an owner which may be in invalid state (r.3: http://go.microsoft.com/fwlink/?linkid=845471). + WARNING_DONT_ASSIGN_TO_VALID = 26405, // Do not assign to an owner which may be in valid state (r.3: http://go.microsoft.com/fwlink/?linkid=845471). + WARNING_DONT_ASSIGN_RAW_TO_OWNER = 26406, // Do not assign a raw pointer to an owner (r.3: http://go.microsoft.com/fwlink/?linkid=845471). + WARNING_DONT_HEAP_ALLOCATE_UNNECESSARILY = 26407, // Prefer scoped objects, don't heap-allocate unnecessarily (r.5: http://go.microsoft.com/fwlink/?linkid=845473). + WARNING_NO_MALLOC_FREE = 26408, // Avoid malloc() and free(), prefer the nothrow version of new with delete (r.10: http://go.microsoft.com/fwlink/?linkid=845483). + WARNING_NO_NEW_DELETE = 26409, // Avoid calling new and delete explicitly, use std::make_unique instead (r.11: http://go.microsoft.com/fwlink/?linkid=845485). + WARNING_NO_REF_TO_CONST_UNIQUE_PTR = 26410, // The parameter '%1$s' is a reference to const unique pointer, use const T* or const T& instead (r.32: http://go.microsoft.com/fwlink/?linkid=845478). + WARNING_NO_REF_TO_UNIQUE_PTR = 26411, // The parameter '%1$s' is a reference to unique pointer and it is never reassigned or reset, use T* or T& instead (r.33: http://go.microsoft.com/fwlink/?linkid=845479). + WARNING_RESET_LOCAL_SMART_PTR = 26414, // Move, copy, reassign or reset a local smart pointer '%1$s' (r.5: http://go.microsoft.com/fwlink/?linkid=845473). + WARNING_SMART_PTR_NOT_NEEDED = 26415, // Smart pointer parameter '%1$s' is used only to access contained pointer. Use T* or T& instead (r.30: http://go.microsoft.com/fwlink/?linkid=845475). + WARNING_NO_RVALUE_REF_SHARED_PTR = 26416, // Shared pointer parameter '%1$s' is passed by rvalue reference. Pass by value instead (r.34: http://go.microsoft.com/fwlink/?linkid=845486). + WARNING_NO_LVALUE_REF_SHARED_PTR = 26417, // Shared pointer parameter '%1$s' is passed by reference and not reset or reassigned. Use T* or T& instead (r.35: http://go.microsoft.com/fwlink/?linkid=845488). + WARNING_NO_VALUE_OR_CONST_REF_SHARED_PTR = 26418, // Shared pointer parameter '%1$s' is not copied or moved. Use T* or T& instead (r.36: http://go.microsoft.com/fwlink/?linkid=845489). + WARNING_NO_GLOBAL_INIT_CALLS = 26426, // Global initializer calls a non-constexpr function '%1$s' (i.22: http://go.microsoft.com/fwlink/?linkid=853919). + WARNING_NO_GLOBAL_INIT_EXTERNS = 26427, // Global initializer accesses extern object '%1$s' (i.22: http://go.microsoft.com/fwlink/?linkid=853919). + WARNING_USE_NOTNULL = 26429, // Symbol '%1$s' is never tested for nullness, it can be marked as not_null (f.23: http://go.microsoft.com/fwlink/?linkid=853921). + WARNING_TEST_ON_ALL_PATHS = 26430, // Symbol '%1$s' is not tested for nullness on all paths (f.23: http://go.microsoft.com/fwlink/?linkid=853921). + WARNING_DONT_TEST_NOTNULL = 26431, // The type of expression '%1$s' is already gsl::not_null. Do not test it for nullness (f.23: http://go.microsoft.com/fwlink/?linkid=853921). + WARNING_DEFINE_OR_DELETE_SPECIAL_OPS = 26432, // If you define or delete any default operation in the type '%1$s', define or delete them all (c.21: http://go.microsoft.com/fwlink/?linkid=853922). + WARNING_OVERRIDE_EXPLICITLY = 26433, // Function '%1$s' should be marked with 'override' (c.128: http://go.microsoft.com/fwlink/?linkid=853923). + WARNING_DONT_HIDE_METHODS = 26434, // Function '%1$s' hides a non-virtual function '%2$s' (c.128: http://go.microsoft.com/fwlink/?linkid=853923). + WARNING_SINGLE_VIRTUAL_SPECIFICATION = 26435, // Function '%1$s' should specify exactly one of 'virtual', 'override', or 'final' (c.128: http://go.microsoft.com/fwlink/?linkid=853923). + WARNING_NEED_VIRTUAL_DTOR = 26436, // The type '%1$s' with a virtual function needs either public virtual or protected non-virtual destructor (c.35: http://go.microsoft.com/fwlink/?linkid=853924). + WARNING_DONT_SLICE = 26437, // Do not slice (es.63: http://go.microsoft.com/fwlink/?linkid=853925). + WARNING_NO_GOTO = 26438, // Avoid 'goto' (es.76: http://go.microsoft.com/fwlink/?linkid=853926). + WARNING_SPECIAL_NOEXCEPT = 26439, // This kind of function may not throw. Declare it 'noexcept' (f.6: http://go.microsoft.com/fwlink/?linkid=853927). + WARNING_DECLARE_NOEXCEPT = 26440, // Function '%1$s' can be declared 'noexcept' (f.6: http://go.microsoft.com/fwlink/?linkid=853927). + WARNING_NO_UNNAMED_GUARDS = 26441, // Guard objects must be named (cp.44: http://go.microsoft.com/fwlink/?linkid=853928). + WARNING_NO_EXPLICIT_DTOR_OVERRIDE = 26443, // Overriding destructor should not use explicit 'override' or 'virtual' specifiers (c.128: http://go.microsoft.com/fwlink/?linkid=853923). + WARNING_NO_UNNAMED_RAII_OBJECTS = 26444, // Avoid unnamed objects with custom construction and destruction (es.84: http://go.microsoft.com/fwlink/?linkid=862923). + WARNING_RESULT_OF_ARITHMETIC_OPERATION_PROVABLY_LOSSY = 26450, // Arithmetic overflow: '%1$s' operation causes overflow at compile time. Use a wider type to store the operands (io.1: https://go.microsoft.com/fwlink/?linkid=864597). + WARNING_RESULT_OF_ARITHMETIC_OPERATION_CAST_TO_LARGER_SIZE = 26451, // Arithmetic overflow: Using operator '%1$s' on a %2$d byte value and then casting the result to a %3$d byte value. Cast the value to the wider type before calling operator '%1$s' to avoid overflow (io.2: https://go.microsoft.com/fwlink/?linkid=864598). + WARNING_SHIFT_COUNT_NEGATIVE_OR_TOO_BIG = 26452, // Arithmetic overflow: Left shift count is negative or greater than or equal to the operand size which is undefined behavior (io.3: https://go.microsoft.com/fwlink/?linkid=864599). + WARNING_LEFTSHIFT_NEGATIVE_SIGNED_NUMBER = 26453, // Arithmetic overflow: Left shift of a negative signed number is undefined behavior (io.4: https://go.microsoft.com/fwlink/?linkid=864600). + WARNING_RESULT_OF_ARITHMETIC_OPERATION_NEGATIVE_UNSIGNED = 26454, // Arithmetic overflow: '%1$s' operation produces a negative unsigned result at compile time (io.5: https://go.microsoft.com/fwlink/?linkid=864602). + WARNING_USE_CONST_REFERENCE_ARGUMENTS = 26460, // The reference argument '%s' for function '%s' can be marked as const (con.3: https://go.microsoft.com/fwlink/p/?LinkID=786684). + WARNING_USE_CONST_POINTER_ARGUMENTS = 26461, // The pointer argument '%s' for function '%s' can be marked as a pointer to const (con.3: https://go.microsoft.com/fwlink/p/?LinkID=786684). + WARNING_USE_CONST_POINTER_FOR_VARIABLE = 26462, // The value pointed to by '%1$s' is assigned only once, mark it as a pointer to const (con.4: https://go.microsoft.com/fwlink/p/?LinkID=784969). + WARNING_USE_CONST_FOR_ELEMENTS = 26463, // The elements of array '%1$s' are assigned only once, mark elements const (con.4: https://go.microsoft.com/fwlink/p/?LinkID=784969). + WARNING_USE_CONST_POINTER_FOR_ELEMENTS = 26464, // The values pointed to by elements of array '%1$s' are assigned only once, mark elements as pointer to const (con.4: https://go.microsoft.com/fwlink/p/?LinkID=784969). + WARNING_NO_CONST_CAST_UNNECESSARY = 26465, // Don't use const_cast to cast away const. const_cast is not required; constness or volatility is not being removed by this conversion (type.3: http://go.microsoft.com/fwlink/p/?LinkID=620419). + WARNING_NO_STATIC_DOWNCAST_POLYMORPHIC = 26466, // Don't use static_cast downcasts. A cast from a polymorphic type should use dynamic_cast (type.2: http://go.microsoft.com/fwlink/p/?LinkID=620418). + WARNING_NO_REINTERPRET_CAST_FROM_VOID_PTR = 26471, // Don't use reinterpret_cast. A cast from void* can use static_cast (type.1: http://go.microsoft.com/fwlink/p/?LinkID=620417). + WARNING_NO_CASTS_FOR_ARITHMETIC_CONVERSION = 26472, // Don't use a static_cast for arithmetic conversions. Use brace initialization, gsl::narrow_cast or gsl::narow (type.1: http://go.microsoft.com/fwlink/p/?LinkID=620417). + WARNING_NO_IDENTITY_CAST = 26473, // Don't cast between pointer types where the source type and the target type are the same (type.1: http://go.microsoft.com/fwlink/p/?LinkID=620417). + WARNING_NO_IMPLICIT_CAST = 26474, // Don't cast between pointer types when the conversion could be implicit (type.1: http://go.microsoft.com/fwlink/p/?LinkID=620417). + WARNING_NO_FUNCTION_STYLE_CASTS = 26475, // Do not use function style C-casts (es.49: http://go.microsoft.com/fwlink/?linkid=853930). + WARNING_NO_POINTER_ARITHMETIC = 26481, // Don't use pointer arithmetic. Use span instead (bounds.1: http://go.microsoft.com/fwlink/p/?LinkID=620413). + WARNING_NO_DYNAMIC_ARRAY_INDEXING = 26482, // Only index into arrays using constant expressions (bounds.2: http://go.microsoft.com/fwlink/p/?LinkID=620414). + WARNING_STATIC_INDEX_OUT_OF_RANGE = 26483, // Value %1$lld is outside the bounds (0, %2$lld) of variable '%3$s'. Only index into arrays using constant expressions that are within bounds of the array (bounds.2: http://go.microsoft.com/fwlink/p/?LinkID=620414). + WARNING_NO_ARRAY_TO_POINTER_DECAY = 26485, // Expression '%1$s': No array to pointer decay (bounds.3: http://go.microsoft.com/fwlink/p/?LinkID=620415). + WARNING_LIFETIMES_FUNCTION_PRECONDITION_VIOLATION = 26486, // Don't pass a pointer that may be invalid to a function. Parameter %1$d '%2$s' in call to '%3$s' may be invalid (lifetime.1: http://go.microsoft.com/fwlink/p/?LinkID=851958). + WARNING_LIFETIMES_FUNCTION_POSTCONDITION_VIOLATION = 26487, // Don't return a pointer that may be invalid (lifetime.1: http://go.microsoft.com/fwlink/p/?LinkID=851958). + WARNING_DEREF_INVALID_POINTER = 26489, // Don't dereference a pointer that may be invalid: '%1$s'. '%2$s' may have been invalidated at line %3$u (lifetime.1: http://go.microsoft.com/fwlink/p/?LinkID=851958). + WARNING_NO_REINTERPRET_CAST = 26490, // Don't use reinterpret_cast (type.1: http://go.microsoft.com/fwlink/p/?LinkID=620417). + WARNING_NO_STATIC_DOWNCAST = 26491, // Don't use static_cast downcasts (type.2: http://go.microsoft.com/fwlink/p/?LinkID=620418). + WARNING_NO_CONST_CAST = 26492, // Don't use const_cast to cast away const (type.3: http://go.microsoft.com/fwlink/p/?LinkID=620419). + WARNING_NO_CSTYLE_CAST = 26493, // Don't use C-style casts (type.4: http://go.microsoft.com/fwlink/p/?LinkID=620420). + WARNING_VAR_USE_BEFORE_INIT = 26494, // Variable '%1$s' is uninitialized. Always initialize an object (type.5: http://go.microsoft.com/fwlink/p/?LinkID=620421). + WARNING_MEMBER_UNINIT = 26495, // Variable '%1$s' is uninitialized. Always initialize a member variable (type.6: http://go.microsoft.com/fwlink/p/?LinkID=620422). + WARNING_USE_CONST_FOR_VARIABLE = 26496, // The variable '%1$s' is assigned only once, mark it as const (con.4: https://go.microsoft.com/fwlink/p/?LinkID=784969). + WARNING_USE_CONSTEXPR_FOR_FUNCTION = 26497, // The function '%s' could be marked constexpr if compile-time evaluation is desired (f.4: https://go.microsoft.com/fwlink/p/?LinkID=784970). + WARNING_USE_CONSTEXPR_FOR_FUNCTIONCALL = 26498, // The function '%1$s' is constexpr, mark variable '%2$s' constexpr if compile-time evaluation is desired (con.5: https://go.microsoft.com/fwlink/p/?LinkID=784974). +}; + +#define ALL_CPPCORECHECK_WARNINGS 26400 26401 26402 26403 26404 26405 26406 26407 26408 26409 26410 26411 26414 26415 26416 26417 26418 26426 26427 26429 26430 26431 26432 26433 26434 26435 26436 26437 26438 26439 26440 26441 26443 26444 26450 26451 26452 26453 26454 26460 26461 26462 26463 26464 26465 26466 26471 26472 26473 26474 26475 26481 26482 26483 26485 26486 26487 26489 26490 26491 26492 26493 26494 26495 26496 26497 26498 + +#define CPPCORECHECK_ARITHMETIC_WARNINGS 26450 26451 26452 26453 26454 + +#define CPPCORECHECK_BOUNDS_WARNINGS 26481 26482 26483 26485 + +#define CPPCORECHECK_CLASS_WARNINGS 26432 26433 26434 26435 26436 26443 + +#define CPPCORECHECK_CONCURRENCY_WARNINGS 26441 + +#define CPPCORECHECK_CONST_WARNINGS 26460 26461 26462 26463 26464 26496 26497 26498 + +#define CPPCORECHECK_DECLARATION_WARNINGS 26426 26427 26444 + +#define CPPCORECHECK_FUNCTION_WARNINGS 26439 26440 + +#define CPPCORECHECK_LIFETIME_WARNINGS 26486 26487 26489 + +#define CPPCORECHECK_OWNER_POINTER_WARNINGS 26402 26403 26404 26405 26406 26407 26429 26430 26431 + +#define CPPCORECHECK_RAW_POINTER_WARNINGS 26400 26401 26402 26408 26409 26429 26430 26431 26481 26485 + +#define CPPCORECHECK_SHARED_POINTER_WARNINGS 26414 26415 26416 26417 26418 + +#define CPPCORECHECK_STYLE_WARNINGS 26438 + +#define CPPCORECHECK_TYPE_WARNINGS 26437 26465 26466 26471 26472 26473 26474 26475 26490 26491 26492 26493 26494 26495 + +#define CPPCORECHECK_UNIQUE_POINTER_WARNINGS 26410 26411 26414 26415 + + + + + diff --git a/src/inc/DefaultSettings.h b/src/inc/DefaultSettings.h new file mode 100644 index 000000000..d959e41f7 --- /dev/null +++ b/src/inc/DefaultSettings.h @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- DefaultSettings.h + +Abstract: +- A header with a bunch of default values used for settings, especially for + terminal settings used by Cascadia +Author(s): +- Mike Griese - March 2019 + +--*/ +#pragma once + +constexpr COLORREF COLOR_WHITE = 0x00ffffff; +constexpr COLORREF COLOR_BLACK = 0x00000000; +constexpr COLORREF OPACITY_OPAQUE = 0xff000000; + +constexpr COLORREF DEFAULT_FOREGROUND = COLOR_WHITE; +constexpr COLORREF DEFAULT_FOREGROUND_WITH_ALPHA = OPACITY_OPAQUE | DEFAULT_FOREGROUND; +constexpr COLORREF DEFAULT_BACKGROUND = COLOR_BLACK; +constexpr COLORREF DEFAULT_BACKGROUND_WITH_ALPHA = OPACITY_OPAQUE | DEFAULT_BACKGROUND; +constexpr short DEFAULT_HISTORY_SIZE = 9001; +const std::wstring DEFAULT_FONT_FACE { L"Consolas" }; +constexpr int DEFAULT_FONT_SIZE = 12; + +constexpr int DEFAULT_ROWS = 30; +constexpr int DEFAULT_COLS = 120; + +const std::wstring DEFAULT_PADDING{ L"0, 0, 0, 0" }; +const std::wstring DEFAULT_STARTING_DIRECTORY{ L"%USERPROFILE%" }; + +constexpr COLORREF DEFAULT_CURSOR_COLOR = COLOR_WHITE; +constexpr COLORREF DEFAULT_CURSOR_HEIGHT = 25; diff --git a/src/inc/HostAndPropsheetIncludes.h b/src/inc/HostAndPropsheetIncludes.h new file mode 100644 index 000000000..4ffc2eedc --- /dev/null +++ b/src/inc/HostAndPropsheetIncludes.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#pragma once + +// Define and then undefine WIN32_NO_STATUS because windows.h has no guard to prevent it from double defing certain statuses +// when included with ntstatus.h +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS + +// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs +typedef _Return_type_success_(return >= 0) LONG NTSTATUS; +/*lint -save -e624 */ // Don't complain about different typedefs. +typedef NTSTATUS *PNTSTATUS; +/*lint -restore */ // Resume checking for different typedefs. +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +// End From ntdef.h + +#define INLINE_NTSTATUS_FROM_WIN32 1 // Must use inline NTSTATUS or it will call the wrapped function twice. +#pragma warning(push) +#pragma warning(disable:4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. +#include +#pragma warning(pop) + +#include + +#ifdef EXTERNAL_BUILD +#include +#else +#include +#endif + +#include + +#include + +#include + +// Only remaining item from w32gdip.h. There's probably a better way to do this as well. +#define IS_ANY_DBCS_CHARSET( CharSet ) \ + ( ((CharSet) == SHIFTJIS_CHARSET) ? TRUE : \ + ((CharSet) == HANGEUL_CHARSET) ? TRUE : \ + ((CharSet) == CHINESEBIG5_CHARSET) ? TRUE : \ + ((CharSet) == GB2312_CHARSET) ? TRUE : FALSE ) + +#include "conddkrefs.h" +#include "conwinuserrefs.h" diff --git a/src/inc/IDefaultColorProvider.hpp b/src/inc/IDefaultColorProvider.hpp new file mode 100644 index 000000000..36bb92b69 --- /dev/null +++ b/src/inc/IDefaultColorProvider.hpp @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- IDefaultColorProvider.hpp + +Abstract: +- Provides an abstraction for aquiring the default colors of a console object. + +Author(s): +- Mike Griese (migrie) 11 Oct 2017 +--*/ + +#pragma once + +namespace Microsoft::Console +{ + class IDefaultColorProvider + { + public: + virtual ~IDefaultColorProvider() = 0; + virtual COLORREF GetDefaultForeground() const = 0; + virtual COLORREF GetDefaultBackground() const = 0; + }; + + inline Microsoft::Console::IDefaultColorProvider::~IDefaultColorProvider() { } +} diff --git a/src/inc/ITerminalOutputConnection.hpp b/src/inc/ITerminalOutputConnection.hpp new file mode 100644 index 000000000..f323c84c5 --- /dev/null +++ b/src/inc/ITerminalOutputConnection.hpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- ITerminalOutputConnection.hpp + +Abstract: +- Provides an abstraction for writing to the output pipe connected to the TTY. + In conpty mode, this is implemented by the VtRenderer, such that other + parts of the codebase (the state machine) can write VT sequences directly + to the terminal controlling us. +*/ + +#pragma once + +namespace Microsoft::Console +{ + class ITerminalOutputConnection + { + public: + virtual ~ITerminalOutputConnection() = 0; + + [[nodiscard]] + virtual HRESULT WriteTerminalUtf8(const std::string& str) = 0; + [[nodiscard]] + virtual HRESULT WriteTerminalW(const std::wstring& wstr) = 0; + }; + + inline Microsoft::Console::ITerminalOutputConnection::~ITerminalOutputConnection() { } +} diff --git a/src/inc/ITerminalOwner.hpp b/src/inc/ITerminalOwner.hpp new file mode 100644 index 000000000..31c6e535c --- /dev/null +++ b/src/inc/ITerminalOwner.hpp @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- ITerminalOwner.hpp + +Abstract: +- Provides an abstraction for Closing the Input/Output objects of a terminal + connection. This is implemented by VtIo in the host, and is used by the + renderer to be able to tell the VtIo object that the renderer has had it's + pipe broken. + +Author(s): +- Mike Griese (migrie) 28 March 2018 +--*/ + +#pragma once + +namespace Microsoft::Console +{ + class ITerminalOwner + { + public: + virtual ~ITerminalOwner() = 0; + + virtual void CloseInput() = 0; + virtual void CloseOutput() = 0; + }; + + // See docs/virtual-dtors.md for an explanation of why this is weird. + inline Microsoft::Console::ITerminalOwner::~ITerminalOwner() { } +} diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h new file mode 100644 index 000000000..b73a1322f --- /dev/null +++ b/src/inc/LibraryIncludes.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#pragma warning(push) +#pragma warning(disable: ALL_CPPCORECHECK_WARNINGS) + +// C +#include +#include + +// STL + +// Block minwindef.h min/max macros to prevent conflict +#define NOMINMAX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// WIL + +#include +#include +#include +#include + +// GSL +// Block GSL Multi Span include because it both has C++17 deprecated iterators +// and uses the C-namespaced "max" which conflicts with Windows definitions. +#define GSL_MULTI_SPAN_H +#include + +// IntSafe +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS +#include + +// SAL +#include + +#pragma warning(pop) diff --git a/src/inc/VtIoModes.hpp b/src/inc/VtIoModes.hpp new file mode 100644 index 000000000..9e9458597 --- /dev/null +++ b/src/inc/VtIoModes.hpp @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +enum class VtIoMode +{ + INVALID, + XTERM, + XTERM_256, + WIN_TELNET, + XTERM_ASCII +}; + +const wchar_t* const XTERM_STRING = L"xterm"; +const wchar_t* const XTERM_256_STRING = L"xterm-256color"; +const wchar_t* const WIN_TELNET_STRING = L"win-telnet"; +const wchar_t* const XTERM_ASCII_STRING = L"xterm-ascii"; +const wchar_t* const DEFAULT_STRING = L""; diff --git a/src/inc/argb.h b/src/inc/argb.h new file mode 100644 index 000000000..2c5170410 --- /dev/null +++ b/src/inc/argb.h @@ -0,0 +1,26 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- argb.h + +Abstract: +- Replaces the RGB macro with one that fills the highest order byte with 0xff. + We use this for the cascadia project, because it can have colors with an + alpha component. For code that is alpha-aware, include this header to make + RGB() fill the alpha byte. Otherwise, colors made with RGB will be transparent. +Author(s): +- Mike Griese (migrie) Feb 2019 +--*/ +#pragma once + +constexpr COLORREF ARGB(const BYTE a, const BYTE r, const BYTE g, const BYTE b) noexcept +{ + return (a<<24) | (b<<16) | (g<<8) | (r); +} + +#ifdef RGB +#undef RGB +#define RGB(r, g, b) (ARGB(255, (r), (g), (b))) +#endif diff --git a/src/inc/conattrs.hpp b/src/inc/conattrs.hpp new file mode 100644 index 000000000..529ae07d1 --- /dev/null +++ b/src/inc/conattrs.hpp @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. +--*/ +#pragma once + +#define FG_ATTRS (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY) +#define BG_ATTRS (BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY) +#define META_ATTRS (COMMON_LVB_LEADING_BYTE | COMMON_LVB_TRAILING_BYTE | COMMON_LVB_GRID_HORIZONTAL | COMMON_LVB_GRID_LVERTICAL | COMMON_LVB_GRID_RVERTICAL | COMMON_LVB_REVERSE_VIDEO | COMMON_LVB_UNDERSCORE ) + +WORD FindNearestTableIndex(const COLORREF Color, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable); + +bool FindTableIndex(const COLORREF Color, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable, + _Out_ WORD* const pFoundIndex); + +WORD XtermToWindowsIndex(const size_t index) noexcept; +WORD Xterm256ToWindowsIndex(const size_t index) noexcept; +WORD XtermToLegacy(const size_t xtermForeground, const size_t xtermBackground); + +COLORREF ForegroundColor(const WORD wLegacyAttrs, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const size_t cColorTable); + +COLORREF BackgroundColor(const WORD wLegacyAttrs, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const size_t cColorTable); + +const WORD WINDOWS_RED_ATTR = FOREGROUND_RED; +const WORD WINDOWS_GREEN_ATTR = FOREGROUND_GREEN; +const WORD WINDOWS_BLUE_ATTR = FOREGROUND_BLUE; +const WORD WINDOWS_BRIGHT_ATTR = FOREGROUND_INTENSITY; + +const WORD XTERM_RED_ATTR = 0x01; +const WORD XTERM_GREEN_ATTR = 0x02; +const WORD XTERM_BLUE_ATTR = 0x04; +const WORD XTERM_BRIGHT_ATTR = 0x08; + +enum class CursorType : unsigned int +{ + Legacy = 0x0, // uses the cursor's height value to range from underscore-like to full box + VerticalBar = 0x1, // A single vertical line, '|' + Underscore = 0x2, // a single horizontal underscore, smaller that the min height legacy cursor. + EmptyBox = 0x3, // Just the outline of a full box + FullBox = 0x4 // a full box, similar to legacy with height=100% +}; + +// Valid COLORREFs are of the pattern 0x00bbggrr. -1 works as an invalid color, +// as the highest byte of a valid color is always 0. +constexpr COLORREF INVALID_COLOR = 0xffffffff; + +constexpr WORD COLOR_TABLE_SIZE = 16; +constexpr WORD XTERM_COLOR_TABLE_SIZE = 256; diff --git a/src/inc/conime.h b/src/inc/conime.h new file mode 100644 index 000000000..f2637cf94 --- /dev/null +++ b/src/inc/conime.h @@ -0,0 +1,63 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + conime.h + +Abstract: + + This module contains the internal structures and definitions used + by the console IME. + +Author: + + v-HirShi Jul.4.1995 + +Revision History: + +--*/ + +#pragma once + +#define CONIME_ATTRCOLOR_SIZE 8 + +#define CONIME_CURSOR_RIGHT 0x10 +#define CONIME_CURSOR_LEFT 0x20 + +[[nodiscard]] +HRESULT ImeStartComposition(); + +[[nodiscard]] +HRESULT ImeEndComposition(); + +[[nodiscard]] +HRESULT ImeComposeData(std::wstring_view text, + std::basic_string_view attributes, + std::basic_string_view colorArray); + +[[nodiscard]] +HRESULT ImeClearComposeData(); + +[[nodiscard]] +HRESULT ImeComposeResult(std::wstring_view text); + +// Default composition color attributes +#define DEFAULT_COMP_ENTERED \ + (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ + COMMON_LVB_UNDERSCORE) +#define DEFAULT_COMP_ALREADY_CONVERTED \ + (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ + BACKGROUND_BLUE ) +#define DEFAULT_COMP_CONVERSION \ + (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | \ + COMMON_LVB_UNDERSCORE) +#define DEFAULT_COMP_YET_CONVERTED \ + (FOREGROUND_BLUE | \ + BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | \ + COMMON_LVB_UNDERSCORE) +#define DEFAULT_COMP_INPUT_ERROR \ + ( FOREGROUND_RED | \ + COMMON_LVB_UNDERSCORE) diff --git a/src/inc/conint.h b/src/inc/conint.h new file mode 100644 index 000000000..1f307a58b --- /dev/null +++ b/src/inc/conint.h @@ -0,0 +1,45 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + conint.h + +Abstract: + + This module defines an abstraction for calling certain OS support + functionality that we can't make completely public due to obligations + we have with our partner teams about disclosure of their API surface. + +Author: + + miniksa 17-Apr-2019 + +Revision History: + +--*/ + +#pragma once + +namespace Microsoft::Console::Internal +{ + + namespace ProcessPolicy + { + [[nodiscard]] + HRESULT CheckAppModelPolicy(const HANDLE hToken, + bool& fIsWrongWayBlocked) noexcept; + + [[nodiscard]] + HRESULT CheckIntegrityLevelPolicy(const HANDLE hOtherToken, + bool& fIsWrongWayBlocked) noexcept; + } + + namespace EdpPolicy + { + void AuditClipboard(const std::wstring_view destinationName) noexcept; + } + +} diff --git a/src/inc/conpty-universal.h b/src/inc/conpty-universal.h new file mode 100644 index 000000000..6f9422451 --- /dev/null +++ b/src/inc/conpty-universal.h @@ -0,0 +1,183 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// WARNING!!! +// This is a fork of conpty.h +// It has some small modifications to help debug conhost-backed pseudoconsoles +// within the context of Universal Applications. Notably: +// * SetHandleInformation and HANDLE_FLAG_INHERIT are not present in +// WINAPI_PARTITION_APP, so we're just leaving the handles inheritable for +// now. This is definitely a bug, but the ConhostConnection isn't meant to +// be shipping code. Conhosts created by this version of CreateConPty will +// only go away when the app is closed, not when the pipes are broken. +// Fortunately, because the universal app is containered, they'll be +// cleaned up when the app is terminated. IF YOU USE THIS HEADER OUTSIDE OF +// A UNIVERSAL APP, THE CHILD CONHOST.EXE PROCESSES WILL NOT BE TERMINATED. +// * Whoever includes this will also need to define STARTF_USESTDHANDLES: +// ``` +// #ifndef STARTF_USESTDHANDLES +// #define STARTF_USESTDHANDLES 0x00000100 +// #endif +// ``` + +#include +#include +#include +#include +#include +#pragma once + +const unsigned int PTY_SIGNAL_RESIZE_WINDOW = 8u; + +HRESULT CreateConPty(const std::wstring& cmdline, // _In_ + const unsigned short w, // _In_ + const unsigned short h, // _In_ + HANDLE* const hInput, // _Out_ + HANDLE* const hOutput, // _Out_ + HANDLE* const hSignal, // _Out_ + PROCESS_INFORMATION* const piPty); // _Out_ + +bool SignalResizeWindow(const HANDLE hSignal, + const unsigned short w, + const unsigned short h); + + +// Function Description: +// - Creates a headless conhost in "pty mode" and launches the given commandline +// attached to the conhost. Gives back handles to three different pipes: +// * hInput: The caller can write input to the conhost, encoded in utf-8, on +// this pipe. For keys that don't have character representations, the +// caller should use the `TERM=xterm` VT sequences for encoding the input. +// * hOutput: The caller should read from this pipe. The headless conhost will +// "render" it's state to a stream of utf-8 encoded text with VT sequences. +// * hSignal: The caller can use this to resize the size of the underlying PTY +// using the SignalResizeWindow function. +// Arguments: +// - cmdline: The commandline to launch as a console process attached to the pty +// that's created. +// - startingDirectory: The directory to start the process in +// - w: The initial width of the pty, in characters +// - h: The initial height of the pty, in characters +// - hInput: A handle to the pipe for writing input to the pty. +// - hOutput: A handle to the pipe for reading the output of the pty. +// - hSignal: A handle to the pipe for writing signal messages to the pty. +// - piPty: The PROCESS_INFORMATION of the pty process. NOTE: This is *not* the +// PROCESS_INFORMATION of the process that's created as a result the cmdline. +// Return Value: +// - S_OK if we succeeded, or an appropriate HRESULT for failing format the +// commandline or failing to launch the conhost +__declspec(noinline) inline +HRESULT CreateConPty(const std::wstring& cmdline, + std::optional startingDirectory, + const unsigned short w, + const unsigned short h, + HANDLE* const hInput, + HANDLE* const hOutput, + HANDLE* const hSignal, + PROCESS_INFORMATION* const piPty) +{ + // Create some anon pipes so we can pass handles down and into the console. + // IMPORTANT NOTE: + // We're creating the pipe here with un-inheritable handles, then marking + // the conhost sides of the pipes as inheritable. We do this because if + // the entire pipe is marked as inheritable, when we pass the handles + // to CreateProcess, at some point the entire pipe object is copied to + // the conhost process, which includes the terminal side of the pipes + // (_inPipe and _outPipe). This means that if we die, there's still + // outstanding handles to our side of the pipes, and those handles are + // in conhost, despite conhost being unable to reference those handles + // and close them. + // CRITICAL: Close our side of the handles. Otherwise you'll get the same + // problem if you close conhost, but not us (the terminal). + HANDLE outPipeConhostSide; + HANDLE inPipeConhostSide; + HANDLE signalPipeConhostSide; + + SECURITY_ATTRIBUTES sa; + sa = {0}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = nullptr; + + CreatePipe(&inPipeConhostSide, hInput, &sa, 0); + CreatePipe(hOutput, &outPipeConhostSide, &sa, 0); + CreatePipe(&signalPipeConhostSide, hSignal, &sa, 0); + + SetHandleInformation(inPipeConhostSide, HANDLE_FLAG_INHERIT, 1); + SetHandleInformation(outPipeConhostSide, HANDLE_FLAG_INHERIT, 1); + SetHandleInformation(signalPipeConhostSide, HANDLE_FLAG_INHERIT, 1); + + std::wstring conhostCmdline = L"conhost.exe"; + conhostCmdline += L" --headless"; + std::wstringstream ss; + if (w != 0 && h != 0) + { + ss << L" --width " << (unsigned long)w; + ss << L" --height " << (unsigned long)h; + } + + ss << L" --signal 0x" << std::hex << HandleToUlong(signalPipeConhostSide); + conhostCmdline += ss.str(); + conhostCmdline += L" -- "; + conhostCmdline += cmdline; + + STARTUPINFO si = {0}; + si.cb = sizeof(STARTUPINFOW); + si.hStdInput = inPipeConhostSide; + si.hStdOutput = outPipeConhostSide; + si.hStdError = outPipeConhostSide; + si.dwFlags |= STARTF_USESTDHANDLES; + + std::unique_ptr mutableCommandline = std::make_unique(conhostCmdline.length() + 1); + if (mutableCommandline == nullptr) + { + return E_OUTOFMEMORY; + } + HRESULT hr = StringCchCopy(mutableCommandline.get(), conhostCmdline.length()+1, conhostCmdline.c_str()); + if (!SUCCEEDED(hr)) + { + return hr; + } + + LPCWSTR lpCurrentDirectory = startingDirectory.has_value() ? startingDirectory.value().c_str() : nullptr; + + bool fSuccess = !!CreateProcessW( + nullptr, + mutableCommandline.get(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + true, // bInheritHandles + 0, // dwCreationFlags + nullptr, // lpEnvironment + lpCurrentDirectory, // lpCurrentDirectory + &si, // lpStartupInfo + piPty // lpProcessInformation + ); + + CloseHandle(inPipeConhostSide); + CloseHandle(outPipeConhostSide); + CloseHandle(signalPipeConhostSide); + + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - Resizes the pty that's connected to hSignal. +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// - w: The new width of the pty, in characters +// - h: The new height of the pty, in characters +// Return Value: +// - true if the resize succeeded, else false. +__declspec(noinline) inline +bool SignalResizeWindow(HANDLE hSignal, const unsigned short w, const unsigned short h) +{ + unsigned short signalPacket[3]; + signalPacket[0] = PTY_SIGNAL_RESIZE_WINDOW; + signalPacket[1] = w; + signalPacket[2] = h; + + return !!WriteFile(hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); +} + diff --git a/src/inc/conpty.h b/src/inc/conpty.h new file mode 100644 index 000000000..236732295 --- /dev/null +++ b/src/inc/conpty.h @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include +#include +#include +#include +#include +#pragma once + +const unsigned int PTY_SIGNAL_RESIZE_WINDOW = 8u; + +HRESULT CreateConPty(const std::wstring& cmdline, // _In_ + const unsigned short w, // _In_ + const unsigned short h, // _In_ + HANDLE* const hInput, // _Out_ + HANDLE* const hOutput, // _Out_ + HANDLE* const hSignal, // _Out_ + PROCESS_INFORMATION* const piPty); // _Out_ + +bool SignalResizeWindow(const HANDLE hSignal, + const unsigned short w, + const unsigned short h); + + +// Function Description: +// - Creates a headless conhost in "pty mode" and launches the given commandline +// attached to the conhost. Gives back handles to three different pipes: +// * hInput: The caller can write input to the conhost, encoded in utf-8, on +// this pipe. For keys that don't have character representations, the +// caller should use the `TERM=xterm` VT sequences for encoding the input. +// * hOutput: The caller should read from this pipe. The headless conhost will +// "render" it's state to a stream of utf-8 encoded text with VT sequences. +// * hSignal: The caller can use this to resize the size of the underlying PTY +// using the SignalResizeWindow function. +// Arguments: +// - cmdline: The commandline to launch as a console process attached to the pty +// that's created. +// - w: The initial width of the pty, in characters +// - h: The initial height of the pty, in characters +// - hInput: A handle to the pipe for writing input to the pty. +// - hOutput: A handle to the pipe for reading the output of the pty. +// - hSignal: A handle to the pipe for writing signal messages to the pty. +// - piPty: The PROCESS_INFORMATION of the pty process. NOTE: This is *not* the +// PROCESS_INFORMATION of the process that's created as a result the cmdline. +// Return Value: +// - S_OK if we succeeded, or an appropriate HRESULT for failing format the +// commandline or failing to launch the conhost +__declspec(noinline) inline +HRESULT CreateConPty(const std::wstring& cmdline, + const unsigned short w, + const unsigned short h, + HANDLE* const hInput, + HANDLE* const hOutput, + HANDLE* const hSignal, + PROCESS_INFORMATION* const piPty) +{ + // Create some anon pipes so we can pass handles down and into the console. + // IMPORTANT NOTE: + // We're creating the pipe here with un-inheritable handles, then marking + // the conhost sides of the pipes as inheritable. We do this because if + // the entire pipe is marked as inheritable, when we pass the handles + // to CreateProcess, at some point the entire pipe object is copied to + // the conhost process, which includes the terminal side of the pipes + // (_inPipe and _outPipe). This means that if we die, there's still + // outstanding handles to our side of the pipes, and those handles are + // in conhost, despite conhost being unable to reference those handles + // and close them. + // CRITICAL: Close our side of the handles. Otherwise you'll get the same + // problem if you close conhost, but not us (the terminal). + HANDLE outPipeConhostSide; + HANDLE inPipeConhostSide; + HANDLE signalPipeConhostSide; + + SECURITY_ATTRIBUTES sa; + sa = {0}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = nullptr; + + CreatePipe(&inPipeConhostSide, hInput, &sa, 0); + CreatePipe(hOutput, &outPipeConhostSide, &sa, 0); + + // Mark inheritable for signal handle when creating. It'll have the same value on the other side. + sa.bInheritHandle = TRUE; + CreatePipe(&signalPipeConhostSide, hSignal, &sa, 0); + + SetHandleInformation(inPipeConhostSide, HANDLE_FLAG_INHERIT, 1); + SetHandleInformation(outPipeConhostSide, HANDLE_FLAG_INHERIT, 1); + + std::wstring conhostCmdline = L"conhost.exe"; + conhostCmdline += L" --headless"; + std::wstringstream ss; + if (w != 0 && h != 0) + { + ss << L" --width " << (unsigned long)w; + ss << L" --height " << (unsigned long)h; + } + + ss << L" --signal 0x" << std::hex << HandleToUlong(signalPipeConhostSide); + conhostCmdline += ss.str(); + conhostCmdline += L" -- "; + conhostCmdline += cmdline; + + STARTUPINFO si = {0}; + si.cb = sizeof(STARTUPINFOW); + si.hStdInput = inPipeConhostSide; + si.hStdOutput = outPipeConhostSide; + si.hStdError = outPipeConhostSide; + si.dwFlags |= STARTF_USESTDHANDLES; + + std::unique_ptr mutableCommandline = std::make_unique(conhostCmdline.length() + 1); + if (mutableCommandline == nullptr) + { + return E_OUTOFMEMORY; + } + HRESULT hr = StringCchCopy(mutableCommandline.get(), conhostCmdline.length()+1, conhostCmdline.c_str()); + if (!SUCCEEDED(hr)) + { + return hr; + } + + bool fSuccess = !!CreateProcessW( + nullptr, + mutableCommandline.get(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + true, // bInheritHandles + 0, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &si, // lpStartupInfo + piPty // lpProcessInformation + ); + + CloseHandle(inPipeConhostSide); + CloseHandle(outPipeConhostSide); + CloseHandle(signalPipeConhostSide); + + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - Resizes the pty that's connected to hSignal. +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// - w: The new width of the pty, in characters +// - h: The new height of the pty, in characters +// Return Value: +// - true if the resize succeeded, else false. +__declspec(noinline) inline +bool SignalResizeWindow(HANDLE hSignal, const unsigned short w, const unsigned short h) +{ + unsigned short signalPacket[3]; + signalPacket[0] = PTY_SIGNAL_RESIZE_WINDOW; + signalPacket[1] = w; + signalPacket[2] = h; + + return !!WriteFile(hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); +} diff --git a/src/inc/consoletaeftemplates.hpp b/src/inc/consoletaeftemplates.hpp new file mode 100644 index 000000000..182c4287d --- /dev/null +++ b/src/inc/consoletaeftemplates.hpp @@ -0,0 +1,546 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleTAEFTemplates.hpp + +Abstract: +- This module contains common TAEF templates for console structures + +Author: +- Michael Niksa (MiNiksa) 2015 +- Paul Campbell (PaulCam) 2015 + +Revision History: +--*/ + +#pragma once + +namespace WEX::TestExecution +{ + template<> + class VerifyOutputTraits < SMALL_RECT > + { + public: + static WEX::Common::NoThrowString ToString(const SMALL_RECT& sr) + { + return WEX::Common::NoThrowString().Format(L"(L:%d, R:%d, T:%d, B:%d)", sr.Left, sr.Right, sr.Top, sr.Bottom); + } + }; + + template<> + class VerifyCompareTraits < SMALL_RECT, SMALL_RECT > + { + public: + static bool AreEqual(const SMALL_RECT& expected, const SMALL_RECT& actual) + { + return expected.Left == actual.Left && + expected.Right == actual.Right && + expected.Top == actual.Top && + expected.Bottom == actual.Bottom; + } + + static bool AreSame(const SMALL_RECT& expected, const SMALL_RECT& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const SMALL_RECT& expectedLess, const SMALL_RECT& expectedGreater) = delete; + + static bool IsGreaterThan(const SMALL_RECT& expectedGreater, const SMALL_RECT& expectedLess) = delete; + + static bool IsNull(const SMALL_RECT& object) + { + return object.Left == 0 && object.Right == 0 && object.Top == 0 && object.Bottom == 0; + } + }; + + template<> + class VerifyOutputTraits < COORD > + { + public: + static WEX::Common::NoThrowString ToString(const COORD& coord) + { + return WEX::Common::NoThrowString().Format(L"(X:%d, Y:%d)", coord.X, coord.Y); + } + }; + + template<> + class VerifyCompareTraits < COORD, COORD> + { + public: + static bool AreEqual(const COORD& expected, const COORD& actual) + { + return expected.X == actual.X && + expected.Y == actual.Y; + } + + static bool AreSame(const COORD& expected, const COORD& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const COORD& expectedLess, const COORD& expectedGreater) + { + // less is on a line above greater (Y values less than) + return (expectedLess.Y < expectedGreater.Y) || + // or on the same lines and less is left of greater (X values less than) + ((expectedLess.Y == expectedGreater.Y) && (expectedLess.X < expectedGreater.X)); + } + + static bool IsGreaterThan(const COORD& expectedGreater, const COORD& expectedLess) + { + // greater is on a line below less (Y value greater than) + return (expectedGreater.Y > expectedLess.Y) || + // or on the same lines and greater is right of less (X values greater than) + ((expectedGreater.Y == expectedLess.Y) && (expectedGreater.X > expectedLess.X)); + } + + static bool IsNull(const COORD& object) + { + return object.X == 0 && object.Y == 0; + } + }; + + template<> + class VerifyOutputTraits < CONSOLE_CURSOR_INFO > + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_CURSOR_INFO& cci) + { + return WEX::Common::NoThrowString().Format(L"(Vis:%s, Size:%d)", cci.bVisible ? L"True" : L"False", cci.dwSize); + } + }; + + template<> + class VerifyCompareTraits < CONSOLE_CURSOR_INFO, CONSOLE_CURSOR_INFO > + { + public: + static bool AreEqual(const CONSOLE_CURSOR_INFO& expected, const CONSOLE_CURSOR_INFO& actual) + { + return expected.bVisible == actual.bVisible && + expected.dwSize == actual.dwSize; + } + + static bool AreSame(const CONSOLE_CURSOR_INFO& expected, const CONSOLE_CURSOR_INFO& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_CURSOR_INFO& expectedLess, const CONSOLE_CURSOR_INFO& expectedGreater) = delete; + + static bool IsGreaterThan(const CONSOLE_CURSOR_INFO& expectedGreater, const CONSOLE_CURSOR_INFO& expectedLess) = delete; + + static bool IsNull(const CONSOLE_CURSOR_INFO& object) + { + return object.bVisible == 0 && object.dwSize == 0; + } + }; + + template<> + class VerifyOutputTraits < CONSOLE_SCREEN_BUFFER_INFOEX > + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_SCREEN_BUFFER_INFOEX& sbiex) + { + return WEX::Common::NoThrowString().Format(L"(Full:%s Attrs:0x%x PopupAttrs:0x%x CursorPos:%s Size:%s MaxSize:%s Viewport:%s)\r\nColors:\r\n(0:0x%x)\r\n(1:0x%x)\r\n(2:0x%x)\r\n(3:0x%x)\r\n(4:0x%x)\r\n(5:0x%x)\r\n(6:0x%x)\r\n(7:0x%x)\r\n(8:0x%x)\r\n(9:0x%x)\r\n(A:0x%x)\r\n(B:0x%x)\r\n(C:0x%x)\r\n(D:0x%x)\r\n(E:0x%x)\r\n(F:0x%x)\r\n", + sbiex.bFullscreenSupported ? L"True" : L"False", + sbiex.wAttributes, + sbiex.wPopupAttributes, + VerifyOutputTraits::ToString(sbiex.dwCursorPosition).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.dwSize).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.dwMaximumWindowSize).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.srWindow).ToCStrWithFallbackTo(L"Fail"), + sbiex.ColorTable[0], + sbiex.ColorTable[1], + sbiex.ColorTable[2], + sbiex.ColorTable[3], + sbiex.ColorTable[4], + sbiex.ColorTable[5], + sbiex.ColorTable[6], + sbiex.ColorTable[7], + sbiex.ColorTable[8], + sbiex.ColorTable[9], + sbiex.ColorTable[10], + sbiex.ColorTable[11], + sbiex.ColorTable[12], + sbiex.ColorTable[13], + sbiex.ColorTable[14], + sbiex.ColorTable[15]); + + } + }; + + template<> + class VerifyCompareTraits < CONSOLE_SCREEN_BUFFER_INFOEX, CONSOLE_SCREEN_BUFFER_INFOEX > + { + public: + static bool AreEqual(const CONSOLE_SCREEN_BUFFER_INFOEX& expected, const CONSOLE_SCREEN_BUFFER_INFOEX& actual) + { + return expected.bFullscreenSupported == actual.bFullscreenSupported && + expected.wAttributes == actual.wAttributes && + expected.wPopupAttributes == actual.wPopupAttributes && + VerifyCompareTraits::AreEqual(expected.dwCursorPosition, actual.dwCursorPosition) && + VerifyCompareTraits::AreEqual(expected.dwSize, actual.dwSize) && + VerifyCompareTraits::AreEqual(expected.dwMaximumWindowSize, actual.dwMaximumWindowSize) && + VerifyCompareTraits::AreEqual(expected.srWindow, actual.srWindow) && + expected.ColorTable[0] == actual.ColorTable[0] && + expected.ColorTable[1] == actual.ColorTable[1] && + expected.ColorTable[2] == actual.ColorTable[2] && + expected.ColorTable[3] == actual.ColorTable[3] && + expected.ColorTable[4] == actual.ColorTable[4] && + expected.ColorTable[5] == actual.ColorTable[5] && + expected.ColorTable[6] == actual.ColorTable[6] && + expected.ColorTable[7] == actual.ColorTable[7] && + expected.ColorTable[8] == actual.ColorTable[8] && + expected.ColorTable[9] == actual.ColorTable[9] && + expected.ColorTable[10] == actual.ColorTable[10] && + expected.ColorTable[11] == actual.ColorTable[11] && + expected.ColorTable[12] == actual.ColorTable[12] && + expected.ColorTable[13] == actual.ColorTable[13] && + expected.ColorTable[14] == actual.ColorTable[14] && + expected.ColorTable[15] == actual.ColorTable[15]; + } + + static bool AreSame(const CONSOLE_SCREEN_BUFFER_INFOEX& expected, const CONSOLE_SCREEN_BUFFER_INFOEX& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_SCREEN_BUFFER_INFOEX& expectedLess, const CONSOLE_SCREEN_BUFFER_INFOEX& expectedGreater) = delete; + + static bool IsGreaterThan(const CONSOLE_SCREEN_BUFFER_INFOEX& expectedGreater, const CONSOLE_SCREEN_BUFFER_INFOEX& expectedLess) = delete; + + static bool IsNull(const CONSOLE_SCREEN_BUFFER_INFOEX& object) + { + return object.bFullscreenSupported == 0 && + object.wAttributes == 0 && + object.wPopupAttributes == 0 && + VerifyCompareTraits::IsNull(object.dwCursorPosition) && + VerifyCompareTraits::IsNull(object.dwSize) && + VerifyCompareTraits::IsNull(object.dwMaximumWindowSize) && + VerifyCompareTraits::IsNull(object.srWindow) && + object.ColorTable[0] == 0x0 && + object.ColorTable[1] == 0x0 && + object.ColorTable[2] == 0x0 && + object.ColorTable[3] == 0x0 && + object.ColorTable[4] == 0x0 && + object.ColorTable[5] == 0x0 && + object.ColorTable[6] == 0x0 && + object.ColorTable[7] == 0x0 && + object.ColorTable[8] == 0x0 && + object.ColorTable[9] == 0x0 && + object.ColorTable[10] == 0x0 && + object.ColorTable[11] == 0x0 && + object.ColorTable[12] == 0x0 && + object.ColorTable[13] == 0x0 && + object.ColorTable[14] == 0x0 && + object.ColorTable[15] == 0x0; + } + }; + + template<> + class VerifyOutputTraits + { + public: + static WEX::Common::NoThrowString ToString(const INPUT_RECORD& ir) + { + SetVerifyOutput verifySettings(VerifyOutputSettings::LogOnlyFailures); + WCHAR szBuf[1024]; + VERIFY_SUCCEEDED(StringCchCopy(szBuf, ARRAYSIZE(szBuf), L"(ev: ")); + switch(ir.EventType) + { + case FOCUS_EVENT: + { + WCHAR szFocus[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szFocus, + ARRAYSIZE(szFocus), + L"FOCUS set: %s)", + ir.Event.FocusEvent.bSetFocus ? L"T" : L"F")); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szFocus)); + break; + } + + case KEY_EVENT: + { + WCHAR szKey[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szKey, + ARRAYSIZE(szKey), + L"KEY down: %s reps: %d kc: 0x%x sc: 0x%x uc: %d ctl: 0x%x)", + ir.Event.KeyEvent.bKeyDown ? L"T" : L"F", + ir.Event.KeyEvent.wRepeatCount, + ir.Event.KeyEvent.wVirtualKeyCode, + ir.Event.KeyEvent.wVirtualScanCode, + ir.Event.KeyEvent.uChar.UnicodeChar, + ir.Event.KeyEvent.dwControlKeyState)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szKey)); + break; + } + + case MENU_EVENT: + { + WCHAR szMenu[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szMenu, + ARRAYSIZE(szMenu), + L"MENU cmd: %d (0x%x))", + ir.Event.MenuEvent.dwCommandId, + ir.Event.MenuEvent.dwCommandId)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szMenu)); + break; + } + + case MOUSE_EVENT: + { + WCHAR szMouse[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szMouse, + ARRAYSIZE(szMouse), + L"MOUSE pos: (%d, %d) buttons: 0x%x ctl: 0x%x evflags: 0x%x)", + ir.Event.MouseEvent.dwMousePosition.X, + ir.Event.MouseEvent.dwMousePosition.Y, + ir.Event.MouseEvent.dwButtonState, + ir.Event.MouseEvent.dwControlKeyState, + ir.Event.MouseEvent.dwEventFlags)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szMouse)); + break; + } + + case WINDOW_BUFFER_SIZE_EVENT: + { + WCHAR szBufferSize[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szBufferSize, + ARRAYSIZE(szBufferSize), + L"WINDOW_BUFFER_SIZE (%d, %d)", + ir.Event.WindowBufferSizeEvent.dwSize.X, + ir.Event.WindowBufferSizeEvent.dwSize.Y)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szBufferSize)); + break; + } + + default: + VERIFY_FAIL(L"ERROR: unknown input event type encountered"); + } + + return WEX::Common::NoThrowString(szBuf); + } + }; + + template<> + class VerifyCompareTraits < INPUT_RECORD, INPUT_RECORD > + { + public: + static bool AreEqual(const INPUT_RECORD& expected, const INPUT_RECORD& actual) + { + bool fEqual = false; + if (expected.EventType == actual.EventType) + { + switch (expected.EventType) + { + case FOCUS_EVENT: + { + fEqual = expected.Event.FocusEvent.bSetFocus == actual.Event.FocusEvent.bSetFocus; + break; + } + + case KEY_EVENT: + { + fEqual = (expected.Event.KeyEvent.bKeyDown == actual.Event.KeyEvent.bKeyDown && + expected.Event.KeyEvent.wRepeatCount == actual.Event.KeyEvent.wRepeatCount && + expected.Event.KeyEvent.wVirtualKeyCode == actual.Event.KeyEvent.wVirtualKeyCode && + expected.Event.KeyEvent.wVirtualScanCode == actual.Event.KeyEvent.wVirtualScanCode && + expected.Event.KeyEvent.uChar.UnicodeChar == actual.Event.KeyEvent.uChar.UnicodeChar && + expected.Event.KeyEvent.dwControlKeyState == actual.Event.KeyEvent.dwControlKeyState); + break; + } + + case MENU_EVENT: + { + fEqual = expected.Event.MenuEvent.dwCommandId == actual.Event.MenuEvent.dwCommandId; + break; + } + + case MOUSE_EVENT: + { + fEqual = (expected.Event.MouseEvent.dwMousePosition.X == actual.Event.MouseEvent.dwMousePosition.X && + expected.Event.MouseEvent.dwMousePosition.Y == actual.Event.MouseEvent.dwMousePosition.Y && + expected.Event.MouseEvent.dwButtonState == actual.Event.MouseEvent.dwButtonState && + expected.Event.MouseEvent.dwControlKeyState == actual.Event.MouseEvent.dwControlKeyState && + expected.Event.MouseEvent.dwEventFlags == actual.Event.MouseEvent.dwEventFlags); + break; + } + + case WINDOW_BUFFER_SIZE_EVENT: + { + fEqual = (expected.Event.WindowBufferSizeEvent.dwSize.X == actual.Event.WindowBufferSizeEvent.dwSize.X && + expected.Event.WindowBufferSizeEvent.dwSize.Y == actual.Event.WindowBufferSizeEvent.dwSize.Y); + break; + } + + default: + VERIFY_FAIL(L"ERROR: unknown input event type encountered"); + } + } + + return fEqual; + } + + static bool AreSame(const INPUT_RECORD& expected, const INPUT_RECORD& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const INPUT_RECORD& expectedLess, const INPUT_RECORD& expectedGreater) = delete; + + static bool IsGreaterThan(const INPUT_RECORD& expectedGreater, const INPUT_RECORD& expectedLess) = delete; + + static bool IsNull(const INPUT_RECORD& object) + { + return object.EventType == 0; + } + }; + + template<> + class VerifyOutputTraits + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_FONT_INFO& cfi) + { + return WEX::Common::NoThrowString().Format(L"Index: %n Size: (X:%d, Y:%d)", cfi.nFont, cfi.dwFontSize.X, cfi.dwFontSize.Y); + } + }; + + template<> + class VerifyCompareTraits + { + public: + static bool AreEqual(const CONSOLE_FONT_INFO& expected, const CONSOLE_FONT_INFO& actual) + { + return expected.nFont == actual.nFont && + expected.dwFontSize.X == actual.dwFontSize.X && + expected.dwFontSize.Y == actual.dwFontSize.Y; + } + + static bool AreSame(const CONSOLE_FONT_INFO& expected, const CONSOLE_FONT_INFO& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_FONT_INFO& expectedLess, const CONSOLE_FONT_INFO& expectedGreater) + { + return expectedLess.dwFontSize.X < expectedGreater.dwFontSize.X && + expectedLess.dwFontSize.Y < expectedGreater.dwFontSize.Y; + } + + static bool IsGreaterThan(const CONSOLE_FONT_INFO& expectedGreater, const CONSOLE_FONT_INFO& expectedLess) + { + return expectedLess.dwFontSize.X < expectedGreater.dwFontSize.X && + expectedLess.dwFontSize.Y < expectedGreater.dwFontSize.Y; + } + + static bool IsNull(const CONSOLE_FONT_INFO& object) + { + return object.nFont == 0 && object.dwFontSize.X == 0 && object.dwFontSize.Y == 0; + } + }; + + template<> + class VerifyOutputTraits + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_FONT_INFOEX& cfiex) + { + return WEX::Common::NoThrowString().Format(L"Index: %d Size: (X:%d, Y:%d) Family: 0x%x (%d) Weight: 0x%x (%d) Name: %ls", + cfiex.nFont, + cfiex.dwFontSize.X, + cfiex.dwFontSize.Y, + cfiex.FontFamily, cfiex.FontFamily, + cfiex.FontWeight, cfiex.FontWeight, + cfiex.FaceName); + } + }; + + template<> + class VerifyCompareTraits + { + public: + static bool AreEqual(const CONSOLE_FONT_INFOEX& expected, const CONSOLE_FONT_INFOEX& actual) + { + return expected.nFont == actual.nFont && + expected.dwFontSize.X == actual.dwFontSize.X && + expected.dwFontSize.Y == actual.dwFontSize.Y && + expected.FontFamily == actual.FontFamily && + expected.FontWeight == actual.FontWeight && + 0 == wcscmp(expected.FaceName, actual.FaceName); + } + + static bool AreSame(const CONSOLE_FONT_INFOEX& expected, const CONSOLE_FONT_INFOEX& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_FONT_INFOEX& expectedLess, const CONSOLE_FONT_INFOEX& expectedGreater) + { + return expectedLess.dwFontSize.X < expectedGreater.dwFontSize.X && + expectedLess.dwFontSize.Y < expectedGreater.dwFontSize.Y; + } + + static bool IsGreaterThan(const CONSOLE_FONT_INFOEX& expectedGreater, const CONSOLE_FONT_INFOEX& expectedLess) + { + return expectedLess.dwFontSize.X < expectedGreater.dwFontSize.X && + expectedLess.dwFontSize.Y < expectedGreater.dwFontSize.Y; + } + + static bool IsNull(const CONSOLE_FONT_INFOEX& object) + { + return object.nFont == 0 && object.dwFontSize.X == 0 && object.dwFontSize.Y == 0 && + object.FontFamily == 0 && object.FontWeight == 0 && object.FaceName[0] == L'\0'; + } + }; + + template<> + class VerifyOutputTraits < CHAR_INFO > + { + public: + static WEX::Common::NoThrowString ToString(const CHAR_INFO& ci) + { + // 0x2400 is the Unicode symbol for a printable 'NUL' inscribed in a 1 column block. It's for communicating NUL without printing 0x0. + wchar_t const wch = ci.Char.UnicodeChar != L'\0' ? ci.Char.UnicodeChar : 0x2400; + + // 0x20 is a standard space character. + char const ch = ci.Char.AsciiChar != '\0' ? ci.Char.AsciiChar : 0x20; + + return WEX::Common::NoThrowString().Format(L"Unicode Char: %lc (0x%x), Attributes: 0x%x, [Ascii Char: %c (0x%hhx)]", + wch, + ci.Char.UnicodeChar, + ci.Attributes, + ch, + ci.Char.AsciiChar); + } + }; + + template<> + class VerifyCompareTraits < CHAR_INFO, CHAR_INFO > + { + public: + static bool AreEqual(const CHAR_INFO& expected, const CHAR_INFO& actual) + { + return expected.Attributes == actual.Attributes && + expected.Char.UnicodeChar == actual.Char.UnicodeChar; + } + + static bool AreSame(const CHAR_INFO& expected, const CHAR_INFO& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CHAR_INFO&, const CHAR_INFO&) = delete; + + static bool IsGreaterThan(const CHAR_INFO&, const CHAR_INFO&) = delete; + + static bool IsNull(const CHAR_INFO& object) + { + return object.Attributes == 0 && object.Char.UnicodeChar == 0; + } + }; + +} diff --git a/src/inc/contsf.h b/src/inc/contsf.h new file mode 100644 index 000000000..67ac9a554 --- /dev/null +++ b/src/inc/contsf.h @@ -0,0 +1,40 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + contsf.h + +Abstract: + + This module contains the internal structures and definitions used + by the console IME. + +Author: + + v-HirShi Jul.4.1995 + +Revision History: + +--*/ + +#pragma once + +#include "conime.h" + +#ifdef __cplusplus +extern "C" { +#endif + + typedef RECT(*GetSuggestionWindowPos)(); + + BOOL ActivateTextServices(HWND hwndConsole, GetSuggestionWindowPos pfnPosition); + void DeactivateTextServices(); + BOOL NotifyTextServices(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* lplResult); + +#ifdef __cplusplus +} +#endif + diff --git a/src/inc/cpl_core.h b/src/inc/cpl_core.h new file mode 100644 index 000000000..6652bb555 --- /dev/null +++ b/src/inc/cpl_core.h @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +/* +* General rules for being installed in the Control Panel: +* +* 1) The CPL/DLL must export a function named CPlApplet which will handle +* the messages discussed below. +* 2) If the applet needs to save information in CONTROL.INI minimize +* clutter by using the application name [MMCPL.appletname]. +* 3) If the applet is refrenced in CONTROL.INI under [MMCPL] use +* the following form: +* ... +* [MMCPL] +* uniqueName=c:\mydir\myapplet.dll +* ... +* +* The order applet CPLs/DLLs are loaded by Control Panel is not guaranteed. +* They may be sorted for display, categorization, etc. +* +*/ +#ifndef _INC_CPL +#define _INC_CPL + + +#include /* Assume byte packing throughout */ + +#ifdef __cplusplus +extern "C" { /* Assume C declarations for C++ */ +#endif /* __cplusplus */ + +/* Deprecated; control.exe no longer uses these messages */ +#define WM_CPL_LAUNCH (WM_USER+1000) +#define WM_CPL_LAUNCHED (WM_USER+1001) + +/* A function prototype for CPlApplet() */ + +typedef LONG (APIENTRY *APPLET_PROC)(HWND hwndCpl, UINT msg, LPARAM lParam1, LPARAM lParam2); + +/* The data structure CPlApplet() must fill in. */ + +typedef struct tagCPLINFO +{ + int idIcon; /* icon resource id, provided by CPlApplet() */ + int idName; /* display name string resource id, provided by CPlApplet() */ + int idInfo; /* description/tooltip/status bar string resource id, provided by CPlApplet() */ + LONG_PTR lData; /* user defined data */ +} CPLINFO, *LPCPLINFO; + +typedef struct tagNEWCPLINFOA +{ + DWORD dwSize; /* size, in bytes, of the structure */ + DWORD dwFlags; + DWORD dwHelpContext; /* help context to use */ + LONG_PTR lData; /* user defined data */ + HICON hIcon; /* icon to use, this is owned by the Control Panel window (may be deleted) */ + CHAR szName[32]; /* display name */ + CHAR szInfo[64]; /* description/tooltip/status bar string */ + CHAR szHelpFile[128];/* path to help file to use */ +} NEWCPLINFOA, *LPNEWCPLINFOA; +typedef struct tagNEWCPLINFOW +{ + DWORD dwSize; /* size, in bytes, of the structure */ + DWORD dwFlags; + DWORD dwHelpContext; /* help context to use */ + LONG_PTR lData; /* user defined data */ + HICON hIcon; /* icon to use, this is owned by the Control Panel window (may be deleted) */ + WCHAR szName[32]; /* display name */ + WCHAR szInfo[64]; /* description/tooltip/status bar string */ + WCHAR szHelpFile[128];/* path to help file to use */ +} NEWCPLINFOW, *LPNEWCPLINFOW; +#ifdef UNICODE +typedef NEWCPLINFOW NEWCPLINFO; +typedef LPNEWCPLINFOW LPNEWCPLINFO; +#else +typedef NEWCPLINFOA NEWCPLINFO; +typedef LPNEWCPLINFOA LPNEWCPLINFO; +#endif // UNICODE + +#if(WINVER >= 0x0400) +#define CPL_DYNAMIC_RES 0 +/* This constant may be used in place of real resource IDs for the idIcon, +* idName or idInfo members of the CPLINFO structure. Normally, the system +* uses these values to extract copies of the resources and store them in a +* cache. Once the resource information is in the cache, the system does not +* need to load a CPL unless the user actually tries to use it. +* CPL_DYNAMIC_RES tells the system not to cache the resource, but instead to +* load the CPL every time it needs to display information about an item. This +* allows a CPL to dynamically decide what information will be displayed, but +* is SIGNIFICANTLY SLOWER than displaying information from a cache. +* Typically, CPL_DYNAMIC_RES is used when a control panel must inspect the +* runtime status of some device in order to provide text or icons to display. +* It should be avoided if possible because of the performance hit to Control Panel. +*/ + +#endif /* WINVER >= 0x0400 */ + +/* The messages CPlApplet() must handle: */ + +#define CPL_INIT 1 +/* This message is sent to indicate CPlApplet() was found. */ +/* lParam1 and lParam2 are not defined. */ +/* Return TRUE or FALSE indicating whether the control panel should proceed. */ + + +#define CPL_GETCOUNT 2 +/* This message is sent to determine the number of applets to be displayed. */ +/* lParam1 and lParam2 are not defined. */ +/* Return the number of applets you wish to display in the control */ +/* panel window. */ + + +#define CPL_INQUIRE 3 +/* This message is sent for information about each applet. */ +/* The return value is ignored. */ +/* lParam1 is the applet number to register, a value from 0 to */ +/* (CPL_GETCOUNT - 1). lParam2 is a pointer to a CPLINFO structure. */ +/* Fill in CPLINFO's idIcon, idName, idInfo and lData fields with */ +/* the resource id for an icon to display, name and description string ids, */ +/* and a long data item associated with applet #lParam1. This information */ +/* may be cached by the caller at runtime and/or across sessions. */ +/* To prevent caching, see CPL_DYNAMIC_RES, above. If the icon, name, and description */ +/* are not dynamic then CPL_DYNAMIC_RES should not be used and the CPL_NEWINQURE message */ +/* should be ignored */ + + +#define CPL_SELECT 4 +/* The CPL_SELECT message is not used. */ + + +#define CPL_DBLCLK 5 +/* This message is sent when the applet's icon has been double-clicked. */ +/* lParam1 is the applet number which was selected. */ +/* lParam2 is the applet's lData value. */ +/* This message should initiate the applet's dialog box. */ + + +#define CPL_STOP 6 +/* This message is sent for each applet when the control panel is exiting. */ +/* lParam1 is the applet number. lParam2 is the applet's lData value. */ +/* Do applet specific cleaning up here. */ + + +#define CPL_EXIT 7 +/* This message is sent just before the control panel calls FreeLibrary. */ +/* lParam1 and lParam2 are not defined. */ +/* Do non-applet specific cleaning up here. */ + + +#define CPL_NEWINQUIRE 8 +/* Same as CPL_INQUIRE execpt lParam2 is a pointer to a NEWCPLINFO struct. */ +/* The return value is ignored. */ +/* A CPL should NOT respond to the CPL_NEWINQURE message unless CPL_DYNAMIC_RES */ +/* is used in CPL_INQUIRE. CPLs which respond to CPL_NEWINQUIRE cannot be cached */ +/* and slow the loading of the Control Panel window. */ + +#if(WINVER >= 0x0400) +#define CPL_STARTWPARMSA 9 +#define CPL_STARTWPARMSW 10 +#ifdef UNICODE +#define CPL_STARTWPARMS CPL_STARTWPARMSW +#else +#define CPL_STARTWPARMS CPL_STARTWPARMSA +#endif +/* This message parallels CPL_DBLCLK in that the applet should initiate +* its dialog box. Where it differs is that this invocation is coming +* out of RUNDLL, and there may be some extra directions for execution. +* lParam1: the applet number. +* lParam2: an LPSTR to any extra directions that might exist. +* returns: TRUE if the message was handled; FALSE if not. +*/ +#endif /* WINVER >= 0x0400 */ + +/* This message is internal to the Control Panel and MAIN applets. */ +/* It is only sent when an applet is invoked from the command line */ +/* during system installation. */ +#define CPL_SETUP 200 + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#include + +#endif /* _INC_CPL */ + diff --git a/src/inc/operators.hpp b/src/inc/operators.hpp new file mode 100644 index 000000000..c1f018b9a --- /dev/null +++ b/src/inc/operators.hpp @@ -0,0 +1,59 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- operators.hpp + +Abstract: +- This file contains helpful operator overloading for some older data structures. + +Author(s): +- Austin Diviness (AustDi) Mar 2017 +--*/ + +#pragma once + +constexpr bool operator==(const COORD& a, const COORD& b) noexcept +{ + return (a.X == b.X && + a.Y == b.Y); +} + +constexpr bool operator!=(const COORD& a, const COORD& b) noexcept +{ + return !(a == b); +} + +constexpr bool operator==(const SMALL_RECT& a, const SMALL_RECT& b) noexcept +{ + return (a.Top == b.Top && + a.Left == b.Left && + a.Bottom == b.Bottom && + a.Right == b.Right); +} + +constexpr bool operator!=(const SMALL_RECT& a, const SMALL_RECT& b) noexcept +{ + return !(a == b); +} + +constexpr bool operator==(const std::wstring& wstr, const std::wstring_view& wstrView) +{ + return (wstrView == std::wstring_view{ wstr.c_str(), wstr.size() }); +} + +constexpr bool operator==(const std::wstring_view& wstrView, const std::wstring& wstr) +{ + return (wstr == wstrView); +} + +constexpr bool operator!=(const std::wstring& wstr, const std::wstring_view& wstrView) +{ + return !(wstr == wstrView); +} + +constexpr bool operator!=(const std::wstring_view& wstrView, const std::wstring& wstr) +{ + return !(wstr == wstrView); +} diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp new file mode 100644 index 000000000..96808713e --- /dev/null +++ b/src/inc/test/CommonState.hpp @@ -0,0 +1,326 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- CommonState.hpp + +Abstract: +- This represents common boilerplate state setup required for unit tests to run + +Author(s): +- Michael Niksa (miniksa) 18-Jun-2014 +- Paul Campbell (paulcam) 18-Jun-2014 + +Revision History: +- Tranformed to header-only class so it can be included by multiple +unit testing projects in the codebase without a bunch of overhead. +--*/ + +#pragma once + +#define VERIFY_SUCCESS_NTSTATUS(x) VERIFY_IS_TRUE(NT_SUCCESS(x)) + +#include "precomp.h" +#include "../host/globals.h" +#include "../host/inputReadHandleData.h" +#include "../buffer/out/CharRow.hpp" +#include "../interactivity/inc/ServiceLocator.hpp" + +class CommonState +{ +public: + + static const SHORT s_csWindowWidth = 80; + static const SHORT s_csWindowHeight = 80; + static const SHORT s_csBufferWidth = 80; + static const SHORT s_csBufferHeight = 300; + + CommonState() : + m_heap(GetProcessHeap()), + m_ntstatusTextBufferInfo(STATUS_FAIL_CHECK), + m_pFontInfo(nullptr), + m_backupTextBufferInfo(), + m_readHandle(nullptr) + { + } + + ~CommonState() + { + m_heap = nullptr; + } + + void InitEvents() + { + ServiceLocator::LocateGlobals().hInputEvent.create(wil::EventOptions::ManualReset); + } + + void PrepareReadHandle() + { + m_readHandle = std::make_unique(); + } + + void CleanupReadHandle() + { + m_readHandle.reset(nullptr); + } + + void PrepareGlobalFont() + { + COORD coordFontSize; + coordFontSize.X = 8; + coordFontSize.Y = 12; + m_pFontInfo = new FontInfo(L"Consolas", 0, 0, coordFontSize, 0); + } + + void CleanupGlobalFont() + { + if (m_pFontInfo != nullptr) + { + delete m_pFontInfo; + } + } + + void PrepareGlobalScreenBuffer() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + COORD coordWindowSize; + coordWindowSize.X = s_csWindowWidth; + coordWindowSize.Y = s_csWindowHeight; + + COORD coordScreenBufferSize; + coordScreenBufferSize.X = s_csBufferWidth; + coordScreenBufferSize.Y = s_csBufferHeight; + + UINT uiCursorSize = 12; + + THROW_IF_FAILED(SCREEN_INFORMATION::CreateInstance(coordWindowSize, + *m_pFontInfo, + coordScreenBufferSize, + gci.GetDefaultAttributes(), + TextAttribute{ FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_RED }, + uiCursorSize, + &gci.pCurrentScreenBuffer)); + } + + void CleanupGlobalScreenBuffer() + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + delete gci.pCurrentScreenBuffer; + } + + void PrepareGlobalInputBuffer() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.pInputBuffer = new InputBuffer(); + } + + void CleanupGlobalInputBuffer() + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + delete gci.pInputBuffer; + } + + void PrepareCookedReadData() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto* readData = new COOKED_READ_DATA(gci.pInputBuffer, + m_readHandle.get(), + gci.GetActiveOutputBuffer(), + 0, + nullptr, + 0, + nullptr, + L"", + {}); + gci.SetCookedReadData(readData); + } + + void CleanupCookedReadData() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + delete &gci.CookedReadData(); + gci.SetCookedReadData(nullptr); + } + + void PrepareNewTextBufferInfo() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + COORD coordScreenBufferSize; + coordScreenBufferSize.X = s_csBufferWidth; + coordScreenBufferSize.Y = s_csBufferHeight; + + UINT uiCursorSize = 12; + + m_backupTextBufferInfo.swap(gci.pCurrentScreenBuffer->_textBuffer); + try + { + std::unique_ptr textBuffer = std::make_unique(coordScreenBufferSize, + TextAttribute{FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY}, + uiCursorSize, + gci.pCurrentScreenBuffer->GetRenderTarget()); + if (textBuffer.get() == nullptr) + { + m_ntstatusTextBufferInfo = STATUS_NO_MEMORY; + } + else + { + m_ntstatusTextBufferInfo = STATUS_SUCCESS; + } + gci.pCurrentScreenBuffer->_textBuffer.swap(textBuffer); + } + catch (...) + { + m_ntstatusTextBufferInfo = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + } + + void CleanupNewTextBufferInfo() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + VERIFY_IS_TRUE(gci.HasActiveOutputBuffer()); + + gci.pCurrentScreenBuffer->_textBuffer.swap(m_backupTextBufferInfo); + } + + void FillTextBuffer() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // fill with some assorted text that doesn't consume the whole row + const SHORT cRowsToFill = 4; + + VERIFY_IS_TRUE(gci.HasActiveOutputBuffer()); + + TextBuffer& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); + + for (SHORT iRow = 0; iRow < cRowsToFill; iRow++) + { + ROW& row = textBuffer.GetRowByOffset(iRow); + FillRow(&row); + } + + textBuffer.GetCursor().SetYPosition(cRowsToFill); + } + + void FillTextBufferBisect() + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // fill with some text that fills the whole row and has bisecting double byte characters + const SHORT cRowsToFill = s_csBufferHeight; + + VERIFY_IS_TRUE(gci.HasActiveOutputBuffer()); + + TextBuffer& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); + + for (SHORT iRow = 0; iRow < cRowsToFill; iRow++) + { + ROW& row = textBuffer.GetRowByOffset(iRow); + FillBisect(&row); + } + + textBuffer.GetCursor().SetYPosition(cRowsToFill); + } + + NTSTATUS GetTextBufferInfoInitResult() + { + return m_ntstatusTextBufferInfo; + } + +private: + HANDLE m_heap; + NTSTATUS m_ntstatusTextBufferInfo; + FontInfo* m_pFontInfo; + std::unique_ptr m_backupTextBufferInfo; + std::unique_ptr m_readHandle; + + void FillRow(ROW* pRow) + { + // fill a row + // 9 characters, 6 spaces. 15 total + // か = \x304b + // き = \x304d + const PCWSTR pwszText = L"AB" L"\x304b\x304b" L"C" L"\x304d\x304d" L"DE "; + const size_t length = wcslen(pwszText); + + std::vector attrs(length, DbcsAttribute()); + // set double-byte/double-width attributes + attrs[2].SetLeading(); + attrs[3].SetTrailing(); + attrs[5].SetLeading(); + attrs[6].SetTrailing(); + + CharRow& charRow = pRow->GetCharRow(); + OverwriteColumns(pwszText, pwszText + length, attrs.cbegin(), charRow.begin()); + + // set some colors + TextAttribute Attr = TextAttribute(0); + pRow->GetAttrRow().Reset(Attr); + // A = bright red on dark gray + // This string starts at index 0 + Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY); + pRow->GetAttrRow().SetAttrToEnd(0, Attr); + + // BかC = dark gold on bright blue + // This string starts at index 1 + Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY); + pRow->GetAttrRow().SetAttrToEnd(1, Attr); + + // き = bright white on dark purple + // This string starts at index 5 + Attr = TextAttribute(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_BLUE); + pRow->GetAttrRow().SetAttrToEnd(5, Attr); + + // DE = black on dark green + // This string starts at index 7 + Attr = TextAttribute(BACKGROUND_GREEN); + pRow->GetAttrRow().SetAttrToEnd(7, Attr); + + // odd rows forced a wrap + if (pRow->GetId() % 2 != 0) + { + pRow->GetCharRow().SetWrapForced(true); + } + else + { + pRow->GetCharRow().SetWrapForced(false); + } + } + + void FillBisect(ROW *pRow) + { + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // length 80 string of text with bisecting characters at the beginning and end. + // positions of き(\x304d) are at 0, 27-28, 39-40, 67-68, 79 + PWCHAR pwszText = + L"\x304d" + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + L"\x304d\x304d" + L"0123456789" + L"\x304d\x304d" + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + L"\x304d\x304d" + L"0123456789" + L"\x304d"; + const size_t length = wcslen(pwszText); + + std::vector attrs(length, DbcsAttribute()); + // set double-byte/double-width attributes + attrs[0].SetTrailing(); + attrs[27].SetLeading(); + attrs[28].SetTrailing(); + attrs[39].SetLeading(); + attrs[40].SetTrailing(); + attrs[67].SetLeading(); + attrs[68].SetTrailing(); + attrs[79].SetLeading(); + + CharRow& charRow = pRow->GetCharRow(); + OverwriteColumns(pwszText, pwszText + length, attrs.cbegin(), charRow.begin()); + + // everything gets default attributes + pRow->GetAttrRow().Reset(gci.GetActiveOutputBuffer().GetAttributes()); + + pRow->GetCharRow().SetWrapForced(true); + } +}; diff --git a/src/inc/unicode.hpp b/src/inc/unicode.hpp new file mode 100644 index 000000000..b7ea15e03 --- /dev/null +++ b/src/inc/unicode.hpp @@ -0,0 +1,60 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- unicode.hpp + +Abstract: +- This file contains global vars for some common wchar values. +- taken from input.h + +Author(s): +- Austin Diviness (AustDi) Oct 2017 +--*/ + +#pragma once + +#define CP_UTF8 65001 +#define CP_USA 437 +#define CP_KOREAN 949 +#define CP_JAPANESE 932 +#define CP_CHINESE_SIMPLIFIED 936 +#define CP_CHINESE_TRADITIONAL 950 + +#define IsBilingualCP(cp) ((cp)==CP_JAPANESE || (cp)==CP_KOREAN) +#define IsEastAsianCP(cp) ((cp)==CP_JAPANESE || (cp)==CP_KOREAN || (cp)==CP_CHINESE_TRADITIONAL || (cp)==CP_CHINESE_SIMPLIFIED) + +// UNICODE_NULL is a Windows macro definition +const wchar_t UNICODE_BACKSPACE = 0x8; +const wchar_t UNICODE_DEL = 0x7f; +// NOTE: This isn't actually a backspace. It's a graphical block. But +// I believe it's emitted by one of our ANSI/OEM --> Unicode conversions. +// We should dig further into this in the future. +const wchar_t UNICODE_BACKSPACE2 = 0x25d8; +const wchar_t UNICODE_CARRIAGERETURN = 0x0d; +const wchar_t UNICODE_LINEFEED = 0x0a; +const wchar_t UNICODE_BELL = 0x07; +const wchar_t UNICODE_TAB = 0x09; +const wchar_t UNICODE_SPACE = 0x20; +const wchar_t UNICODE_LEFT_SMARTQUOTE = 0x201c; +const wchar_t UNICODE_RIGHT_SMARTQUOTE = 0x201d; +const wchar_t UNICODE_EM_DASH = 0x2014; +const wchar_t UNICODE_EN_DASH = 0x2013; +const wchar_t UNICODE_NBSP = 0xa0; +const wchar_t UNICODE_NARROW_NBSP = 0x202f; +const wchar_t UNICODE_QUOTE = L'\"'; +const wchar_t UNICODE_HYPHEN = L'-'; +const wchar_t UNICODE_BOX_DRAW_LIGHT_DOWN_AND_RIGHT = 0x250c; +const wchar_t UNICODE_BOX_DRAW_LIGHT_DOWN_AND_LEFT = 0x2510; +const wchar_t UNICODE_BOX_DRAW_LIGHT_HORIZONTAL = 0x2500; +const wchar_t UNICODE_BOX_DRAW_LIGHT_VERTICAL = 0x2502; +const wchar_t UNICODE_BOX_DRAW_LIGHT_UP_AND_RIGHT = 0x2514; +const wchar_t UNICODE_BOX_DRAW_LIGHT_UP_AND_LEFT = 0x2518; +const wchar_t UNICODE_INVALID = 0xFFFF; + +// This is the "Ctrl+C" character. +// With VKey='C', it generates a CTRL_C_EVENT +// With VKey=VK_CANCEL (0x3), it generates a CTRL_BREAK_EVENT +const wchar_t UNICODE_ETX = L'\x3'; +const wchar_t UNICODE_REPLACEMENT = 0xFFFD; diff --git a/src/interactivity/base/ApiDetector.cpp b/src/interactivity/base/ApiDetector.cpp new file mode 100644 index 000000000..bfefc0bf9 --- /dev/null +++ b/src/interactivity/base/ApiDetector.cpp @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiDetector.hpp" + +#include + +#pragma hdrstop + +using namespace Microsoft::Console::Interactivity; + +// API Sets +#define EXT_API_SET_NTUSER_WINDOW L"ext-ms-win-ntuser-window-l1-1-0" + +// This may not be defined depending on the SDK version being targetted. +#ifndef LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER +#define LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER 0x00004000 +#endif + +#pragma region Public Methods + +// Routine Description +// - This routine detects whether the system hosts the extension API set that +// includes, among others, CreateWindowExW. +// Arguments: +// - level - pointer to an APILevel enum stating the level of support the +// system offers for the given functionality. +[[nodiscard]] +NTSTATUS ApiDetector::DetectNtUserWindow(_Out_ ApiLevel* level) +{ + // N.B.: Testing for the API set implies the function is present. + return DetectApiSupport(EXT_API_SET_NTUSER_WINDOW, nullptr, level); +} + +#pragma endregion + +#pragma region Private Methods + +[[nodiscard]] +NTSTATUS ApiDetector::DetectApiSupport(_In_ LPCWSTR lpApiHost, _In_ LPCSTR lpProcedure, _Out_ ApiLevel* level) +{ + if (!level) + { + return STATUS_INVALID_PARAMETER; + } + + NTSTATUS status = STATUS_SUCCESS; + HMODULE hModule = nullptr; + + status = TryLoadWellKnownLibrary(lpApiHost, &hModule); + if (NT_SUCCESS(status) && lpProcedure) + { + status = TryLocateProcedure(hModule, lpProcedure); + } + + SetLevelAndFreeIfNecessary(status, hModule, level); + + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS ApiDetector::TryLoadWellKnownLibrary(_In_ LPCWSTR lpLibrary, _Outptr_result_nullonfailure_ HMODULE *phModule) +{ + NTSTATUS status = STATUS_SUCCESS; + + // N.B.: Suppose we attempt to load user32.dll and locate CreateWindowExW + // on a Nano Server system with reverse forwarders enabled. Since the + // reverse forwarder modules have the same name as their regular + // counterparts, the loader will claim to have found the module. In + // addition, since reverse forwarders contain all the functions of + // their regular counterparts, just stubbed to return or set the last + // error to STATUS_NOT_IMPLEMENTED, GetProcAddress will indeed + // indicate that the procedure exists. Hence, we need to search for + // modules skipping over the reverse forwarders. + // + // This however has the side-effect of not working on downlevel. + // LoadLibraryEx asserts that the flags passed in are valid. If any + // invalid flags are passed, it sets the last error to + // STATUS_INVALID_PARAMETER and returns. Since + // LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER does not exist on + // downlevel Windows, the call will fail there. + // + // To counteract that, we try to load the library skipping forwarders + // first under the assumption that we are running on a sufficiently + // new system. If the call fails with STATUS_INVALID_PARAMETER, we + // know there is a problem with the flags, so we try again without + // the NO_FORWARDER part. Because reverse forwarders do not exist on + // downlevel (i.e. < Windows 10), we do not run the risk of failing + // to accurately detect system functionality there. + // + // N.B.: We cannot use IsWindowsVersionOrGreater or associated helper API's + // because those are subject to manifesting and may tell us we are + // running on Windows 8 even if we are running on Windows 10. + // + // TODO: MSFT 10916452 Determine what manifest magic is required to make + // versioning API's behave sanely. + + status = TryLoadWellKnownLibrary(lpLibrary, LOAD_LIBRARY_SEARCH_SYSTEM32_NO_FORWARDER, phModule); + if (!NT_SUCCESS(status) && GetLastError() == STATUS_INVALID_PARAMETER) + { + status = TryLoadWellKnownLibrary(lpLibrary, LOAD_LIBRARY_SEARCH_SYSTEM32, phModule); + } + + return status; +} + +[[nodiscard]] +NTSTATUS ApiDetector::TryLoadWellKnownLibrary(_In_ LPCWSTR lpLibrary, _In_ DWORD dwLoaderFlags, _Outptr_result_nullonfailure_ HMODULE *phModule) +{ + HMODULE hModule = nullptr; + + hModule = LoadLibraryExW(lpLibrary, + nullptr, + dwLoaderFlags); + if (hModule) + { + *phModule = hModule; + + return STATUS_SUCCESS; + } + else + { + return STATUS_UNSUCCESSFUL; + } +} + +[[nodiscard]] +NTSTATUS ApiDetector::TryLocateProcedure(_In_ HMODULE hModule, _In_ LPCSTR lpProcedure) +{ + FARPROC proc = GetProcAddress(hModule, lpProcedure); + + if (proc) + { + return STATUS_SUCCESS; + } + else + { + return STATUS_UNSUCCESSFUL; + } +} + +void ApiDetector::SetLevelAndFreeIfNecessary(_In_ NTSTATUS status, _In_ HMODULE hModule, _Out_ ApiLevel* level) +{ + if (NT_SUCCESS(status)) + { + *level = ApiLevel::Win32; + } + else + { + FreeLibrary(hModule); + + *level = ApiLevel::OneCore; + } +} + +#pragma endregion diff --git a/src/interactivity/base/ApiDetector.hpp b/src/interactivity/base/ApiDetector.hpp new file mode 100644 index 000000000..b7fdb3a56 --- /dev/null +++ b/src/interactivity/base/ApiDetector.hpp @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity +{ + enum class ApiLevel + { + Win32, + OneCore + }; + + class ApiDetector final + { + public: + [[nodiscard]] + static NTSTATUS DetectNtUserWindow(_Out_ ApiLevel* level); + + private: + [[nodiscard]] + static NTSTATUS DetectApiSupport(_In_ LPCWSTR lpLibrary, _In_ LPCSTR lpApi, _Out_ ApiLevel* level); + [[nodiscard]] + static NTSTATUS TryLoadWellKnownLibrary(_In_ LPCWSTR library, _Outptr_result_nullonfailure_ HMODULE* module); + [[nodiscard]] + static NTSTATUS TryLocateProcedure(_In_ HMODULE hModule, _In_ LPCSTR lpProcedure); + static void SetLevelAndFreeIfNecessary(_In_ NTSTATUS status, _In_ HMODULE hModule, _Out_ ApiLevel* level); + [[nodiscard]] + static NTSTATUS TryLoadWellKnownLibrary(_In_ LPCWSTR lpLibrary, + _In_ DWORD dwLoaderFlags, + _Outptr_result_nullonfailure_ HMODULE *phModule); + }; +} diff --git a/src/interactivity/base/InteractivityFactory.cpp b/src/interactivity/base/InteractivityFactory.cpp new file mode 100644 index 000000000..7566afec4 --- /dev/null +++ b/src/interactivity/base/InteractivityFactory.cpp @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "InteractivityFactory.hpp" + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "..\onecore\AccessibilityNotifier.hpp" +#include "..\onecore\ConsoleControl.hpp" +#include "..\onecore\ConsoleInputThread.hpp" +#include "..\onecore\ConsoleWindow.hpp" +#include "..\onecore\ConIoSrvComm.hpp" +#include "..\onecore\SystemConfigurationProvider.hpp" +#include "..\onecore\WindowMetrics.hpp" +#endif + +#include "..\win32\AccessibilityNotifier.hpp" +#include "..\win32\ConsoleControl.hpp" +#include "..\win32\ConsoleInputThread.hpp" +#include "..\win32\InputServices.hpp" +#include "..\win32\WindowDpiApi.hpp" +#include "..\win32\WindowMetrics.hpp" +#include "..\win32\SystemConfigurationProvider.hpp" + +#pragma hdrstop + +using namespace std; +using namespace Microsoft::Console::Interactivity; + +#pragma region Public Methods + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateConsoleControl(_Inout_ std::unique_ptr& control) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newControl; + try + { + switch (level) + { + case ApiLevel::Win32: + newControl = std::make_unique(); + break; + +#ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newControl = std::make_unique(); + break; +#endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + control.swap(newControl); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateConsoleInputThread(_Inout_ std::unique_ptr& thread) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newThread; + try + { + switch (level) + { + case ApiLevel::Win32: + newThread = std::make_unique(); + break; + +#ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newThread = std::make_unique(); + break; +#endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + thread.swap(newThread); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateHighDpiApi(_Inout_ std::unique_ptr& api) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newApi; + try + { + switch (level) + { + case ApiLevel::Win32: + newApi = std::make_unique(); + break; + +#ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newApi.reset(); + break; +#endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + api.swap(newApi); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateWindowMetrics(_Inout_ std::unique_ptr& metrics) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newMetrics; + try + { + switch (level) + { + case ApiLevel::Win32: + newMetrics = std::make_unique(); + break; + +#ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newMetrics = std::make_unique(); + break; +#endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + metrics.swap(newMetrics); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateAccessibilityNotifier(_Inout_ std::unique_ptr& notifier) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newNotifier; + try + { + switch (level) + { + case ApiLevel::Win32: + newNotifier = std::make_unique(); + break; + + #ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newNotifier = std::make_unique(); + break; + #endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + notifier.swap(newNotifier); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateSystemConfigurationProvider(_Inout_ std::unique_ptr& provider) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr NewProvider; + try + { + switch (level) + { + case ApiLevel::Win32: + NewProvider = std::make_unique(); + break; + + #ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + NewProvider = std::make_unique(); + break; + #endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + provider.swap(NewProvider); + } + } + + return status; +} + +[[nodiscard]] +NTSTATUS InteractivityFactory::CreateInputServices(_Inout_ std::unique_ptr& services) +{ + NTSTATUS status = STATUS_SUCCESS; + + ApiLevel level; + status = ApiDetector::DetectNtUserWindow(&level); + + if (NT_SUCCESS(status)) + { + std::unique_ptr newServices; + try + { + switch (level) + { + case ApiLevel::Win32: + newServices = std::make_unique(); + break; + + #ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + newServices = std::make_unique(); + break; + #endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + services.swap(newServices); + } + } + + return status; +} + +// Method Description: +// - Attempts to instantiate a "pseudo window" for when we're operating in +// pseudoconsole mode. There are some tools (cygwin & derivatives) that use +// the GetConsoleWindow API to uniquely identify console sessions. This +// function is used to create an invisible window for that scenario, so +// that GetConsoleWindow returns a real value. +// Arguments: +// - hwnd: Recieves the value of the newly created window's HWND. +// Return Value: +// - STATUS_SUCCESS on success, otherwise an appropriate error. +[[nodiscard]] +NTSTATUS InteractivityFactory::CreatePseudoWindow(HWND& hwnd) +{ + hwnd = 0; + ApiLevel level; + NTSTATUS status = ApiDetector::DetectNtUserWindow(&level);; + if (NT_SUCCESS(status)) + { + try + { + static const wchar_t* const PSEUDO_WINDOW_CLASS = L"PseudoConsoleWindow"; + WNDCLASS pseudoClass {0}; + switch (level) + { + case ApiLevel::Win32: + pseudoClass.lpszClassName = PSEUDO_WINDOW_CLASS; + pseudoClass.lpfnWndProc = DefWindowProc; + RegisterClass(&pseudoClass); + // Attempt to create window + hwnd = CreateWindowExW( + 0, PSEUDO_WINDOW_CLASS, nullptr, WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, HWND_DESKTOP, nullptr, nullptr, nullptr + ); + if (hwnd == nullptr) + { + DWORD const gle = GetLastError(); + status = NTSTATUS_FROM_WIN32(gle); + } + break; + + #ifdef BUILD_ONECORE_INTERACTIVITY + case ApiLevel::OneCore: + hwnd = 0; + status = STATUS_SUCCESS; + break; + #endif + default: + status = STATUS_INVALID_LEVEL; + break; + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + } + + return status; +} +#pragma endregion diff --git a/src/interactivity/base/InteractivityFactory.hpp b/src/interactivity/base/InteractivityFactory.hpp new file mode 100644 index 000000000..ad830bd5f --- /dev/null +++ b/src/interactivity/base/InteractivityFactory.hpp @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "precomp.h" + +#include "ApiDetector.hpp" + +#include "..\inc\IInteractivityFactory.hpp" + +#include + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity +{ + class InteractivityFactory final : public IInteractivityFactory + { + public: + [[nodiscard]] + NTSTATUS CreateConsoleControl(_Inout_ std::unique_ptr& control); + [[nodiscard]] + NTSTATUS CreateConsoleInputThread(_Inout_ std::unique_ptr& thread); + + [[nodiscard]] + NTSTATUS CreateHighDpiApi(_Inout_ std::unique_ptr& api); + [[nodiscard]] + NTSTATUS CreateWindowMetrics(_Inout_ std::unique_ptr& metrics); + [[nodiscard]] + NTSTATUS CreateAccessibilityNotifier(_Inout_ std::unique_ptr& notifier); + [[nodiscard]] + NTSTATUS CreateSystemConfigurationProvider(_Inout_ std::unique_ptr& provider); + [[nodiscard]] + NTSTATUS CreateInputServices(_Inout_ std::unique_ptr& services); + + [[nodiscard]] + NTSTATUS CreatePseudoWindow(HWND& hwnd); + }; +} diff --git a/src/interactivity/base/ServiceLocator.cpp b/src/interactivity/base/ServiceLocator.cpp new file mode 100644 index 000000000..fbebd2e32 --- /dev/null +++ b/src/interactivity/base/ServiceLocator.cpp @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\inc\ServiceLocator.hpp" + +#include "InteractivityFactory.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Interactivity; + +#pragma region Private Static Member Initialization + +std::unique_ptr ServiceLocator::s_interactivityFactory; +std::unique_ptr ServiceLocator::s_consoleControl; +std::unique_ptr ServiceLocator::s_consoleInputThread; +std::unique_ptr ServiceLocator::s_windowMetrics; +std::unique_ptr ServiceLocator::s_accessibilityNotifier; +std::unique_ptr ServiceLocator::s_highDpiApi; +std::unique_ptr ServiceLocator::s_systemConfigurationProvider; +std::unique_ptr ServiceLocator::s_inputServices; + +IConsoleWindow* ServiceLocator::s_consoleWindow = nullptr; + +Globals ServiceLocator::s_globals; + +bool ServiceLocator::s_pseudoWindowInitialized = false; +wil::unique_hwnd ServiceLocator::s_pseudoWindow = 0; + +#pragma endregion + +#pragma region Public Methods + +void ServiceLocator::RundownAndExit(const HRESULT hr) +{ + // MSFT:15506250 + // In VT I/O Mode, a client application might die before we've rendered + // the last bit of text they've emitted. So give the VtRenderer one + // last chance to paint before it is killed. + if (s_globals.pRender) + { + s_globals.pRender->TriggerTeardown(); + } + + // A History Lesson from MSFT: 13576341: + // We introduced RundownAndExit to give services that hold onto important handles + // an opportunity to let those go when we decide to exit from the console for various reasons. + // This was because Console IO Services (ConIoSvcComm) on OneCore editions was holding onto + // pipe and ALPC handles to talk to CSRSS ConIoSrv.dll to broker which console got display/keyboard control. + // If we simply run straight into TerminateProcess, those handles aren't necessarily released right away. + // The terminate operation can have a rundown period of time where APCs are serviced (such as from + // a DirectX kernel callback/flush/cleanup) that can take substantially longer than we expect (several whole seconds). + // This rundown happens before the final destruction of any outstanding handles or resources. + // If someone is waiting on one of those handles or resources outside our process, they're stuck waiting + // for our terminate rundown and can't continue execution until we're done. + // We don't want to have other execution in the system get stuck , so this is a great + // place to clean up and notify any objects or threads in the system that have to cleanup safely before + // we head into TerminateProcess and tear everything else down less gracefully. + + // TODO: MSFT: 14397093 - Expand graceful rundown beyond just the Hot Bug input services case. + + if (s_inputServices.get() != nullptr) + { + s_inputServices.reset(nullptr); + } + + TerminateProcess(GetCurrentProcess(), hr); +} + +#pragma region Creation Methods + +[[nodiscard]] +NTSTATUS ServiceLocator::CreateConsoleInputThread(_Outptr_result_nullonfailure_ IConsoleInputThread** thread) +{ + NTSTATUS status = STATUS_SUCCESS; + + if (s_consoleInputThread) + { + status = STATUS_INVALID_HANDLE; + } + else + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateConsoleInputThread(s_consoleInputThread); + + if (NT_SUCCESS(status)) + { + *thread = s_consoleInputThread.get(); + } + } + } + + return status; +} + +#pragma endregion + +#pragma region Set Methods + +[[nodiscard]] +NTSTATUS ServiceLocator::SetConsoleWindowInstance(_In_ IConsoleWindow* window) +{ + NTSTATUS status = STATUS_SUCCESS; + + if (s_consoleWindow) + { + status = STATUS_INVALID_HANDLE; + } + else if (!window) + { + status = STATUS_INVALID_PARAMETER; + } + else + { + s_consoleWindow = window; + } + + return status; +} + +#pragma endregion + +#pragma region Location Methods + +IConsoleWindow *ServiceLocator::LocateConsoleWindow() +{ + return s_consoleWindow; +} + +IConsoleControl *ServiceLocator::LocateConsoleControl() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_consoleControl) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateConsoleControl(s_consoleControl); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_consoleControl.get(); +} + +IConsoleInputThread* ServiceLocator::LocateConsoleInputThread() +{ + return s_consoleInputThread.get(); +} + +IHighDpiApi* ServiceLocator::LocateHighDpiApi() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_highDpiApi) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateHighDpiApi(s_highDpiApi); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_highDpiApi.get(); +} + +IWindowMetrics* ServiceLocator::LocateWindowMetrics() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_windowMetrics) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateWindowMetrics(s_windowMetrics); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_windowMetrics.get(); +} + +IAccessibilityNotifier* ServiceLocator::LocateAccessibilityNotifier() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_accessibilityNotifier) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateAccessibilityNotifier(s_accessibilityNotifier); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_accessibilityNotifier.get(); +} + +ISystemConfigurationProvider* ServiceLocator::LocateSystemConfigurationProvider() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_systemConfigurationProvider) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateSystemConfigurationProvider(s_systemConfigurationProvider); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_systemConfigurationProvider.get(); +} + +IInputServices* ServiceLocator::LocateInputServices() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (!s_inputServices) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + status = s_interactivityFactory->CreateInputServices(s_inputServices); + } + } + + LOG_IF_NTSTATUS_FAILED(status); + + return s_inputServices.get(); +} + +Globals& ServiceLocator::LocateGlobals() +{ + return s_globals; +} + +// Method Description: +// - Retrieves the pseudo console window, or attempts to instantiate one. +// Arguments: +// - +// Return Value: +// - a reference to the pseudoconsole window. +HWND ServiceLocator::LocatePseudoWindow() +{ + NTSTATUS status = STATUS_SUCCESS; + if (!s_pseudoWindowInitialized) + { + if (s_interactivityFactory.get() == nullptr) + { + status = ServiceLocator::LoadInteractivityFactory(); + } + + if (NT_SUCCESS(status)) + { + HWND hwnd; + status = s_interactivityFactory->CreatePseudoWindow(hwnd); + s_pseudoWindow.reset(hwnd); + } + s_pseudoWindowInitialized = true; + } + LOG_IF_NTSTATUS_FAILED(status); + return s_pseudoWindow.get(); +} + +#pragma endregion + +#pragma endregion + +#pragma region Private Methods + +[[nodiscard]] +NTSTATUS ServiceLocator::LoadInteractivityFactory() +{ + NTSTATUS status = STATUS_SUCCESS; + + if (s_interactivityFactory.get() == nullptr) + { + s_interactivityFactory = std::make_unique(); + status = NT_TESTNULL(s_interactivityFactory.get()); + } + return status; +} + +#pragma endregion diff --git a/src/interactivity/base/VtApiRedirection.cpp b/src/interactivity/base/VtApiRedirection.cpp new file mode 100644 index 000000000..abde80de5 --- /dev/null +++ b/src/interactivity/base/VtApiRedirection.cpp @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\inc\VtApiRedirection.hpp" + +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +UINT VTRedirMapVirtualKeyW(_In_ UINT uCode, _In_ UINT uMapType) +{ + return ServiceLocator::LocateInputServices()->MapVirtualKeyW(uCode, uMapType); +} + +SHORT VTRedirVkKeyScanW(_In_ WCHAR ch) +{ + return ServiceLocator::LocateInputServices()->VkKeyScanW(ch); +} + +SHORT VTRedirGetKeyState(_In_ int nVirtKey) +{ + return ServiceLocator::LocateInputServices()->GetKeyState(nVirtKey); +} diff --git a/src/interactivity/base/coninteractivitybase.rcv b/src/interactivity/base/coninteractivitybase.rcv new file mode 100644 index 000000000..b50f52e74 --- /dev/null +++ b/src/interactivity/base/coninteractivitybase.rcv @@ -0,0 +1,5 @@ +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Interactivity Base" +#define VER_INTERNALNAME_STR "ConInteractivityBase" +#define VER_ORIGINALFILENAME_STR "CONINTERACTIVITYBASE.DLL" diff --git a/src/interactivity/base/dirs b/src/interactivity/base/dirs new file mode 100644 index 000000000..d0cc270d9 --- /dev/null +++ b/src/interactivity/base/dirs @@ -0,0 +1,2 @@ +DIRS=\ + lib \ diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj b/src/interactivity/base/lib/InteractivityBase.vcxproj new file mode 100644 index 000000000..f8d695200 --- /dev/null +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + {06EC74CB-9A12-429C-B551-8562EC964846} + Win32Proj + Base + InteractivityBase + ConInteractivityBaseLib + + + + + \ No newline at end of file diff --git a/src/interactivity/base/lib/InteractivityBase.vcxproj.filters b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters new file mode 100644 index 000000000..a47641e36 --- /dev/null +++ b/src/interactivity/base/lib/InteractivityBase.vcxproj.filters @@ -0,0 +1,78 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Source Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/interactivity/base/lib/product.pbxproj b/src/interactivity/base/lib/product.pbxproj new file mode 100644 index 000000000..a8e53134d --- /dev/null +++ b/src/interactivity/base/lib/product.pbxproj @@ -0,0 +1,11 @@ + + + + v110 + Utility + + + + + + \ No newline at end of file diff --git a/src/interactivity/base/lib/sources b/src/interactivity/base/lib/sources new file mode 100644 index 000000000..676d80aa6 --- /dev/null +++ b/src/interactivity/base/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConInteractivityBaseLib +TARGETTYPE = LIBRARY diff --git a/src/interactivity/base/precomp.cpp b/src/interactivity/base/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/interactivity/base/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/interactivity/base/precomp.h b/src/interactivity/base/precomp.h new file mode 100644 index 000000000..a8b9af497 --- /dev/null +++ b/src/interactivity/base/precomp.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "..\..\host\precomp.h" + +#ifdef BUILD_ONECORE_INTERACTIVITY +// From ntdef.h and winnt.h +typedef char CCHAR; +typedef short CSHORT; +typedef ULONG CLONG; + +typedef CCHAR *PCCHAR; +typedef CSHORT *PCSHORT; +typedef CLONG *PCLONG; + +typedef ULONG LOGICAL; +typedef ULONG *PLOGICAL; +// End ntdef.h and winnt.h + +#include +#endif + +#pragma hdrstop diff --git a/src/interactivity/base/res.rc b/src/interactivity/base/res.rc new file mode 100644 index 000000000..c4cf2665b --- /dev/null +++ b/src/interactivity/base/res.rc @@ -0,0 +1,19 @@ +/****************************** Module Header ******************************\ +* Module Name: res.rc +* +* Copyright (c) 1985-91, Microsoft Corporation +* +* Constants +* +* History: +* 08-21-91 Created. +\***************************************************************************/ + +#include +#include "resource.h" + +#ifndef EXTERNAL_BUILD +#include "coninteractivitybase.rcv" +#include +#include +#endif diff --git a/src/interactivity/base/sources.inc b/src/interactivity/base/sources.inc new file mode 100644 index 000000000..323690abc --- /dev/null +++ b/src/interactivity/base/sources.inc @@ -0,0 +1,51 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Interactivity Base Layer +# ------------------------------------- + +# This module defines the interfaces by which the console will interact +# with a user. This includes unifying various input methods into a single +# abstract method of talking to the console. + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\ApiDetector.cpp \ + ..\InteractivityFactory.cpp \ + ..\ServiceLocator.cpp \ + ..\VtApiRedirection.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\..\..\..\..\ConIoSrv; \ + \ No newline at end of file diff --git a/src/interactivity/dirs b/src/interactivity/dirs new file mode 100644 index 000000000..99e11a993 --- /dev/null +++ b/src/interactivity/dirs @@ -0,0 +1,4 @@ +DIRS=\ + base \ + win32 \ + onecore \ diff --git a/src/interactivity/inc/IAccessibilityNotifier.hpp b/src/interactivity/inc/IAccessibilityNotifier.hpp new file mode 100644 index 000000000..23d4b7f85 --- /dev/null +++ b/src/interactivity/inc/IAccessibilityNotifier.hpp @@ -0,0 +1,49 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IAccessibilityNotifier.hpp + +Abstract: +- Defines accessibility notification methods used by accessibility systems to + provide accessible access to the console. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IAccessibilityNotifier + { + public: + enum class ConsoleCaretEventFlags + { + CaretInvisible, + CaretSelection, + CaretVisible + }; + + virtual ~IAccessibilityNotifier() = 0; + + virtual void NotifyConsoleCaretEvent(_In_ RECT rectangle) = 0; + virtual void NotifyConsoleCaretEvent(_In_ ConsoleCaretEventFlags flags, _In_ LONG position) = 0; + virtual void NotifyConsoleUpdateScrollEvent(_In_ LONG x, _In_ LONG y) = 0; + virtual void NotifyConsoleUpdateSimpleEvent(_In_ LONG start, _In_ LONG charAndAttribute) = 0; + virtual void NotifyConsoleUpdateRegionEvent(_In_ LONG startXY, _In_ LONG endXY) = 0; + virtual void NotifyConsoleLayoutEvent() = 0; + virtual void NotifyConsoleStartApplicationEvent(_In_ DWORD processId) = 0; + virtual void NotifyConsoleEndApplicationEvent(_In_ DWORD processId) = 0; + + protected: + IAccessibilityNotifier() { } + + IAccessibilityNotifier(IAccessibilityNotifier const&) = delete; + IAccessibilityNotifier& operator=(IAccessibilityNotifier const&) = delete; + }; + + inline IAccessibilityNotifier::~IAccessibilityNotifier() {} +} diff --git a/src/interactivity/inc/IConsoleControl.hpp b/src/interactivity/inc/IConsoleControl.hpp new file mode 100644 index 000000000..ec3f30bdf --- /dev/null +++ b/src/interactivity/inc/IConsoleControl.hpp @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IConsoleControl.hpp + +Abstract: +- Defines methods that delegate the execution of privileged operations or notify + Windows subsystems about console state. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IConsoleControl + { + public: + virtual ~IConsoleControl() = 0; + virtual NTSTATUS NotifyConsoleApplication(DWORD dwProcessId) = 0; + virtual NTSTATUS SetForeground(HANDLE hProcess, BOOL fForeground) = 0; + virtual NTSTATUS EndTask(HANDLE hProcessId, DWORD dwEventType, ULONG ulCtrlFlags) = 0; + + protected: + IConsoleControl() { } + + IConsoleControl(IConsoleControl const&) = delete; + IConsoleControl& operator=(IConsoleControl const &) = delete; + }; + + inline IConsoleControl::~IConsoleControl() {} +} diff --git a/src/interactivity/inc/IConsoleInputThread.hpp b/src/interactivity/inc/IConsoleInputThread.hpp new file mode 100644 index 000000000..ff6fd5f8b --- /dev/null +++ b/src/interactivity/inc/IConsoleInputThread.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IConsoleInputThread.hpp + +Abstract: +- Defines methods that wrap the thread that reads input from the keyboard and + feeds it into the console's input buffer. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IConsoleInputThread + { + public: + virtual ~IConsoleInputThread() = 0; + virtual HANDLE Start() = 0; + + HANDLE GetHandle() { return _hThread; } + DWORD GetThreadId() { return _dwThreadId; } + + protected: + // Prevent accidental copies. + IConsoleInputThread(IConsoleInputThread const&) = delete; + IConsoleInputThread& operator=(IConsoleInputThread const&) = delete; + + // .ctor + IConsoleInputThread() : + _hThread(nullptr), + _dwThreadId((DWORD)(-1)) { } + + // Protected Variables + HANDLE _hThread; + DWORD _dwThreadId; + }; + + inline IConsoleInputThread::~IConsoleInputThread() {} +} diff --git a/src/interactivity/inc/IConsoleWindow.hpp b/src/interactivity/inc/IConsoleWindow.hpp new file mode 100644 index 000000000..13e2f0007 --- /dev/null +++ b/src/interactivity/inc/IConsoleWindow.hpp @@ -0,0 +1,75 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IConsoleWindow.hpp + +Abstract: +- Defines the methods and properties of what makes a window into a console + window. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +// copied typedef from uiautomationcore.h +typedef int EVENTID; + +namespace Microsoft::Console::Interactivity +{ + class IConsoleWindow + { + public: + virtual ~IConsoleWindow() = 0; + + virtual BOOL EnableBothScrollBars() = 0; + virtual int UpdateScrollBar(_In_ bool isVertical, + _In_ bool isAltBuffer, + _In_ UINT pageSize, + _In_ int maxSize, + _In_ int viewportPosition) = 0; + + virtual bool IsInFullscreen() const = 0; + + virtual void SetIsFullscreen(const bool fFullscreenEnabled) = 0; + + virtual void ChangeViewport(const SMALL_RECT NewWindow) = 0; + + virtual void CaptureMouse() = 0; + virtual BOOL ReleaseMouse() = 0; + + virtual HWND GetWindowHandle() const = 0; + + // Pass null. + virtual void SetOwner() = 0; + + virtual BOOL GetCursorPosition(_Out_ LPPOINT lpPoint) = 0; + virtual BOOL GetClientRectangle(_Out_ LPRECT lpRect) = 0; + virtual int MapPoints(_Inout_updates_(cPoints) LPPOINT lpPoints, _In_ UINT cPoints) = 0; + virtual BOOL ConvertScreenToClient(_Inout_ LPPOINT lpPoint) = 0; + + virtual BOOL SendNotifyBeep() const = 0; + + virtual BOOL PostUpdateScrollBars() const = 0; + + virtual BOOL PostUpdateWindowSize() const = 0; + + virtual void UpdateWindowSize(const COORD coordSizeInChars) = 0; + virtual void UpdateWindowText() = 0; + + virtual void HorizontalScroll(const WORD wScrollCommand, + const WORD wAbsoluteChange) = 0; + virtual void VerticalScroll(const WORD wScrollCommand, + const WORD wAbsoluteChange) = 0; + [[nodiscard]] + virtual HRESULT SignalUia(_In_ EVENTID id) = 0; + [[nodiscard]] + virtual HRESULT UiaSetTextAreaFocus() = 0; + virtual RECT GetWindowRect() const = 0; + }; + + inline IConsoleWindow::~IConsoleWindow() {} +} diff --git a/src/interactivity/inc/IHighDpiApi.hpp b/src/interactivity/inc/IHighDpiApi.hpp new file mode 100644 index 000000000..53abb1bf0 --- /dev/null +++ b/src/interactivity/inc/IHighDpiApi.hpp @@ -0,0 +1,37 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IHighDpiApi.hpp + +Abstract: +- Defines the methods used by the console to support high DPI displays. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IHighDpiApi + { + public: + virtual BOOL SetProcessDpiAwarenessContext() = 0; + [[nodiscard]] + virtual HRESULT SetProcessPerMonitorDpiAwareness() = 0; + virtual BOOL EnablePerMonitorDialogScaling() = 0; + + virtual ~IHighDpiApi() = 0; + + protected: + IHighDpiApi() { } + + IHighDpiApi(IHighDpiApi const&) = delete; + IHighDpiApi& operator=(IHighDpiApi const&) = delete; + }; + + inline IHighDpiApi::~IHighDpiApi() {} +} diff --git a/src/interactivity/inc/IInputServices.hpp b/src/interactivity/inc/IInputServices.hpp new file mode 100644 index 000000000..79bd44aa1 --- /dev/null +++ b/src/interactivity/inc/IInputServices.hpp @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IInputServices.hpp + +Abstract: +- Defines the methods used by the console to process input. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IInputServices + { + public: + virtual UINT MapVirtualKeyW(_In_ UINT uCode, _In_ UINT uMapType) = 0; + virtual SHORT VkKeyScanW(_In_ WCHAR ch) = 0; + virtual SHORT GetKeyState(_In_ int nVirtKey) = 0; + virtual BOOL TranslateCharsetInfo(_Inout_ DWORD FAR *lpSrc, _Out_ LPCHARSETINFO lpCs, _In_ DWORD dwFlags) = 0; + virtual ~IInputServices() = 0; + + protected: + IInputServices() { } + + IInputServices(IInputServices const&) = delete; + IInputServices& operator=(IInputServices const&) = delete; + }; + + inline IInputServices::~IInputServices() {} +} diff --git a/src/interactivity/inc/IInteractivityFactory.hpp b/src/interactivity/inc/IInteractivityFactory.hpp new file mode 100644 index 000000000..5e95e7ec6 --- /dev/null +++ b/src/interactivity/inc/IInteractivityFactory.hpp @@ -0,0 +1,57 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IInteractivityFactory.hpp + +Abstract: +- Defines methods for a factory class that picks the implementation of + interfaces depending on whether the console is running on OneCore or a larger + edition of Windows with all the requisite API's to run the full console. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "IConsoleControl.hpp" +#include "IConsoleInputThread.hpp" + +#include "IHighDpiApi.hpp" +#include "IWindowMetrics.hpp" +#include "IAccessibilityNotifier.hpp" +#include "ISystemConfigurationProvider.hpp" +#include "IInputServices.hpp" + +#include + +namespace Microsoft::Console::Interactivity +{ + class IInteractivityFactory + { + public: + virtual ~IInteractivityFactory() = 0; + [[nodiscard]] + virtual NTSTATUS CreateConsoleControl(_Inout_ std::unique_ptr& control) = 0; + [[nodiscard]] + virtual NTSTATUS CreateConsoleInputThread(_Inout_ std::unique_ptr& thread) = 0; + + [[nodiscard]] + virtual NTSTATUS CreateHighDpiApi(_Inout_ std::unique_ptr& api) = 0; + [[nodiscard]] + virtual NTSTATUS CreateWindowMetrics(_Inout_ std::unique_ptr& metrics) = 0; + [[nodiscard]] + virtual NTSTATUS CreateAccessibilityNotifier(_Inout_ std::unique_ptr& notifier) = 0; + [[nodiscard]] + virtual NTSTATUS CreateSystemConfigurationProvider(_Inout_ std::unique_ptr& provider) = 0; + [[nodiscard]] + virtual NTSTATUS CreateInputServices(_Inout_ std::unique_ptr& services) = 0; + + [[nodiscard]] + virtual NTSTATUS CreatePseudoWindow(HWND& hwnd) = 0; + }; + + inline IInteractivityFactory::~IInteractivityFactory() {} +} diff --git a/src/interactivity/inc/ISystemConfigurationProvider.hpp b/src/interactivity/inc/ISystemConfigurationProvider.hpp new file mode 100644 index 000000000..50fef9201 --- /dev/null +++ b/src/interactivity/inc/ISystemConfigurationProvider.hpp @@ -0,0 +1,49 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ISystemConfigurationProvider.hpp + +Abstract: +- Defines methods that fetch user settings that can customize the console's + behavior. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +class Settings; + +namespace Microsoft::Console::Interactivity +{ + class ISystemConfigurationProvider + { + public: + virtual ~ISystemConfigurationProvider() = 0; + + virtual bool IsCaretBlinkingEnabled() = 0; + + virtual UINT GetCaretBlinkTime() = 0; + virtual int GetNumberOfMouseButtons() = 0; + virtual ULONG GetCursorWidth() = 0; + virtual ULONG GetNumberOfWheelScrollLines() = 0; + virtual ULONG GetNumberOfWheelScrollCharacters() = 0; + + virtual void GetSettingsFromLink(_Inout_ Settings* pLinkSettings, + _Inout_updates_bytes_(*pdwTitleLength) LPWSTR pwszTitle, + _Inout_ PDWORD pdwTitleLength, + _In_ PCWSTR pwszCurrDir, + _In_ PCWSTR pwszAppName) = 0; + + protected: + ISystemConfigurationProvider() { }; + + ISystemConfigurationProvider(ISystemConfigurationProvider const&) = delete; + ISystemConfigurationProvider& operator=(ISystemConfigurationProvider const&) = delete; + }; + + inline ISystemConfigurationProvider::~ISystemConfigurationProvider() {} +} diff --git a/src/interactivity/inc/IWindowMetrics.hpp b/src/interactivity/inc/IWindowMetrics.hpp new file mode 100644 index 000000000..c13493e41 --- /dev/null +++ b/src/interactivity/inc/IWindowMetrics.hpp @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IWindowMetrics.hpp + +Abstract: +- Defines methods that return the maximum and minimum dimensions permissible for + the console window. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + class IWindowMetrics + { + public: + virtual ~IWindowMetrics() = 0; + virtual RECT GetMinClientRectInPixels() = 0; + virtual RECT GetMaxClientRectInPixels() = 0; + + protected: + IWindowMetrics() { } + + IWindowMetrics(IWindowMetrics const&) = delete; + IWindowMetrics& operator=(IWindowMetrics const&) = delete; + }; + + inline IWindowMetrics::~IWindowMetrics() {} +} diff --git a/src/interactivity/inc/Module.hpp b/src/interactivity/inc/Module.hpp new file mode 100644 index 000000000..cea667e43 --- /dev/null +++ b/src/interactivity/inc/Module.hpp @@ -0,0 +1,30 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Module.hpp + +Abstract: +- Lists all the interfaces for which there exist multiple implementations that + can be picked amongst depending on API's available on the host OS. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity +{ + enum class Module + { + AccessibilityNotifier, + ConsoleControl, + ConsoleInputThread, + ConsoleWindowMetrics, + HighDpiApi, + InputServices, + SystemConfigurationProvider + }; +} diff --git a/src/interactivity/inc/ServiceLocator.hpp b/src/interactivity/inc/ServiceLocator.hpp new file mode 100644 index 000000000..3e419b759 --- /dev/null +++ b/src/interactivity/inc/ServiceLocator.hpp @@ -0,0 +1,115 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ServiceLocator.hpp + +Abstract: +- Locates and holds instances of classes for which multiple implementations + exist depending on API's available on the host OS. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "IInteractivityFactory.hpp" +#include "IConsoleWindow.hpp" +#include "../../host/globals.h" + +#include + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity +{ + class ServiceLocator final + { + public: + + static void RundownAndExit(const HRESULT hr); + + // N.B.: Location methods without corresponding creation methods + // automatically create the singleton object on demand. + // In case the on-demand creation fails, the return value + // is nullptr and a message is logged. + + + static IAccessibilityNotifier *LocateAccessibilityNotifier(); + + static IConsoleControl *LocateConsoleControl(); + template static T *LocateConsoleControl() + { + return static_cast(LocateConsoleControl()); + } + + [[nodiscard]] + static NTSTATUS CreateConsoleInputThread(_Outptr_result_nullonfailure_ IConsoleInputThread** thread); + static IConsoleInputThread *LocateConsoleInputThread(); + template static T *LocateConsoleInputThread() + { + return static_cast(LocateConsoleInputThread()); + } + + [[nodiscard]] + static NTSTATUS SetConsoleWindowInstance(_In_ IConsoleWindow *window); + static IConsoleWindow *LocateConsoleWindow(); + template static T *LocateConsoleWindow() + { + return static_cast(s_consoleWindow); + } + + static IWindowMetrics *LocateWindowMetrics(); + template static T *LocateWindowMetrics() + { + return static_cast(LocateWindowMetrics()); + } + + static IHighDpiApi *LocateHighDpiApi(); + template static T *LocateHighDpiApi() + { + return static_cast(LocateHighDpiApi()); + } + + static IInputServices *LocateInputServices(); + template static T *LocateInputServices() + { + return static_cast(LocateInputServices()); + } + + static ISystemConfigurationProvider *LocateSystemConfigurationProvider(); + + static Globals& LocateGlobals(); + + static HWND LocatePseudoWindow(); + + + protected: + ServiceLocator(ServiceLocator const&) = delete; + ServiceLocator& operator=(ServiceLocator const&) = delete; + + private: + [[nodiscard]] + static NTSTATUS LoadInteractivityFactory(); + + static std::unique_ptr s_interactivityFactory; + + static std::unique_ptr s_accessibilityNotifier; + static std::unique_ptr s_consoleControl; + static std::unique_ptr s_consoleInputThread; + // TODO: MSFT 15344939 - some implementations of IConsoleWindow are currently singleton + // classes so we can't own a pointer to them here. fix this so s_consoleWindow can follow the + // pattern of the rest of the service interface pointers. + static IConsoleWindow* s_consoleWindow; + static std::unique_ptr s_windowMetrics; + static std::unique_ptr s_highDpiApi; + static std::unique_ptr s_systemConfigurationProvider; + static std::unique_ptr s_inputServices; + + static Globals s_globals; + static bool s_pseudoWindowInitialized; + static wil::unique_hwnd s_pseudoWindow; + }; +} diff --git a/src/interactivity/inc/VtApiRedirection.hpp b/src/interactivity/inc/VtApiRedirection.hpp new file mode 100644 index 000000000..66fb24f30 --- /dev/null +++ b/src/interactivity/inc/VtApiRedirection.hpp @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- VtApiRedirection.h + +Abstract: +- Redefines several input-related API's that are not available on OneCore such + that they be redirected through the ServiceLocator via the IInputServices + interface. +- This ensures that all calls to these API's are executed as normal when the + console is running on big Windows, but that they are also redirected to the + Console IO Server when it is running on a OneCore system, where the OneCore + implementations live. + +Author: +- HeGatta Apr.25.2017 +--*/ + +#pragma once + +#define MapVirtualKeyW(x,y) VTRedirMapVirtualKeyW(x,y) +#define VkKeyScanW(x) VTRedirVkKeyScanW(x) +#define GetKeyState(x) VTRedirGetKeyState(x) + +UINT VTRedirMapVirtualKeyW(_In_ UINT uCode, _In_ UINT uMapType); +SHORT VTRedirVkKeyScanW(_In_ WCHAR ch); +SHORT VTRedirGetKeyState(_In_ int nVirtKey); diff --git a/src/interactivity/onecore/AccessibilityNotifier.cpp b/src/interactivity/onecore/AccessibilityNotifier.cpp new file mode 100644 index 000000000..a4a26cc39 --- /dev/null +++ b/src/interactivity/onecore/AccessibilityNotifier.cpp @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "AccessibilityNotifier.hpp" + +using namespace Microsoft::Console::Interactivity::OneCore; + +void AccessibilityNotifier::NotifyConsoleCaretEvent(_In_ RECT /*rectangle*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleCaretEvent(_In_ ConsoleCaretEventFlags /*flags*/, _In_ LONG /*position*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleUpdateScrollEvent(_In_ LONG /*x*/, _In_ LONG /*y*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleUpdateSimpleEvent(_In_ LONG /*start*/, _In_ LONG /*charAndAttribute*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleUpdateRegionEvent(_In_ LONG /*startXY*/, _In_ LONG /*endXY*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleLayoutEvent() +{ +} + +void AccessibilityNotifier::NotifyConsoleStartApplicationEvent(_In_ DWORD /*processId*/) +{ +} + +void AccessibilityNotifier::NotifyConsoleEndApplicationEvent(_In_ DWORD /*processId*/) +{ +} diff --git a/src/interactivity/onecore/AccessibilityNotifier.hpp b/src/interactivity/onecore/AccessibilityNotifier.hpp new file mode 100644 index 000000000..d5ceeaa6b --- /dev/null +++ b/src/interactivity/onecore/AccessibilityNotifier.hpp @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IAccessibilityNotifier.hpp + +Abstract: +- OneCore implementation of the IAccessibilityNotifier interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\IAccessibilityNotifier.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class AccessibilityNotifier sealed : public IAccessibilityNotifier + { + public: + void NotifyConsoleCaretEvent(_In_ RECT rectangle); + void NotifyConsoleCaretEvent(_In_ ConsoleCaretEventFlags flags, _In_ LONG position); + void NotifyConsoleUpdateScrollEvent(_In_ LONG x, _In_ LONG y); + void NotifyConsoleUpdateSimpleEvent(_In_ LONG start, _In_ LONG charAndAttribute); + void NotifyConsoleUpdateRegionEvent(_In_ LONG startXY, _In_ LONG endXY); + void NotifyConsoleLayoutEvent(); + void NotifyConsoleStartApplicationEvent(_In_ DWORD processId); + void NotifyConsoleEndApplicationEvent(_In_ DWORD processId); + }; +} diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp new file mode 100644 index 000000000..4763f0723 --- /dev/null +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "BgfxEngine.hpp" + +#include "ConIoSrvComm.hpp" +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// +// Default non-bright white. +// + +#define DEFAULT_COLOR_ATTRIBUTE (0xC) + +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Interactivity::OneCore; + +BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWidth, LONG FontWidth, LONG FontHeight) + : RenderEngineBase(), + _sharedViewBase((ULONG_PTR)SharedViewBase), + _displayHeight(DisplayHeight), + _displayWidth(DisplayWidth), + _currentLegacyColorAttribute(DEFAULT_COLOR_ATTRIBUTE) +{ + _runLength = sizeof(CD_IO_CHARACTER) * DisplayWidth; + + _fontSize.X = FontWidth > SHORT_MAX ? SHORT_MAX : (SHORT)FontWidth; + _fontSize.Y = FontHeight > SHORT_MAX ? SHORT_MAX : (SHORT)FontHeight; +} + +[[nodiscard]] +HRESULT BgfxEngine::Invalidate(const SMALL_RECT* const /*psrRegion*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateSystem(const RECT* const /*prcDirtyClient*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateSelection(const std::vector& /*rectangles*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateScroll(const COORD* const /*pcoordDelta*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateAll() noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +[[nodiscard]] +HRESULT BgfxEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +[[nodiscard]] +HRESULT BgfxEngine::StartPaint() noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::EndPaint() noexcept +{ + NTSTATUS Status; + + PVOID OldRunBase; + PVOID NewRunBase; + + Status = ServiceLocator::LocateInputServices()->RequestUpdateDisplay(0); + + if (NT_SUCCESS(Status)) + { + for (SHORT i = 0 ; i < _displayHeight ; i++) + { + OldRunBase = (PVOID)(_sharedViewBase + (i * 2 * _runLength)); + NewRunBase = (PVOID)(_sharedViewBase + (i * 2 * _runLength) + _runLength); + memcpy_s(OldRunBase, _runLength, NewRunBase, _runLength); + } + } + + return HRESULT_FROM_NT(Status); +} + +// Routine Description: +// - Used to perform longer running presentation steps outside the lock so the other threads can continue. +// - Not currently used by BgfxEngine. +// Arguments: +// - +// Return Value: +// - S_FALSE since we do nothing. +[[nodiscard]] +HRESULT BgfxEngine::Present() noexcept +{ + return S_FALSE; +} + +[[nodiscard]] +HRESULT BgfxEngine::ScrollFrame() noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::PaintBackground() noexcept +{ + PVOID OldRunBase; + PVOID NewRunBase; + + PCD_IO_CHARACTER OldRun; + PCD_IO_CHARACTER NewRun; + + for (SHORT i = 0 ; i < _displayHeight ; i++) + { + OldRunBase = (PVOID)(_sharedViewBase + (i * 2 * _runLength)); + NewRunBase = (PVOID)(_sharedViewBase + (i * 2 * _runLength) + _runLength); + + OldRun = (PCD_IO_CHARACTER)OldRunBase; + NewRun = (PCD_IO_CHARACTER)NewRunBase; + + for (SHORT j = 0 ; j < _displayWidth ; j++) + { + NewRun[j].Character = L' '; + NewRun[j].Atribute = 0; + } + } + + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::PaintBufferLine(const std::basic_string_view clusters, + const COORD coord, + const bool /*trimLeft*/) noexcept +{ + try + { + PVOID NewRunBase = (PVOID)(_sharedViewBase + (coord.Y * 2 * _runLength) + _runLength); + PCD_IO_CHARACTER NewRun = (PCD_IO_CHARACTER)NewRunBase; + + for (size_t i = 0 ; i < clusters.size() && i < (size_t)_displayWidth ; i++) + { + NewRun[coord.X + i].Character = clusters.at(i).GetTextAsSingle(); + NewRun[coord.X + i].Atribute = _currentLegacyColorAttribute; + } + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT BgfxEngine::PaintBufferGridLines(GridLines const /*lines*/, + COLORREF const /*color*/, + size_t const /*cchLine*/, + COORD const /*coordTarget*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept +{ + // TODO: MSFT: 11448021 - Modify BGFX to support rendering full-width + // characters and a full-width cursor. + CD_IO_CURSOR_INFORMATION CursorInfo; + CursorInfo.Row = options.coordCursor.Y; + CursorInfo.Column = options.coordCursor.X; + CursorInfo.Height = options.ulCursorHeightPercent; + CursorInfo.IsVisible = TRUE; + + NTSTATUS Status = ServiceLocator::LocateInputServices()->RequestSetCursor(&CursorInfo); + + return HRESULT_FROM_NT(Status); + +} + +[[nodiscard]] +HRESULT BgfxEngine::UpdateDrawingBrushes(COLORREF const /*colorForeground*/, + COLORREF const /*colorBackground*/, + const WORD legacyColorAttribute, + const bool /*isBold*/, + bool const /*isSettingDefaultBrushes*/) noexcept +{ + _currentLegacyColorAttribute = legacyColorAttribute; + + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::UpdateFont(const FontInfoDesired& /*pfiFontInfoDesired*/, FontInfo& /*pfiFontInfo*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::UpdateDpi(int const /*iDpi*/) noexcept +{ + return S_OK; +} + +// Method Description: +// - This method will update our internal reference for how big the viewport is. +// Does nothing for BGFX. +// Arguments: +// - srNewViewport - The bounds of the new viewport. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT BgfxEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::GetProposedFont(const FontInfoDesired& /*pfiFontInfoDesired*/, FontInfo& /*pfiFontInfo*/, int const /*iDpi*/) noexcept +{ + return S_OK; +} + +SMALL_RECT BgfxEngine::GetDirtyRectInChars() +{ + SMALL_RECT r; + r.Bottom = _displayHeight > 0 ? (SHORT)(_displayHeight - 1) : 0; + r.Top = 0; + r.Left = 0; + r.Right = _displayWidth > 0 ? (SHORT)(_displayWidth - 1) : 0; + + return r; +} + +[[nodiscard]] +HRESULT BgfxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +{ + *pFontSize =_fontSize; + return S_OK; +} + +[[nodiscard]] +HRESULT BgfxEngine::IsGlyphWideByFont(const std::wstring_view /*glyph*/, _Out_ bool* const pResult) noexcept +{ + *pResult = false; + return S_OK; +} + +// Method Description: +// - Updates the window's title string. +// Does nothing for BGFX. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT BgfxEngine::_DoUpdateTitle(_In_ const std::wstring& /*newTitle*/) noexcept +{ + return S_OK; +} diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp new file mode 100644 index 000000000..80fc5d88e --- /dev/null +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -0,0 +1,114 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- BgfxEngine.hpp + +Abstract: +- OneCore implementation of the IRenderEngine interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +// Typically, renderers live under the renderer/xxx top-level folder. This +// renderer however has strong ties to the interactivity library. More +// specifically, it makes use of the Console IO Server communication class. +// It is also a one-file renderer and I had problems with header file +// definitions. Placing this renderer in the OneCore Interactivity library fixes +// the header issues, and is more sensible given its ties to ConIoSrv. +// (hegatta, 2017) + +#pragma once + +#include "..\..\renderer\inc\RenderEngineBase.hpp" + +namespace Microsoft::Console::Render +{ + class BgfxEngine final : public RenderEngineBase + { + public: + BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWidth, LONG FontWidth, LONG FontHeight); + ~BgfxEngine() override = default; + + // IRenderEngine Members + [[nodiscard]] + HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; + [[nodiscard]] + HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override; + [[nodiscard]] + HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; + [[nodiscard]] + HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] + HRESULT InvalidateAll() noexcept override; + [[nodiscard]] + HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] + HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; + + [[nodiscard]] + HRESULT StartPaint() noexcept override; + [[nodiscard]] + HRESULT EndPaint() noexcept override; + [[nodiscard]] + HRESULT Present() noexcept override; + + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT PaintBackground() noexcept override; + [[nodiscard]] + HRESULT PaintBufferLine(const std::basic_string_view clusters, + const COORD coord, + const bool trimLeft) noexcept override; + [[nodiscard]] + HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] + HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + + [[nodiscard]] + HRESULT PaintCursor(const CursorOptions& options) noexcept override; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(COLORREF const colorForeground, + COLORREF const colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + bool const isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] + HRESULT UpdateDpi(int const iDpi) noexcept override; + [[nodiscard]] + HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + + [[nodiscard]] + HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; + + SMALL_RECT GetDirtyRectInChars() override; + [[nodiscard]] + HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; + [[nodiscard]] + HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; + + protected: + [[nodiscard]] + HRESULT _DoUpdateTitle(_In_ const std::wstring& newTitle) noexcept override; + + private: + ULONG_PTR _sharedViewBase; + SIZE_T _runLength; + + LONG _displayHeight; + LONG _displayWidth; + + COORD _fontSize; + + WORD _currentLegacyColorAttribute; + }; +} diff --git a/src/interactivity/onecore/ConIoSrvComm.cpp b/src/interactivity/onecore/ConIoSrvComm.cpp new file mode 100644 index 000000000..8301511e0 --- /dev/null +++ b/src/interactivity/onecore/ConIoSrvComm.cpp @@ -0,0 +1,765 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConIoSrvComm.hpp" +#include "ConIoSrv.h" + +#include "..\..\host\dbcs.h" +#include "..\..\host\input.h" +#include "..\..\types\inc\IInputEvent.hpp" + +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// For details on the mechanisms employed in this class, read the comments in +// ConIoSrv.h, included above. For security-related considerations, see Trust.h +// in the ConIoSrv directory. + +extern void LockConsole(); +extern void UnlockConsole(); + +using namespace Microsoft::Console::Interactivity::OneCore; + +ConIoSrvComm::ConIoSrvComm() : + _inputPipeThreadHandle(INVALID_HANDLE_VALUE), + _pipeReadHandle(INVALID_HANDLE_VALUE), + _pipeWriteHandle(INVALID_HANDLE_VALUE), + _alpcClientCommunicationPort(INVALID_HANDLE_VALUE), + _alpcSharedViewSize(0), + _alpcSharedViewBase(NULL), + _displayMode(CIS_DISPLAY_MODE_NONE), + _fIsInputInitialized(false), + pWddmConEngine(nullptr) +{ + +} + +ConIoSrvComm::~ConIoSrvComm() +{ + // Cancel pending IOs on the input thread that might get us stuck. + if (INVALID_HANDLE_VALUE != _inputPipeThreadHandle) + { + LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(_inputPipeThreadHandle)); + CloseHandle(_inputPipeThreadHandle); + _inputPipeThreadHandle = INVALID_HANDLE_VALUE; + } + + // Free any handles we might have open. + if (INVALID_HANDLE_VALUE != _pipeReadHandle) + { + CloseHandle(_pipeReadHandle); + _pipeReadHandle = INVALID_HANDLE_VALUE; + } + + if (INVALID_HANDLE_VALUE != _pipeWriteHandle) + { + CloseHandle(_pipeWriteHandle); + _pipeWriteHandle = INVALID_HANDLE_VALUE; + } + + if (INVALID_HANDLE_VALUE != _alpcClientCommunicationPort) + { + CloseHandle(_alpcClientCommunicationPort); + _alpcClientCommunicationPort = INVALID_HANDLE_VALUE; + } +} + +#pragma region Communication + +[[nodiscard]] +NTSTATUS ConIoSrvComm::Connect() +{ + BOOL Ret = TRUE; + NTSTATUS Status = STATUS_SUCCESS; + + // Port handle and name. + HANDLE PortHandle; + UNICODE_STRING PortName; + + // Generic Object Manager attributes for the port object and ALPC-specific + // port attributes. + OBJECT_ATTRIBUTES ObjectAttributes; + ALPC_PORT_ATTRIBUTES PortAttributes; + + // Connection message. + CIS_MSG ConnectionMessage; + SIZE_T ConnectionMessageLength; + + // Connection message attributes. + SIZE_T ConnectionMessageAttributesBufferLength; + UCHAR ConnectionMessageAttributesBuffer[CIS_MSG_ATTR_BUFFER_SIZE]; + + // Type-specific pointers into the connection message attributes. + PALPC_HANDLE_ATTR HandleAttributes; + PALPC_DATA_VIEW_ATTR ViewAttributes; + + // Structure used to iterate over the handles given to us by the server. + ALPC_MESSAGE_HANDLE_INFORMATION HandleInfo; + + // Initialize the server port name. + Ret = RtlCreateUnicodeString(&PortName, CIS_ALPC_PORT_NAME); + if (Ret == FALSE) + { + return STATUS_NO_MEMORY; + } + + // Initialize the attributes of the port object. + InitializeObjectAttributes(&ObjectAttributes, + NULL, + 0, + NULL, + NULL); + + // Initialize the connection message attributes. + PALPC_MESSAGE_ATTRIBUTES ConnectionMessageAttributes + = (PALPC_MESSAGE_ATTRIBUTES)&ConnectionMessageAttributesBuffer; + + Status = AlpcInitializeMessageAttribute(CIS_MSG_ATTR_FLAGS, + ConnectionMessageAttributes, + CIS_MSG_ATTR_BUFFER_SIZE, + &ConnectionMessageAttributesBufferLength); + + // Set up the default security QoS descriptor. + const SECURITY_QUALITY_OF_SERVICE DefaultQoS = { + sizeof(SECURITY_QUALITY_OF_SERVICE), + SecurityAnonymous, + SECURITY_DYNAMIC_TRACKING, + FALSE + }; + + // Set up the port attributes. + PortAttributes.Flags = ALPC_PORFLG_ACCEPT_DUP_HANDLES | + ALPC_PORFLG_ACCEPT_INDIRECT_HANDLES; + PortAttributes.MaxMessageLength = sizeof(CIS_MSG); + PortAttributes.MaxPoolUsage = 0x4000; + PortAttributes.MaxSectionSize = 0; + PortAttributes.MaxTotalSectionSize = 0; + PortAttributes.MaxViewSize = 0; + PortAttributes.MemoryBandwidth = 0; + PortAttributes.SecurityQos = DefaultQoS; + PortAttributes.DupObjectTypes = OB_FILE_OBJECT_TYPE; + + // Initialize the connection message structure. + ConnectionMessage.AlpcHeader.MessageId = 0; + ConnectionMessage.AlpcHeader.u2.ZeroInit = 0; + + ConnectionMessage.AlpcHeader.u1.s1.TotalLength = sizeof(CIS_MSG); + ConnectionMessage.AlpcHeader.u1.s1.DataLength = sizeof(CIS_MSG) - sizeof(PORT_MESSAGE); + + ConnectionMessage.AlpcHeader.ClientId.UniqueProcess = 0; + ConnectionMessage.AlpcHeader.ClientId.UniqueThread = 0; + + // Request to connect to the server. + ConnectionMessageLength = sizeof(CIS_MSG); + Status = NtAlpcConnectPort(&PortHandle, + &PortName, + NULL, + &PortAttributes, + ALPC_MSGFLG_SYNC_REQUEST, + NULL, + (PPORT_MESSAGE)&ConnectionMessage, + &ConnectionMessageLength, + NULL, + ConnectionMessageAttributes, + 0); + if (NT_SUCCESS(Status)) + { + ViewAttributes = ALPC_GET_DATAVIEW_ATTRIBUTES(ConnectionMessageAttributes); + HandleAttributes = ALPC_GET_HANDLE_ATTRIBUTES(ConnectionMessageAttributes); + + // We must have exactly two handles, one for read, and one for write for + // the pipe. + if (HandleAttributes->HandleCount != 2) + { + Status = STATUS_UNSUCCESSFUL; + } + + if (NT_SUCCESS(Status)) + { + // Get each handle out. ALPC does not allow to pass indirect handles + // all at once; they must be retrieved one by one. + for (ULONG Index = 0; Index < HandleAttributes->HandleCount; Index++) + { + HandleInfo.Index = Index; + + Status = NtAlpcQueryInformationMessage(PortHandle, + (PPORT_MESSAGE)&ConnectionMessage, + AlpcMessageHandleInformation, + &HandleInfo, + sizeof(HandleInfo), + NULL); + if (NT_SUCCESS(Status)) + { + if (Index == 0) + { + _pipeReadHandle = ULongToHandle(HandleInfo.Handle); + } + else if (Index == 1) + { + _pipeWriteHandle = ULongToHandle(HandleInfo.Handle); + } + } + } + + // Keep the shared view information. + _alpcSharedViewSize = ViewAttributes->ViewSize; + _alpcSharedViewBase = ViewAttributes->ViewBase; + + // As well as a pointer to our communication port. + _alpcClientCommunicationPort = PortHandle; + + // Zero out the view. + memset(_alpcSharedViewBase, 0, _alpcSharedViewSize); + + // Get the display mode out of the connection message. + _displayMode = ConnectionMessage.GetDisplayModeParams.DisplayMode; + } + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::EnsureConnection() +{ + NTSTATUS Status; + + if (_alpcClientCommunicationPort == INVALID_HANDLE_VALUE) + { + Status = Connect(); + } + else + { + Status = STATUS_SUCCESS; + } + + return Status; +} + +VOID ConIoSrvComm::ServiceInputPipe() +{ + // Save off a handle to the thread that is coming in here in case it gets blocked and we need to tear down. + THROW_HR_IF(E_NOT_VALID_STATE, INVALID_HANDLE_VALUE != _inputPipeThreadHandle); // We can't store two of them, so it's invalid if there are two. + THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), + GetCurrentThread(), + GetCurrentProcess(), + &_inputPipeThreadHandle, + 0, + FALSE, + DUPLICATE_SAME_ACCESS)); + + BOOL Ret; + + CIS_EVENT Event = { 0 }; + + while (TRUE) + { + Ret = ReadFile(_pipeReadHandle, + &Event, + sizeof(CIS_EVENT), + NULL, + NULL); + + if (Ret != FALSE) + { + LockConsole(); + switch (Event.Type) + { + case CIS_EVENT_TYPE_INPUT: + try + { + KEY_EVENT_RECORD keyRecord = Event.InputEvent.Record.Event.KeyEvent; + KeyEvent keyEvent{ keyRecord }; + HandleGenericKeyEvent(keyEvent, false); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + break; + + case CIS_EVENT_TYPE_FOCUS: + HandleFocusEvent(&Event); + break; + } + UnlockConsole(); + } + else + { + // If we get disconnected, terminate. + ServiceLocator::RundownAndExit(GetLastError()); + } + } +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::SendRequestReceiveReply(PCIS_MSG Message) const +{ + NTSTATUS Status; + + Message->AlpcHeader.MessageId = 0; + Message->AlpcHeader.u2.ZeroInit = 0; + + Message->AlpcHeader.u1.s1.TotalLength = sizeof(CIS_MSG); + Message->AlpcHeader.u1.s1.DataLength = sizeof(CIS_MSG) - sizeof(PORT_MESSAGE); + + Message->AlpcHeader.ClientId.UniqueProcess = 0; + Message->AlpcHeader.ClientId.UniqueThread = 0; + + SIZE_T ActualReceiveMessageLength = sizeof(CIS_MSG); + + Status = NtAlpcSendWaitReceivePort(_alpcClientCommunicationPort, + 0, + (PPORT_MESSAGE)Message, + NULL, + (PPORT_MESSAGE)Message, + &ActualReceiveMessageLength, + NULL, + 0); + + return Status; +} + +VOID ConIoSrvComm::HandleFocusEvent(PCIS_EVENT Event) +{ + BOOL Ret; + IRenderer *Renderer; + CIS_EVENT ReplyEvent; + + Renderer = ServiceLocator::LocateGlobals().pRender; + + switch (_displayMode) + { + case CIS_DISPLAY_MODE_BGFX: + if (Event->FocusEvent.IsActive) + { + // Allow the renderer to paint (this has an effect only on + // the first call). + Renderer->EnablePainting(); + + // Force a complete redraw. + Renderer->TriggerRedrawAll(); + } + break; + + case CIS_DISPLAY_MODE_DIRECTX: + { + Globals& globals = ServiceLocator::LocateGlobals(); + + if (Event->FocusEvent.IsActive) + { + HRESULT hr = S_OK; + + // Lazy-initialize the WddmCon engine. + // + // This is necessary because the engine cannot be allowed to + // request ownership of the display before whatever instance + // of conhost was using it before has relinquished it. + if (!pWddmConEngine->IsInitialized()) + { + hr = pWddmConEngine->Initialize(); + LOG_IF_FAILED(hr); + + // Right after we initialize, synchronize the screen/viewport states with the WddmCon surface dimensions + if (SUCCEEDED(hr)) + { + const RECT rcOld = { 0 }; + + // WddmEngine reports display size in characters, adjust to pixels for resize window calc. + RECT rcDisplay = pWddmConEngine->GetDisplaySize(); + + // Get font to adjust char to pixels. + COORD coordFont = { 0 }; + LOG_IF_FAILED(pWddmConEngine->GetFontSize(&coordFont)); + + rcDisplay.right *= coordFont.X; + rcDisplay.bottom *= coordFont.Y; + + // Ask the screen buffer to resize itself (and all related components) based on the screen size. + globals.getConsoleInformation().GetActiveOutputBuffer().ProcessResizeWindow(&rcDisplay, &rcOld); + } + } + + if (SUCCEEDED(hr)) + { + // Allow acquiring device resources before drawing. + hr = pWddmConEngine->Enable(); + LOG_IF_FAILED(hr); + if (SUCCEEDED(hr)) + { + // Allow the renderer to paint. + Renderer->EnablePainting(); + + // Force a complete redraw. + Renderer->TriggerRedrawAll(); + } + } + } + else + { + if (pWddmConEngine->IsInitialized()) + { + // Wait for the currently running paint operation, if any, + // and prevent further attempts to render. + Renderer->WaitForPaintCompletionAndDisable(1000); + + // Relinquish control of the graphics device (only one + // DirectX application may control the device at any one + // time). + LOG_IF_FAILED(pWddmConEngine->Disable()); + + // Let the Console IO Server that we have relinquished + // control of the display. + ReplyEvent.Type = CIS_EVENT_TYPE_FOCUS_ACK; + Ret = WriteFile(_pipeWriteHandle, + &ReplyEvent, + sizeof(CIS_EVENT), + NULL, + NULL); + } + } + } + break; + + case CIS_DISPLAY_MODE_NONE: + default: + // Focus events have no meaning in a headless environment. + break; + } +} + +VOID ConIoSrvComm::CleanupForHeadless(const NTSTATUS status) +{ + if (!_fIsInputInitialized) + { + // Free any handles we might have open. + if (INVALID_HANDLE_VALUE != _pipeReadHandle) + { + CloseHandle(_pipeReadHandle); + _pipeReadHandle = INVALID_HANDLE_VALUE; + } + + if (INVALID_HANDLE_VALUE != _pipeWriteHandle) + { + CloseHandle(_pipeWriteHandle); + _pipeWriteHandle = INVALID_HANDLE_VALUE; + } + + if (INVALID_HANDLE_VALUE != _alpcClientCommunicationPort) + { + CloseHandle(_alpcClientCommunicationPort); + _alpcClientCommunicationPort = INVALID_HANDLE_VALUE; + } + + // Set the status for the IO thread to find. + ServiceLocator::LocateGlobals().ntstatusConsoleInputInitStatus = status; + + // Signal that input is ready to go. + ServiceLocator::LocateGlobals().hConsoleInputInitEvent.SetEvent(); + + _fIsInputInitialized = true; + } +} + +#pragma endregion + +#pragma region Request Methods + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestGetDisplaySize(_Inout_ PCD_IO_DISPLAY_SIZE pCdDisplaySize) const +{ + NTSTATUS Status; + + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_GETDISPLAYSIZE; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + *pCdDisplaySize = Message.GetDisplaySizeParams.DisplaySize; + Status = Message.GetDisplaySizeParams.ReturnValue; + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestGetFontSize(_Inout_ PCD_IO_FONT_SIZE pCdFontSize) const +{ + NTSTATUS Status; + + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_GETFONTSIZE; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + *pCdFontSize = Message.GetFontSizeParams.FontSize; + Status = Message.GetFontSizeParams.ReturnValue; + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestSetCursor(_In_ CD_IO_CURSOR_INFORMATION* const pCdCursorInformation) const +{ + NTSTATUS Status; + + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_SETCURSOR; + Message.SetCursorParams.CursorInformation = *pCdCursorInformation; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + Status = Message.SetCursorParams.ReturnValue; + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestUpdateDisplay(_In_ SHORT RowIndex) const +{ + NTSTATUS Status; + + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_UPDATEDISPLAY; + Message.UpdateDisplayParams.RowIndex = RowIndex; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + Status = Message.UpdateDisplayParams.ReturnValue; + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestMapVirtualKey(_In_ UINT uCode, _In_ UINT uMapType, _Out_ UINT* puReturnValue) +{ + NTSTATUS Status; + + Status = EnsureConnection(); + if (NT_SUCCESS(Status)) + { + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_MAPVIRTUALKEY; + Message.MapVirtualKeyParams.Code = uCode; + Message.MapVirtualKeyParams.MapType = uMapType; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + *puReturnValue = Message.MapVirtualKeyParams.ReturnValue; + } + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestVkKeyScan(_In_ WCHAR wCharacter, _Out_ SHORT* psReturnValue) +{ + NTSTATUS Status; + + Status = EnsureConnection(); + if (NT_SUCCESS(Status)) + { + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_VKKEYSCAN; + Message.VkKeyScanParams.Character = wCharacter; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + *psReturnValue = Message.VkKeyScanParams.ReturnValue; + } + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::RequestGetKeyState(_In_ int iVirtualKey, _Out_ SHORT *psReturnValue) +{ + NTSTATUS Status; + + Status = EnsureConnection(); + if (NT_SUCCESS(Status)) + { + CIS_MSG Message = { 0 }; + Message.Type = CIS_MSG_TYPE_GETKEYSTATE; + Message.GetKeyStateParams.VirtualKey = iVirtualKey; + + Status = SendRequestReceiveReply(&Message); + if (NT_SUCCESS(Status)) + { + *psReturnValue = Message.GetKeyStateParams.ReturnValue; + } + } + + return Status; +} + +[[nodiscard]] +USHORT ConIoSrvComm::GetDisplayMode() const +{ + return _displayMode; +} + +PVOID ConIoSrvComm::GetSharedViewBase() const +{ + return _alpcSharedViewBase; +} + +#pragma endregion + +#pragma region IInputServices Members + +UINT ConIoSrvComm::MapVirtualKeyW(UINT uCode, UINT uMapType) +{ + NTSTATUS Status = STATUS_SUCCESS; + + UINT ReturnValue; + Status = RequestMapVirtualKey(uCode, uMapType, &ReturnValue); + + if (!NT_SUCCESS(Status)) + { + ReturnValue = 0; + SetLastError(ERROR_PROC_NOT_FOUND); + } + + return ReturnValue; +} + +SHORT ConIoSrvComm::VkKeyScanW(WCHAR ch) +{ + NTSTATUS Status = STATUS_SUCCESS; + + SHORT ReturnValue; + Status = RequestVkKeyScan(ch, &ReturnValue); + + if (!NT_SUCCESS(Status)) + { + ReturnValue = 0; + SetLastError(ERROR_PROC_NOT_FOUND); + } + + return ReturnValue; +} + +SHORT ConIoSrvComm::GetKeyState(int nVirtKey) +{ + NTSTATUS Status = STATUS_SUCCESS; + + SHORT ReturnValue; + Status = RequestGetKeyState(nVirtKey, &ReturnValue); + + if (!NT_SUCCESS(Status)) + { + ReturnValue = 0; + SetLastError(ERROR_PROC_NOT_FOUND); + } + + return ReturnValue; +} + +BOOL ConIoSrvComm::TranslateCharsetInfo(DWORD * lpSrc, LPCHARSETINFO lpCs, DWORD dwFlags) +{ + SetLastError(ERROR_SUCCESS); + + if (TCI_SRCCODEPAGE == dwFlags) + { + *lpCs = { 0 }; + + DWORD dwSrc = (DWORD)lpSrc; + switch (dwSrc) + { + case CP_JAPANESE: + lpCs->ciCharset = SHIFTJIS_CHARSET; + return TRUE; + case CP_CHINESE_SIMPLIFIED: + lpCs->ciCharset = GB2312_CHARSET; + return TRUE; + case CP_KOREAN: + lpCs->ciCharset = HANGEUL_CHARSET; + return TRUE; + case CP_CHINESE_TRADITIONAL: + lpCs->ciCharset = CHINESEBIG5_CHARSET; + return TRUE; + } + } + + SetLastError(ERROR_NOT_SUPPORTED); + return FALSE; +} + +#pragma endregion + + +[[nodiscard]] +NTSTATUS ConIoSrvComm::InitializeBgfx() +{ + NTSTATUS Status; + + Globals& globals = ServiceLocator::LocateGlobals(); + FAIL_FAST_IF_NULL(globals.pRender); + IWindowMetrics * const Metrics = ServiceLocator::LocateWindowMetrics(); + + // Fetch the display size from the console driver. + const RECT DisplaySize = Metrics->GetMaxClientRectInPixels(); + Status = GetLastError(); + + if (NT_SUCCESS(Status)) + { + // Same with the font size. + CD_IO_FONT_SIZE FontSize = { 0 }; + Status = RequestGetFontSize(&FontSize); + + if (NT_SUCCESS(Status)) + { + try + { + // Create and set the render engine. + BgfxEngine* const pBgfxEngine = new BgfxEngine(GetSharedViewBase(), + DisplaySize.bottom / FontSize.Height, + DisplaySize.right / FontSize.Width, + FontSize.Width, + FontSize.Height); + + globals.pRender->AddRenderEngine(pBgfxEngine); + } + catch (...) + { + Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + } + } + + return Status; +} + +[[nodiscard]] +NTSTATUS ConIoSrvComm::InitializeWddmCon() +{ + Globals& globals = ServiceLocator::LocateGlobals(); + FAIL_FAST_IF_NULL(globals.pRender); + + try + { + pWddmConEngine = new WddmConEngine(); + globals.pRender->AddRenderEngine(pWddmConEngine); + } + catch (...) + { + return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + return STATUS_SUCCESS; +} diff --git a/src/interactivity/onecore/ConIoSrvComm.hpp b/src/interactivity/onecore/ConIoSrvComm.hpp new file mode 100644 index 000000000..0ca3f3c29 --- /dev/null +++ b/src/interactivity/onecore/ConIoSrvComm.hpp @@ -0,0 +1,96 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConIoSrvComm.hpp + +Abstract: +- OneCore implementation of the IConIoSrvComm interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include + +#include "ConIoSrv.h" +#include "..\..\inc\IInputServices.hpp" + +#include "BgfxEngine.hpp" +#include "..\..\renderer\wddmcon\wddmconrenderer.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class ConIoSrvComm final : public IInputServices + { + public: + ConIoSrvComm(); + ~ConIoSrvComm() override; + + [[nodiscard]] + NTSTATUS Connect(); + VOID ServiceInputPipe(); + + [[nodiscard]] + NTSTATUS RequestGetDisplaySize(_Inout_ PCD_IO_DISPLAY_SIZE pCdDisplaySize) const; + [[nodiscard]] + NTSTATUS RequestGetFontSize(_Inout_ PCD_IO_FONT_SIZE pCdFontSize) const; + [[nodiscard]] + NTSTATUS RequestSetCursor(_In_ CD_IO_CURSOR_INFORMATION* const pCdCursorInformation) const; + [[nodiscard]] + NTSTATUS RequestUpdateDisplay(_In_ SHORT RowIndex) const; + + [[nodiscard]] + NTSTATUS RequestMapVirtualKey(_In_ UINT uCode, _In_ UINT uMapType, _Out_ UINT* puReturnValue); + [[nodiscard]] + NTSTATUS RequestVkKeyScan(_In_ WCHAR wCharacter, _Out_ SHORT* psReturnValue); + [[nodiscard]] + NTSTATUS RequestGetKeyState(_In_ int iVirtualKey, _Out_ SHORT *psReturnValue); + + [[nodiscard]] + USHORT GetDisplayMode() const; + + PVOID GetSharedViewBase() const; + + VOID CleanupForHeadless(const NTSTATUS status); + + // IInputServices Members + UINT MapVirtualKeyW(UINT uCode, UINT uMapType); + SHORT VkKeyScanW(WCHAR ch); + SHORT GetKeyState(int nVirtKey); + BOOL TranslateCharsetInfo(DWORD * lpSrc, LPCHARSETINFO lpCs, DWORD dwFlags); + + [[nodiscard]] + NTSTATUS InitializeBgfx(); + [[nodiscard]] + NTSTATUS InitializeWddmCon(); + + Microsoft::Console::Render::WddmConEngine* pWddmConEngine; + private: + [[nodiscard]] + NTSTATUS EnsureConnection(); + [[nodiscard]] + NTSTATUS SendRequestReceiveReply(PCIS_MSG Message) const; + + VOID HandleFocusEvent(PCIS_EVENT const FocusEvent); + + HANDLE _inputPipeThreadHandle; + + HANDLE _pipeReadHandle; + HANDLE _pipeWriteHandle; + + HANDLE _alpcClientCommunicationPort; + SIZE_T _alpcSharedViewSize; + PVOID _alpcSharedViewBase; + + USHORT _displayMode; + + bool _fIsInputInitialized; + + }; +} diff --git a/src/interactivity/onecore/ConsoleControl.cpp b/src/interactivity/onecore/ConsoleControl.cpp new file mode 100644 index 000000000..417574269 --- /dev/null +++ b/src/interactivity/onecore/ConsoleControl.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleControl.hpp" + +#include +#include + +using namespace Microsoft::Console::Interactivity::OneCore; + +#pragma region IConsoleControl Members + +[[nodiscard]] +NTSTATUS ConsoleControl::NotifyConsoleApplication(_In_ DWORD /*dwProcessId*/) +{ + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS ConsoleControl::SetForeground(_In_ HANDLE /*hProcess*/, _In_ BOOL /*fForeground*/) +{ + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS ConsoleControl::EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags) +{ + USER_API_MSG m; + PENDTASKMSG a = &m.u.EndTask; + + RtlZeroMemory(a, sizeof(*a)); + a->ProcessId = hProcessId; + a->ConsoleEventCode = dwEventType; + a->ConsoleFlags = ulCtrlFlags; + + return CsrClientCallServer((PCSR_API_MSG)&m, + NULL, + CSR_MAKE_API_NUMBER(USERSRV_SERVERDLL_INDEX, UserpEndTask), + sizeof(*a)); +} + +#pragma endregion diff --git a/src/interactivity/onecore/ConsoleControl.hpp b/src/interactivity/onecore/ConsoleControl.hpp new file mode 100644 index 000000000..48d27c389 --- /dev/null +++ b/src/interactivity/onecore/ConsoleControl.hpp @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleControl.hpp + +Abstract: +- OneCore implementation of the IConsoleControl interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\IConsoleControl.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class ConsoleControl sealed : public IConsoleControl + { + public: + // IConsoleControl Members + [[nodiscard]] + NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId); + [[nodiscard]] + NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground); + [[nodiscard]] + NTSTATUS EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags); + }; +} diff --git a/src/interactivity/onecore/ConsoleInputThread.cpp b/src/interactivity/onecore/ConsoleInputThread.cpp new file mode 100644 index 000000000..d62a7c53d --- /dev/null +++ b/src/interactivity/onecore/ConsoleInputThread.cpp @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleInputThread.hpp" +#include "ConIoSrvComm.hpp" +#include "ConsoleWindow.hpp" + +#include "ConIoSrv.h" + +#include "..\..\host\input.h" + +#include "..\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::OneCore; + + +DWORD ConsoleInputThreadProcOneCore(LPVOID /*lpParam*/) +{ + Globals& globals = ServiceLocator::LocateGlobals(); + ConIoSrvComm * const Server = ServiceLocator::LocateInputServices(); + + NTSTATUS Status = Server->Connect(); + + if (NT_SUCCESS(Status)) + { + USHORT DisplayMode = Server->GetDisplayMode(); + + if (DisplayMode != CIS_DISPLAY_MODE_NONE) + { + // Create and set the console window. + ConsoleWindow * const wnd = new(std::nothrow) ConsoleWindow(); + Status = NT_TESTNULL(wnd); + + if (NT_SUCCESS(Status)) + { + LOG_IF_FAILED(ServiceLocator::SetConsoleWindowInstance(wnd)); + + // The console's renderer should be created before we get here. + FAIL_FAST_IF_NULL(globals.pRender); + + switch (DisplayMode) + { + case CIS_DISPLAY_MODE_BGFX: + Status = Server->InitializeBgfx(); + break; + + case CIS_DISPLAY_MODE_DIRECTX: + Status = Server->InitializeWddmCon(); + break; + } + + if (NT_SUCCESS(Status)) + { + globals.ntstatusConsoleInputInitStatus = Status; + globals.hConsoleInputInitEvent.SetEvent(); + + try + { + // Start listening for input (returns on failure only). + // This will never return. + Server->ServiceInputPipe(); + } + catch(...) + { + // If we couldn't set up the input thread, log and cleanup + // and go to headless mode instead. + LOG_CAUGHT_EXCEPTION(); + Status = wil::ResultFromCaughtException(); + Server->CleanupForHeadless(Status); + } + } + } + } + else + { + // Nothing to do input-wise, but we must let the rest of the console + // continue. + Server->CleanupForHeadless(Status); + } + } + else + { + // If we get an access denied and couldn't connect to the coniosrv in CSRSS.exe. + // that's OK. We're likely inside an AppContainer in a TAEF /runas:uap test. + // We don't want AppContainered things to have access to the hardware devices directly + // like coniosrv in CSRSS offers, so we "succeeded" and will let the IO thread know it + // can continue. + if (STATUS_ACCESS_DENIED == Status) + { + Status = STATUS_SUCCESS; + } + + // Notify IO thread of our status. + Server->CleanupForHeadless(Status); + } + + return Status; +} + +// Routine Description: +// - Starts the OneCore-specific console input thread. +HANDLE ConsoleInputThread::Start() +{ + HANDLE hThread = nullptr; + DWORD dwThreadId = (DWORD)-1; + + hThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)ConsoleInputThreadProcOneCore, + _pConIoSrvComm, + 0, + &dwThreadId); + if (hThread) + { + _hThread = hThread; + _dwThreadId = dwThreadId; + } + + return hThread; +} + +ConIoSrvComm *ConsoleInputThread::GetConIoSrvComm() +{ + return _pConIoSrvComm; +} diff --git a/src/interactivity/onecore/ConsoleInputThread.hpp b/src/interactivity/onecore/ConsoleInputThread.hpp new file mode 100644 index 000000000..112d23be8 --- /dev/null +++ b/src/interactivity/onecore/ConsoleInputThread.hpp @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleInputThread.hpp + +Abstract: +- OneCore implementation of the IConsoleInputThread interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\IConsoleInputThread.hpp" +#include "ConIoSrvComm.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class ConsoleInputThread sealed : public IConsoleInputThread + { + public: + HANDLE Start(); + + ConIoSrvComm *GetConIoSrvComm(); + + private: + ConIoSrvComm *_pConIoSrvComm; + }; +} diff --git a/src/interactivity/onecore/ConsoleWindow.cpp b/src/interactivity/onecore/ConsoleWindow.cpp new file mode 100644 index 000000000..569c66c3c --- /dev/null +++ b/src/interactivity/onecore/ConsoleWindow.cpp @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleWindow.hpp" + +#include "..\inc\ServiceLocator.hpp" +#include "..\..\types\inc\Viewport.hpp" + +using namespace Microsoft::Console::Interactivity::OneCore; +using namespace Microsoft::Console::Types; + +BOOL ConsoleWindow::EnableBothScrollBars() +{ + return FALSE; +} + +int ConsoleWindow::UpdateScrollBar(bool /*isVertical*/, + bool /*isAltBuffer*/, + UINT /*pageSize*/, + int /*maxSize*/, + int /*viewportPosition*/) +{ + return 0; +} + +bool ConsoleWindow::IsInFullscreen() const +{ + return true; +} + +void ConsoleWindow::SetIsFullscreen(bool const /*fFullscreenEnabled*/) +{ +} + +void ConsoleWindow::ChangeViewport(const SMALL_RECT NewWindow) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + COORD const FontSize = ScreenInfo.GetScreenFontSize(); + + Selection* pSelection = &Selection::Instance(); + pSelection->HideSelection(); + + ScreenInfo.SetViewport(Viewport::FromInclusive(NewWindow), true); + + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender->TriggerScroll(); + } + + pSelection->ShowSelection(); + + ScreenInfo.UpdateScrollBars(); +} + +void ConsoleWindow::CaptureMouse() +{ +} + +BOOL ConsoleWindow::ReleaseMouse() +{ + return TRUE; +} + +HWND ConsoleWindow::GetWindowHandle() const +{ + return nullptr; +} + +void ConsoleWindow::SetOwner() +{ +} + +BOOL ConsoleWindow::GetCursorPosition(LPPOINT /*lpPoint*/) +{ + return FALSE; +} + +BOOL ConsoleWindow::GetClientRectangle(LPRECT /*lpRect*/) +{ + return FALSE; +} + +int ConsoleWindow::MapPoints(LPPOINT /*lpPoints*/, UINT /*cPoints*/) +{ + return 0; +} + +BOOL ConsoleWindow::ConvertScreenToClient(LPPOINT /*lpPoint*/) +{ + return 0; +} + +BOOL ConsoleWindow::SendNotifyBeep() const +{ + return Beep(800, 200); +} + +BOOL ConsoleWindow::PostUpdateScrollBars() const +{ + return FALSE; +} + +BOOL ConsoleWindow::PostUpdateWindowSize() const +{ + return FALSE; +} + +void ConsoleWindow::UpdateWindowSize(COORD const /*coordSizeInChars*/) +{ +} + +void ConsoleWindow::UpdateWindowText() +{ +} + +void ConsoleWindow::HorizontalScroll(const WORD /*wScrollCommand*/, const WORD /*wAbsoluteChange*/) +{ +} + +void ConsoleWindow::VerticalScroll(const WORD /*wScrollCommand*/, const WORD /*wAbsoluteChange*/) +{ +} + +[[nodiscard]] +HRESULT ConsoleWindow::SignalUia(_In_ EVENTID /*id*/) +{ + return E_NOTIMPL; +} + +[[nodiscard]] +HRESULT ConsoleWindow::UiaSetTextAreaFocus() +{ + return E_NOTIMPL; +} + +RECT ConsoleWindow::GetWindowRect() const +{ + RECT rc = {0}; + return rc; +} diff --git a/src/interactivity/onecore/ConsoleWindow.hpp b/src/interactivity/onecore/ConsoleWindow.hpp new file mode 100644 index 000000000..35d840d7f --- /dev/null +++ b/src/interactivity/onecore/ConsoleWindow.hpp @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleWindow.hpp + +Abstract: +- OneCore implementation of the IConsoleWindow interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\IConsoleWindow.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class ConsoleWindow sealed : public IConsoleWindow + { + public: + + // Inherited via IConsoleWindow + BOOL EnableBothScrollBars(); + int UpdateScrollBar(bool isVertical, bool isAltBuffer, UINT pageSize, int maxSize, int viewportPosition); + + bool IsInFullscreen() const; + void SetIsFullscreen(bool const fFullscreenEnabled); + void ChangeViewport(const SMALL_RECT NewWindow); + + void CaptureMouse(); + BOOL ReleaseMouse(); + + HWND GetWindowHandle() const; + + void SetOwner(); + + BOOL GetCursorPosition(LPPOINT lpPoint); + BOOL GetClientRectangle(LPRECT lpRect); + int MapPoints(LPPOINT lpPoints, UINT cPoints); + BOOL ConvertScreenToClient(LPPOINT lpPoint); + + BOOL SendNotifyBeep() const; + + BOOL PostUpdateScrollBars() const; + BOOL PostUpdateTitleWithCopy(const PCWSTR pwszNewTitle) const; + BOOL PostUpdateWindowSize() const; + + void UpdateWindowSize(COORD const coordSizeInChars); + void UpdateWindowText(); + + void HorizontalScroll(const WORD wScrollCommand, const WORD wAbsoluteChange); + void VerticalScroll(const WORD wScrollCommand, const WORD wAbsoluteChange); + + [[nodiscard]] + HRESULT SignalUia(_In_ EVENTID id); + [[nodiscard]] + HRESULT UiaSetTextAreaFocus(); + RECT GetWindowRect() const; + }; +} diff --git a/src/interactivity/onecore/SystemConfigurationProvider.cpp b/src/interactivity/onecore/SystemConfigurationProvider.cpp new file mode 100644 index 000000000..ff9e985f1 --- /dev/null +++ b/src/interactivity/onecore/SystemConfigurationProvider.cpp @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "SystemConfigurationProvider.hpp" + +using namespace Microsoft::Console::Interactivity::OneCore; + +UINT SystemConfigurationProvider::GetCaretBlinkTime() +{ + return s_DefaultCaretBlinkTime; +} + +bool SystemConfigurationProvider::IsCaretBlinkingEnabled() +{ + return s_DefaultIsCaretBlinkingEnabled; +} + +int SystemConfigurationProvider::GetNumberOfMouseButtons() +{ + if (IsGetSystemMetricsPresent()) + { + return GetSystemMetrics(SM_CMOUSEBUTTONS); + } + else + { + return s_DefaultNumberOfMouseButtons; + } +} + +ULONG SystemConfigurationProvider::GetCursorWidth() +{ + return s_DefaultCursorWidth; +} + +ULONG SystemConfigurationProvider::GetNumberOfWheelScrollLines() +{ + return s_DefaultNumberOfWheelScrollLines; +} + +ULONG SystemConfigurationProvider::GetNumberOfWheelScrollCharacters() +{ + return s_DefaultNumberOfWheelScrollCharacters; +} + +void SystemConfigurationProvider::GetSettingsFromLink( + _Inout_ Settings* pLinkSettings, + _Inout_updates_bytes_(*pdwTitleLength) LPWSTR /*pwszTitle*/, + _Inout_ PDWORD /*pdwTitleLength*/, + _In_ PCWSTR /*pwszCurrDir*/, + _In_ PCWSTR /*pwszAppName*/) +{ + // While both OneCore console renderers use TrueType fonts, there is no + // advanced font support on that platform. Namely, there is no way to pick + // neither the font nor the font size. Since this choice of TrueType font + // is made implicitly by the renderers, the rest of the console is not aware + // of it and the renderer procedure goes on to translate output text so that + // it be renderable with raster fonts, which messes up the final output. + // Hence, we make it seem like the console is in fact configred to use a + // TrueType font by the user. + + pLinkSettings->SetFaceName(DEFAULT_TT_FONT_FACENAME, ARRAYSIZE(DEFAULT_TT_FONT_FACENAME)); + pLinkSettings->SetFontFamily(TMPF_TRUETYPE); + + return; +} diff --git a/src/interactivity/onecore/SystemConfigurationProvider.hpp b/src/interactivity/onecore/SystemConfigurationProvider.hpp new file mode 100644 index 000000000..654f1caf7 --- /dev/null +++ b/src/interactivity/onecore/SystemConfigurationProvider.hpp @@ -0,0 +1,55 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SystemConfigurationProvider.hpp + +Abstract: +- OneCore implementation of the ISystemConfigurationProvider interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\ISystemConfigurationProvider.hpp" + +#pragma hdrstop + +#ifndef DEFAULT_TT_FONT_FACENAME +#define DEFAULT_TT_FONT_FACENAME L"__DefaultTTFont__" +#endif + +class InputTests; + +namespace Microsoft::Console::Interactivity::OneCore +{ + class SystemConfigurationProvider sealed : public ISystemConfigurationProvider + { + public: + bool IsCaretBlinkingEnabled(); + + UINT GetCaretBlinkTime(); + int GetNumberOfMouseButtons(); + ULONG GetCursorWidth() override; + ULONG GetNumberOfWheelScrollLines(); + ULONG GetNumberOfWheelScrollCharacters(); + + void GetSettingsFromLink(_Inout_ Settings* pLinkSettings, + _Inout_updates_bytes_(*pdwTitleLength) LPWSTR pwszTitle, + _Inout_ PDWORD pdwTitleLength, + _In_ PCWSTR pwszCurrDir, + _In_ PCWSTR pwszAppName); + private: + static const UINT s_DefaultCaretBlinkTime = 530; // milliseconds + static const bool s_DefaultIsCaretBlinkingEnabled = true; + static const int s_DefaultNumberOfMouseButtons = 3; + static const ULONG s_DefaultCursorWidth = 1; + static const ULONG s_DefaultNumberOfWheelScrollLines = 3; + static const ULONG s_DefaultNumberOfWheelScrollCharacters = 3; + + friend class ::InputTests; + }; +} diff --git a/src/interactivity/onecore/WindowMetrics.cpp b/src/interactivity/onecore/WindowMetrics.cpp new file mode 100644 index 000000000..0fd04ebc3 --- /dev/null +++ b/src/interactivity/onecore/WindowMetrics.cpp @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "WindowMetrics.hpp" +#include "ConIoSrvComm.hpp" + +#include "..\..\renderer\wddmcon\wddmconrenderer.hpp" + +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// Default metrics for when in headless mode. +#define HEADLESS_FONT_SIZE_WIDTH (8) +#define HEADLESS_FONT_SIZE_HEIGHT (12) +#define HEADLESS_DISPLAY_SIZE_WIDTH (80) +#define HEADLESS_DISPLAY_SIZE_HEIGHT (25) + +using namespace Microsoft::Console::Interactivity::OneCore; + +RECT WindowMetrics::GetMinClientRectInPixels() +{ + ConIoSrvComm *Server; + + NTSTATUS Status; + + // We need to always return something viable for this call, + // so by default, set the font and display size to our headless + // constants. + // If we get information from the Server, great. We'll calculate + // the values for that at the end. + // If we don't... then at least we have a non-zero rectangle. + COORD FontSize = { 0 }; + FontSize.X = HEADLESS_FONT_SIZE_WIDTH; + FontSize.Y = HEADLESS_FONT_SIZE_HEIGHT; + + RECT DisplaySize = { 0 }; + DisplaySize.right = HEADLESS_DISPLAY_SIZE_WIDTH; + DisplaySize.bottom = HEADLESS_DISPLAY_SIZE_HEIGHT; + + CD_IO_FONT_SIZE FontSizeIoctl = { 0 }; + CD_IO_DISPLAY_SIZE DisplaySizeIoctl = { 0 }; + + USHORT DisplayMode; + + // Fetch a reference to the Console IO Server. + Server = ServiceLocator::LocateInputServices(); + + // Figure out what kind of display we are using. + DisplayMode = Server->GetDisplayMode(); + + // Note on status propagation: + // + // The IWindowMetrics contract was extracted from the original methods in + // the Win32 Window class, which have no failure modes. However, in the case + // of their OneCore implementations, because getting this information + // requires reaching out to the Console IO Server if display output occurs + // via BGFX, there is a possibility of failure where the server may be + // unreachable. As a result, Get[Max|Min]ClientRectInPixels call + // SetLastError in their OneCore implementations to reflect whether their + // return value is accurate. + + switch (DisplayMode) + { + case CIS_DISPLAY_MODE_BGFX: + { + // TODO: MSFT: 10916072 This requires switching to kernel mode and calling + // BgkGetConsoleState. The call's result can be cached, though that + // might be a problem for plugging/unplugging monitors or perhaps + // across KVM sessions. + + Status = Server->RequestGetDisplaySize(&DisplaySizeIoctl); + + if (NT_SUCCESS(Status)) + { + Status = Server->RequestGetFontSize(&FontSizeIoctl); + + if (NT_SUCCESS(Status)) + { + DisplaySize.top = 0; + DisplaySize.left = 0; + DisplaySize.bottom = DisplaySizeIoctl.Height; + DisplaySize.right = DisplaySizeIoctl.Width; + + FontSize.X = (SHORT)FontSizeIoctl.Width; + FontSize.Y = (SHORT)FontSizeIoctl.Height; + } + } + else + { + SetLastError(Status); + } + } + break; + + case CIS_DISPLAY_MODE_DIRECTX: + { + LOG_IF_FAILED(Server->pWddmConEngine->GetFontSize(&FontSize)); + DisplaySize = Server->pWddmConEngine->GetDisplaySize(); + } + break; + + case CIS_DISPLAY_MODE_NONE: + { + // When in headless mode and using EMS (Emergency Management + // Services), ensure that the buffer isn't zero-sized by + // using the default values. + } + break; + } + + // The result is expected to be in pixels, not rows/columns. + DisplaySize.right *= FontSize.X; + DisplaySize.bottom *= FontSize.Y; + + return DisplaySize; +} + +RECT WindowMetrics::GetMaxClientRectInPixels() +{ + // OneCore consoles only have one size and cannot be resized. + return GetMinClientRectInPixels(); +} diff --git a/src/interactivity/onecore/WindowMetrics.hpp b/src/interactivity/onecore/WindowMetrics.hpp new file mode 100644 index 000000000..2721cc84e --- /dev/null +++ b/src/interactivity/onecore/WindowMetrics.hpp @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WindowMetrics.hpp + +Abstract: +- OneCore implementation of the IWindowMetrics interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "..\inc\IWindowMetrics.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::OneCore +{ + class WindowMetrics sealed : public IWindowMetrics + { + public: + RECT GetMinClientRectInPixels(); + RECT GetMaxClientRectInPixels(); + }; +} diff --git a/src/interactivity/onecore/coninteractivityonecore.rcv b/src/interactivity/onecore/coninteractivityonecore.rcv new file mode 100644 index 000000000..42be8e3aa --- /dev/null +++ b/src/interactivity/onecore/coninteractivityonecore.rcv @@ -0,0 +1,5 @@ +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Interactivity OneCore" +#define VER_INTERNALNAME_STR "ConInteractivityOneCore" +#define VER_ORIGINALFILENAME_STR "CONINTERACTIVITYONECORE.DLL" diff --git a/src/interactivity/onecore/dirs b/src/interactivity/onecore/dirs new file mode 100644 index 000000000..33b9c20cb --- /dev/null +++ b/src/interactivity/onecore/dirs @@ -0,0 +1,2 @@ +DIRS= \ + lib \ diff --git a/src/interactivity/onecore/lib/onecore.LIB.vcxproj b/src/interactivity/onecore/lib/onecore.LIB.vcxproj new file mode 100644 index 000000000..8b104fd08 --- /dev/null +++ b/src/interactivity/onecore/lib/onecore.LIB.vcxproj @@ -0,0 +1,43 @@ + + + + + + IS_INTERACTIVITYBASE_CONSUMER;_WINDLL;%(PreprocessorDefinitions) + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + {06EC74CB-9A12-428C-B551-8537EC964726} + Win32Proj + OneCore + InteractivityOneCore + ConInteractivityOneCoreLib + + + + + \ No newline at end of file diff --git a/src/interactivity/onecore/lib/onecore.LIB.vcxproj.filters b/src/interactivity/onecore/lib/onecore.LIB.vcxproj.filters new file mode 100644 index 000000000..73a3682e5 --- /dev/null +++ b/src/interactivity/onecore/lib/onecore.LIB.vcxproj.filters @@ -0,0 +1,75 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/interactivity/onecore/lib/sources b/src/interactivity/onecore/lib/sources new file mode 100644 index 000000000..fe7998da2 --- /dev/null +++ b/src/interactivity/onecore/lib/sources @@ -0,0 +1,13 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConInteractivityOneCoreLib +TARGETTYPE = LIBRARY + +# VSTS 14847240: Locally suppress individual -Wv:17 compiler warnings. +# For more information, visit https://osgwiki.com/wiki/Windows_C%2B%2B_Toolset_Status. +USER_C_FLAGS=$(USER_C_FLAGS) /wd4302 # 'conversion': truncation from 'type1' to 'type2' +USER_C_FLAGS=$(USER_C_FLAGS) /wd4311 # 'variable': pointer truncation from 'type 1' to 'type 2' diff --git a/src/interactivity/onecore/precomp.cpp b/src/interactivity/onecore/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/interactivity/onecore/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/interactivity/onecore/precomp.h b/src/interactivity/onecore/precomp.h new file mode 100644 index 000000000..dcf54628f --- /dev/null +++ b/src/interactivity/onecore/precomp.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include +#include +#include "wchar.h" + +// Extension presence detection +#include + +#define _DDK_INCLUDED +#include "..\..\host\precomp.h" diff --git a/src/interactivity/onecore/res.rc b/src/interactivity/onecore/res.rc new file mode 100644 index 000000000..3c08238ff --- /dev/null +++ b/src/interactivity/onecore/res.rc @@ -0,0 +1,18 @@ +/****************************** Module Header ******************************\ +* Module Name: res.rc +* +* Copyright (c) 1985-91, Microsoft Corporation +* +* Constants +* +* History: +* 08-21-91 Created. +\***************************************************************************/ + +#include + +#ifndef EXTERNAL_BUILD +#include "coninteractivityonecore.rcv" +#include +#include +#endif diff --git a/src/interactivity/onecore/sources.inc b/src/interactivity/onecore/sources.inc new file mode 100644 index 000000000..aaa917f5c --- /dev/null +++ b/src/interactivity/onecore/sources.inc @@ -0,0 +1,44 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Interactivity for OneCore +# ------------------------------------- + +# This module defines interaction with the user on +# system configurations with minimal input/output support + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\AccessibilityNotifier.cpp \ + ..\BgfxEngine.cpp \ + ..\ConIoSrvComm.cpp \ + ..\ConsoleControl.cpp \ + ..\ConsoleInputThread.cpp \ + ..\ConsoleWindow.cpp \ + ..\SystemConfigurationProvider.cpp \ + ..\WindowMetrics.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\..\..\..\..\ConIoSrv; \ diff --git a/src/interactivity/win32/AccessibilityNotifier.cpp b/src/interactivity/win32/AccessibilityNotifier.cpp new file mode 100644 index 000000000..c0ae78eda --- /dev/null +++ b/src/interactivity/win32/AccessibilityNotifier.cpp @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "AccessibilityNotifier.hpp" + +#include "..\inc\ServiceLocator.hpp" +#include "ConsoleControl.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +void AccessibilityNotifier::NotifyConsoleCaretEvent(_In_ RECT rectangle) +{ + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + CONSOLE_CARET_INFO caretInfo; + caretInfo.hwnd = pWindow->GetWindowHandle(); + caretInfo.rc = rectangle; + + LOG_IF_FAILED(ServiceLocator::LocateConsoleControl()->Control(ConsoleControl::ControlType::ConsoleSetCaretInfo, + &caretInfo, + sizeof(caretInfo))); + } + +} + +void AccessibilityNotifier::NotifyConsoleCaretEvent(_In_ ConsoleCaretEventFlags flags, _In_ LONG position) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + DWORD dwFlags = 0; + + if (flags == ConsoleCaretEventFlags::CaretSelection) + { + dwFlags = CONSOLE_CARET_SELECTION; + } + else if (flags == ConsoleCaretEventFlags::CaretVisible) + { + dwFlags = CONSOLE_CARET_VISIBLE; + } + + // UIA event notification + static COORD previousCursorLocation = { 0, 0 }; + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + + if (pWindow != nullptr) + { + NotifyWinEvent(EVENT_CONSOLE_CARET, + pWindow->GetWindowHandle(), + dwFlags, + position); + + const auto& screenInfo = gci.GetActiveOutputBuffer(); + const Cursor& cursor = screenInfo.GetTextBuffer().GetCursor(); + const COORD currentCursorPosition = cursor.GetPosition(); + if (currentCursorPosition != previousCursorLocation) + { + LOG_IF_FAILED(pWindow->SignalUia(UIA_Text_TextSelectionChangedEventId)); + } + previousCursorLocation = currentCursorPosition; + } + +} + +void AccessibilityNotifier::NotifyConsoleUpdateScrollEvent(_In_ LONG x, _In_ LONG y) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_UPDATE_SCROLL, + pWindow->GetWindowHandle(), + x, + y); + } +} + +void AccessibilityNotifier::NotifyConsoleUpdateSimpleEvent(_In_ LONG start, _In_ LONG charAndAttribute) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_UPDATE_SIMPLE, + pWindow->GetWindowHandle(), + start, + charAndAttribute); + } +} + +void AccessibilityNotifier::NotifyConsoleUpdateRegionEvent(_In_ LONG startXY, _In_ LONG endXY) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_UPDATE_REGION, + pWindow->GetWindowHandle(), + startXY, + endXY); + } +} + +void AccessibilityNotifier::NotifyConsoleLayoutEvent() +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_LAYOUT, + pWindow->GetWindowHandle(), + 0, + 0); + } +} + +void AccessibilityNotifier::NotifyConsoleStartApplicationEvent(_In_ DWORD processId) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_START_APPLICATION, + pWindow->GetWindowHandle(), + processId, + 0); + } +} + +void AccessibilityNotifier::NotifyConsoleEndApplicationEvent(_In_ DWORD processId) +{ + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow) + { + NotifyWinEvent(EVENT_CONSOLE_END_APPLICATION, + pWindow->GetWindowHandle(), + processId, + 0); + } +} diff --git a/src/interactivity/win32/AccessibilityNotifier.hpp b/src/interactivity/win32/AccessibilityNotifier.hpp new file mode 100644 index 000000000..74402e581 --- /dev/null +++ b/src/interactivity/win32/AccessibilityNotifier.hpp @@ -0,0 +1,39 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IAccessibilityNotifier.hpp + +Abstract: +- Win32 implementation of the IAccessibilityNotifier interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "precomp.h" + +#include "..\inc\IAccessibilityNotifier.hpp" + +#pragma hdrstop + +namespace Microsoft::Console::Interactivity::Win32 +{ + class AccessibilityNotifier final : public IAccessibilityNotifier + { + public: + ~AccessibilityNotifier() = default; + + void NotifyConsoleCaretEvent(_In_ RECT rectangle); + void NotifyConsoleCaretEvent(_In_ ConsoleCaretEventFlags flags, _In_ LONG position); + void NotifyConsoleUpdateScrollEvent(_In_ LONG x, _In_ LONG y); + void NotifyConsoleUpdateSimpleEvent(_In_ LONG start, _In_ LONG charAndAttribute); + void NotifyConsoleUpdateRegionEvent(_In_ LONG startXY, _In_ LONG endXY); + void NotifyConsoleLayoutEvent(); + void NotifyConsoleStartApplicationEvent(_In_ DWORD processId); + void NotifyConsoleEndApplicationEvent(_In_ DWORD processId); + }; +} diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp new file mode 100644 index 000000000..d9d65e86b --- /dev/null +++ b/src/interactivity/win32/Clipboard.cpp @@ -0,0 +1,607 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "clipboard.hpp" +#include "resource.h" + +#include "..\..\host\dbcs.h" +#include "..\..\host\scrolling.hpp" +#include "..\..\host\output.h" + +#include "..\..\types\inc\convert.hpp" +#include "..\..\types\inc\viewport.hpp" + +#include "..\inc\conint.h" +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Types; + +#pragma region Public Methods + +// Arguments: +// - fAlsoCopyHtml - Place colored HTML text onto the clipboard as well as the usual plain text. +// Return Value: +// +// NOTE: if the registry is set to always copy color data then we will even if fAlsoCopyHTML is false +void Clipboard::Copy(bool fAlsoCopyHtml) +{ + try + { + // registry settings may tell us to always copy the color/formating + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + fAlsoCopyHtml = fAlsoCopyHtml || gci.GetCopyColor(); + + // store selection in clipboard + StoreSelectionToClipboard(fAlsoCopyHtml); + Selection::Instance().ClearSelection(); // clear selection in console + } + CATCH_LOG(); +} + +/*++ + +Perform paste request into old app by pulling out clipboard +contents and writing them to the console's input buffer + +--*/ +void Clipboard::Paste() +{ + HANDLE ClipboardDataHandle; + + // Clear any selection or scrolling that may be active. + Selection::Instance().ClearSelection(); + Scrolling::s_ClearScroll(); + + // Get paste data from clipboard + if (!OpenClipboard(ServiceLocator::LocateConsoleWindow()->GetWindowHandle())) + { + return; + } + + ClipboardDataHandle = GetClipboardData(CF_UNICODETEXT); + if (ClipboardDataHandle == nullptr) + { + CloseClipboard(); + return; + } + + PWCHAR pwstr = (PWCHAR)GlobalLock(ClipboardDataHandle); + StringPaste(pwstr, (ULONG)GlobalSize(ClipboardDataHandle) / sizeof(WCHAR)); + + // WIP auditing if user is enrolled + static std::wstring DestinationName = _LoadString(ID_CONSOLE_WIP_DESTINATIONNAME); + Microsoft::Console::Internal::EdpPolicy::AuditClipboard(DestinationName); + + GlobalUnlock(ClipboardDataHandle); + + CloseClipboard(); +} + +Clipboard& Clipboard::Instance() +{ + static Clipboard clipboard; + return clipboard; +} + +// Routine Description: +// - This routine pastes given Unicode string into the console window. +// Arguments: +// - pData - Unicode string that is pasted to the console window +// - cchData - Size of the Unicode String in characters +// Return Value: +// - None +void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData, + const size_t cchData) +{ + if (pData == nullptr) + { + return; + } + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + try + { + std::deque> inEvents = TextToKeyEvents(pData, cchData); + gci.pInputBuffer->Write(inEvents); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +#pragma endregion + +#pragma region Private Methods + +// Routine Description: +// - converts a wchar_t* into a series of KeyEvents as if it was typed +// from the keyboard +// Arguments: +// - pData - the text to convert +// - cchData - the size of pData, in wchars +// Return Value: +// - deque of KeyEvents that represent the string passed in +// Note: +// - will throw exception on error +std::deque> Clipboard::TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, + const size_t cchData) +{ + THROW_IF_NULL_ALLOC(pData); + + std::deque> keyEvents; + + for (size_t i = 0; i < cchData; ++i) + { + wchar_t currentChar = pData[i]; + + const bool charAllowed = FilterCharacterOnPaste(¤tChar); + // filter out linefeed if it's not the first char and preceded + // by a carriage return + const bool skipLinefeed = (i != 0 && + currentChar == UNICODE_LINEFEED && + pData[i - 1] == UNICODE_CARRIAGERETURN); + + if (!charAllowed || skipLinefeed) + { + continue; + } + + if (currentChar == 0) + { + break; + } + + // MSFT:12123975 / WSL GH#2006 + // If you paste text with ONLY linefeed line endings (unix style) in wsl, + // then we faithfully pass those along, which the underlying terminal + // interprets as C-j. In nano, C-j is mapped to "Justify text", which + // causes the pasted text to get broken at the width of the terminal. + // This behavior doesn't occur in gnome-terminal, and nothing like it occurs + // in vi or emacs. + // This change doesn't break pasting text into any of those applications + // with CR/LF (Windows) line endings either. That apparently always + // worked right. + if (IsInVirtualTerminalInputMode() && currentChar == UNICODE_LINEFEED) + { + currentChar = UNICODE_CARRIAGERETURN; + } + + const UINT codepage = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP; + std::deque> convertedEvents = CharToKeyEvents(currentChar, codepage); + while (!convertedEvents.empty()) + { + keyEvents.push_back(std::move(convertedEvents.front())); + convertedEvents.pop_front(); + } + } + return keyEvents; +} + +// Routine Description: +// - Copies the selected area onto the global system clipboard. +// - NOTE: Throws on allocation and other clipboard failures. +// Arguments: +// - fAlsoCopyHtml - This will also place colored HTML text onto the clipboard as well as the usual plain text. +// Return Value: +// +void Clipboard::StoreSelectionToClipboard(bool const fAlsoCopyHtml) +{ + const auto& selection = Selection::Instance(); + + // See if there is a selection to get + if (!selection.IsAreaSelected()) + { + return; + } + + // read selection area. + const auto selectionRects = selection.GetSelectionRects(); + const bool lineSelection = Selection::Instance().IsLineSelection(); + + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& screenInfo = gci.GetActiveOutputBuffer(); + + const auto text = RetrieveTextFromBuffer(screenInfo, + lineSelection, + selectionRects); + + CopyTextToSystemClipboard(text, fAlsoCopyHtml); +} + +// Routine Description: +// - Retrieves the text data from the selected region of the text buffer +// Arguments: +// - screenInfo - what is rendered on the screen +// - lineSelection - true if entire line is being selected. False otherwise (box selection) +// - selectionRects - the selection regions from which the data will be extracted from the buffer +TextBuffer::TextAndColor Clipboard::RetrieveTextFromBuffer(const SCREEN_INFORMATION& screenInfo, + const bool lineSelection, + const std::vector& selectionRects) +{ + const auto &buffer = screenInfo.GetTextBuffer(); + const bool trimTrailingWhitespace = !WI_IsFlagSet(GetKeyState(VK_SHIFT), KEY_PRESSED); + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + std::function GetForegroundColor = std::bind(&CONSOLE_INFORMATION::LookupForegroundColor, &gci, std::placeholders::_1); + std::function GetBackgroundColor = std::bind(&CONSOLE_INFORMATION::LookupBackgroundColor, &gci, std::placeholders::_1); + + return buffer.GetTextForClipboard(lineSelection, + trimTrailingWhitespace, + selectionRects, + GetForegroundColor, + GetBackgroundColor); +} + +// Routine Description: +// - Generates a CF_HTML compliant structure based on the passed in text and color data +// Arguments: +// - rows - the text and color data we will format & encapsulate +// Return Value: +// - string containing the generated HTML +std::string Clipboard::GenHTML(const TextBuffer::TextAndColor& rows) +{ + std::string szClipboard; // we will build the data going back in this string buffer + + try + { + std::string const szHtmlClipFormat = + "Version:0.9\r\n" + "StartHTML:%010d\r\n" + "EndHTML:%010d\r\n" + "StartFragment:%010d\r\n" + "EndFragment:%010d\r\n" + "StartSelection:%010d\r\n" + "EndSelection:%010d\r\n"; + + // measure clip header + size_t const cbHeader = 157; // when formats are expanded, there will be 157 bytes in the header. + + std::string const szHtmlHeader = + "Windows Console Host"; + size_t const cbHtmlHeader = szHtmlHeader.size(); + + std::string const szHtmlFragStart = ""; + std::string const szHtmlFragEnd = ""; + std::string const szHtmlFooter = ""; + size_t const cbHtmlFooter = szHtmlFooter.size(); + + std::string const szDivOuterBackgroundPattern = R"X(
)X"; + + size_t const cbDivOuter = 55; + std::string szDivOuter; + szDivOuter.reserve(cbDivOuter); + + std::string const szSpanFontSizePattern = R"X()X"; + + const auto& fontData = ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetCurrentFont(); + int const iFontHeightPoints = fontData.GetUnscaledSize().Y * 72 / ServiceLocator::LocateGlobals().dpi; + size_t const cbSpanFontSize = 28 + (iFontHeightPoints / 10) + 1; + + std::string szSpanFontSize; + szSpanFontSize.resize(cbSpanFontSize + 1); // reserve space for null after string for sprintf + sprintf_s(szSpanFontSize.data(), cbSpanFontSize + 1, szSpanFontSizePattern.data(), iFontHeightPoints); + szSpanFontSize.resize(cbSpanFontSize); //chop off null at end + + std::string const szSpanStartPattern = R"X()X"; + + size_t const cbSpanStart = 53; // when format is expanded, there will be 53 bytes per color pattern. + std::string szSpanStart; + szSpanStart.resize(cbSpanStart + 1); // +1 for null terminator + + std::string const szSpanStartFontPattern = R"X()X"; + size_t const cbSpanStartFontPattern = 41; + + std::string const szSpanStartFontConstant = R"X()X"; + size_t const cbSpanStartFontConstant = 37; + + std::string szSpanStartFont; + size_t cbSpanStartFont; + bool fDeleteSpanStartFont = false; + + std::wstring const wszFontFaceName = fontData.GetFaceName(); + size_t const cchFontFaceName = wszFontFaceName.size(); + if (cchFontFaceName > 0) + { + // measure and create buffer to convert face name to UTF8 + int const cbNeeded = WideCharToMultiByte(CP_UTF8, 0, wszFontFaceName.data(), static_cast(cchFontFaceName), nullptr, 0, nullptr, nullptr); + std::string szBuffer; + szBuffer.resize(cbNeeded); + + // do conversion + WideCharToMultiByte(CP_UTF8, 0, wszFontFaceName.data(), static_cast(cchFontFaceName), szBuffer.data(), cbNeeded, nullptr, nullptr); + + // format converted font name into pattern + std::string const szFinalFontPattern = R"X()X"; + size_t const cbBytesNeeded = szFinalFontPattern.length(); + + fDeleteSpanStartFont = true; + szSpanStartFont = szFinalFontPattern; + cbSpanStartFont = cbBytesNeeded; + } + else + { + szSpanStartFont = szSpanStartFontConstant; + cbSpanStartFont = cbSpanStartFontConstant; + } + + std::string const szSpanEnd = ""; + std::string const szDivEnd = "
"; + + // Start building the HTML formated string to return + // First we have to add the required header and then + // some standard HTML boiler plate required for CF_HTML + // as part of the HTML Clipboard format + szClipboard.append(cbHeader, 'H'); // reserve space for a header we fill in later + szClipboard.append(szHtmlHeader); + szClipboard.append(szHtmlFragStart); + + COLORREF iBgColor = rows.BkAttr.at(0).at(0); + + szDivOuter.resize(cbDivOuter + 1); + sprintf_s(szDivOuter.data(), cbDivOuter + 1, szDivOuterBackgroundPattern.data(), GetRValue(iBgColor), GetGValue(iBgColor), GetBValue(iBgColor)); + szDivOuter.resize(cbDivOuter); + szClipboard.append(szDivOuter); + + // copy font face start + szClipboard.append(szSpanStartFont); + + // copy font size start + szClipboard.append(szSpanFontSize); + + bool bColorFound = false; + + // copy all text into the final clipboard data handle. There should be no nulls between rows of + // characters, but there should be a \0 at the end. + for (UINT iRow = 0; iRow < rows.text.size(); iRow++) + { + size_t cbStartOffset = 0; + size_t cchCharsToPrint = 0; + + COLORREF const Blackness = RGB(0x00, 0x00, 0x00); + COLORREF fgColor = Blackness; + COLORREF bkColor = Blackness; + + for (UINT iCol = 0; iCol < rows.text.at(iRow).length(); iCol++) + { + bool fColorDelta = false; + + if (!bColorFound) + { + fgColor = rows.FgAttr.at(iRow).at(iCol); + bkColor = rows.BkAttr.at(iRow).at(iCol); + bColorFound = true; + fColorDelta = true; + } + else if ((rows.FgAttr.at(iRow).at(iCol) != fgColor) || (rows.BkAttr.at(iRow).at(iCol) != bkColor)) + { + fgColor = rows.FgAttr.at(iRow).at(iCol); + bkColor = rows.BkAttr.at(iRow).at(iCol); + fColorDelta = true; + } + + if (fColorDelta) + { + if (cchCharsToPrint > 0) + { + // write accumulated characters to stream .... + std::string TempBuff; + int const cbTempCharsNeeded = WideCharToMultiByte(CP_UTF8, 0, rows.text[iRow].data() + cbStartOffset, static_cast(cchCharsToPrint), nullptr, 0, nullptr, nullptr); + TempBuff.resize(cbTempCharsNeeded); + WideCharToMultiByte(CP_UTF8, 0, rows.text[iRow].data() + cbStartOffset, static_cast(cchCharsToPrint), TempBuff.data(), cbTempCharsNeeded, nullptr, nullptr); + szClipboard.append(TempBuff); + cbStartOffset += cchCharsToPrint; + cchCharsToPrint = 0; + + // close previous span + szClipboard += szSpanEnd; + } + + // start new span + + // format with color then copy formatted string + szSpanStart.resize(cbSpanStart + 1); // add room for null + sprintf_s(szSpanStart.data(), cbSpanStart + 1, szSpanStartPattern.data(), + GetRValue(fgColor), GetGValue(fgColor), GetBValue(fgColor), + GetRValue(bkColor), GetGValue(bkColor), GetBValue(bkColor)); + szSpanStart.resize(cbSpanStart); // chop null from sprintf + szClipboard.append(szSpanStart); + + } + + // accumulate 1 character + cchCharsToPrint++; + } + + PCWCHAR pwchAccumulateStart = rows.text.at(iRow).data() + cbStartOffset; + + // write accumulated characters to stream + std::string CharsConverted; + int cbCharsConverted = WideCharToMultiByte(CP_UTF8, 0, pwchAccumulateStart, static_cast(cchCharsToPrint), nullptr, 0, nullptr, nullptr); + CharsConverted.resize(cbCharsConverted); + WideCharToMultiByte(CP_UTF8, 0, pwchAccumulateStart, static_cast(cchCharsToPrint), CharsConverted.data(), cbCharsConverted, nullptr, nullptr); + szClipboard.append(CharsConverted); + } + + if (bColorFound) + { + // copy end span + szClipboard.append(szSpanEnd); + } + + // after we have copied all text we must wrap up + // with a standard set of HTML boilerplate required + // by CF_HTML + + // copy end font size span + szClipboard.append(szSpanEnd); + + // copy end font face span + szClipboard.append(szSpanEnd); + + // copy end background color span + szClipboard.append(szDivEnd); + + // copy HTML end fragment + szClipboard.append(szHtmlFragEnd); + + // copy HTML footer + szClipboard.append(szHtmlFooter); + + // null terminate the clipboard data + szClipboard += '\0'; + + // we are done generating formating & building HTML for the selection + // prepare the header text with the byte counts now that we know them + size_t const cbHtmlStart = cbHeader; // bytecount to start of HTML context + size_t const cbHtmlEnd = szClipboard.size() - 1; // don't count the null at the end + size_t const cbFragStart = cbHeader + cbHtmlHeader; // bytecount to start of selection fragment + size_t const cbFragEnd = cbHtmlEnd - cbHtmlFooter; + + // push the values into the required HTML 0.9 header format + std::string szHtmlClipHeaderFinal; + szHtmlClipHeaderFinal.resize(cbHeader + 1); // add room for a null + sprintf_s(szHtmlClipHeaderFinal.data(), cbHeader + 1, szHtmlClipFormat.data(), cbHtmlStart, cbHtmlEnd, cbFragStart, cbFragEnd, cbFragStart, cbFragEnd); + szHtmlClipHeaderFinal.resize(cbHeader); // chop off the null + + // overwrite the reserved space with the actual header & offsets we calculated + szClipboard.replace(0, cbHeader, szHtmlClipHeaderFinal.data()); + } + catch(...) + { + LOG_HR(wil::ResultFromCaughtException()); + szClipboard.clear(); // dont return a partial html fragment... + } + + return szClipboard; +} + + +// Routine Description: +// - Copies the text given onto the global system clipboard. +// Arguments: +// - rows - Rows of text data to copy +void Clipboard::CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, bool const fAlsoCopyHtml) +{ + std::wstring finalString; + + // Concatenate strings into one giant string to put onto the clipboard. + for (const auto& str : rows.text) + { + finalString += str; + } + + // allocate the final clipboard data + const size_t cchNeeded = finalString.size() + 1; + const size_t cbNeeded = sizeof(wchar_t) * cchNeeded; + wil::unique_hglobal globalHandle(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbNeeded)); + THROW_LAST_ERROR_IF_NULL(globalHandle.get()); + + PWSTR pwszClipboard = (PWSTR)GlobalLock(globalHandle.get()); + THROW_LAST_ERROR_IF_NULL(pwszClipboard); + + // The pattern gets a bit strange here because there's no good wil built-in for global lock of this type. + // Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock). + const HRESULT hr = StringCchCopyW(pwszClipboard, cchNeeded, finalString.data()); + GlobalUnlock(globalHandle.get()); + THROW_IF_FAILED(hr); + + // Set global data to clipboard + THROW_LAST_ERROR_IF(!OpenClipboard(ServiceLocator::LocateConsoleWindow()->GetWindowHandle())); + THROW_LAST_ERROR_IF(!EmptyClipboard()); + THROW_LAST_ERROR_IF_NULL(SetClipboardData(CF_UNICODETEXT, globalHandle.get())); + + if (fAlsoCopyHtml) + { + std::string HTMLToPlaceOnClip = GenHTML(rows); + const size_t cbNeededHTML = HTMLToPlaceOnClip.size(); + if (cbNeededHTML) + { + wil::unique_hglobal globalHandleHTML(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbNeededHTML)); + THROW_LAST_ERROR_IF_NULL(globalHandleHTML.get()); + + PSTR pszClipboardHTML = (PSTR)GlobalLock(globalHandleHTML.get()); + THROW_LAST_ERROR_IF_NULL(pszClipboardHTML); + + // The pattern gets a bit strange here because there's no good wil built-in for global lock of this type. + // Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock). + const HRESULT hr2 = StringCchCopyA(pszClipboardHTML, cbNeededHTML, HTMLToPlaceOnClip.data()); + GlobalUnlock(globalHandleHTML.get()); + THROW_IF_FAILED(hr2); + + UINT const CF_HTML = RegisterClipboardFormatW(L"HTML Format"); + THROW_LAST_ERROR_IF(0 == CF_HTML); + + THROW_LAST_ERROR_IF_NULL(SetClipboardData(CF_HTML, globalHandleHTML.get())); + + // only free if we failed. + // the memory has to remain allocated if we successfully placed it on the clipboard. + // Releasing the smart pointer will leave it allocated as we exit scope. + globalHandleHTML.release(); + } + } + + THROW_LAST_ERROR_IF(!CloseClipboard()); + + // only free if we failed. + // the memory has to remain allocated if we successfully placed it on the clipboard. + // Releasing the smart pointer will leave it allocated as we exit scope. + globalHandle.release(); +} + + +// Returns true if the character should be emitted to the paste stream +// -- in some cases, we will change what character should be emitted, as in the case of "smart quotes" +// Returns false if the character should not be emitted (e.g. ) +bool Clipboard::FilterCharacterOnPaste(_Inout_ WCHAR * const pwch) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + bool fAllowChar = true; + if (gci.GetFilterOnPaste() && + (WI_IsFlagSet(gci.pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT))) + { + switch (*pwch) + { + // swallow tabs to prevent inadvertant tab expansion + case UNICODE_TAB: + { + fAllowChar = false; + break; + } + + // Replace Unicode space with standard space + case UNICODE_NBSP: + case UNICODE_NARROW_NBSP: + { + *pwch = UNICODE_SPACE; + break; + } + + // Replace "smart quotes" with "dumb ones" + case UNICODE_LEFT_SMARTQUOTE: + case UNICODE_RIGHT_SMARTQUOTE: + { + *pwch = UNICODE_QUOTE; + break; + } + + // Replace Unicode dashes with a standard hypen + case UNICODE_EM_DASH: + case UNICODE_EN_DASH: + { + *pwch = UNICODE_HYPHEN; + break; + } + } + } + + return fAllowChar; +} + +#pragma endregion diff --git a/src/interactivity/win32/ConsoleControl.cpp b/src/interactivity/win32/ConsoleControl.cpp new file mode 100644 index 000000000..61720b984 --- /dev/null +++ b/src/interactivity/win32/ConsoleControl.cpp @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#ifndef CON_USERPRIVAPI_INDIRECT +#include +#endif + +#include "ConsoleControl.hpp" + +#include "..\..\interactivity\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +#pragma region IConsoleControl Members + +[[nodiscard]] +NTSTATUS ConsoleControl::NotifyConsoleApplication(_In_ DWORD dwProcessId) +{ + CONSOLE_PROCESS_INFO cpi; + cpi.dwProcessID = dwProcessId; + cpi.dwFlags = CPI_NEWPROCESSWINDOW; + + return Control(ControlType::ConsoleNotifyConsoleApplication, + &cpi, + sizeof(CONSOLE_PROCESS_INFO)); +} + +[[nodiscard]] +NTSTATUS ConsoleControl::SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground) +{ + CONSOLESETFOREGROUND Flags; + Flags.hProcess = hProcess; + Flags.bForeground = fForeground; + + return Control(ControlType::ConsoleSetForeground, + &Flags, + sizeof(Flags)); +} + +[[nodiscard]] +NTSTATUS ConsoleControl::EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags) +{ + auto pConsoleWindow = ServiceLocator::LocateConsoleWindow(); + + CONSOLEENDTASK ConsoleEndTaskParams; + ConsoleEndTaskParams.ProcessId = hProcessId; + ConsoleEndTaskParams.ConsoleEventCode = dwEventType; + ConsoleEndTaskParams.ConsoleFlags = ulCtrlFlags; + ConsoleEndTaskParams.hwnd = pConsoleWindow == nullptr + ? nullptr + : pConsoleWindow->GetWindowHandle(); + + return Control(ControlType::ConsoleEndTask, + &ConsoleEndTaskParams, + sizeof(ConsoleEndTaskParams)); +} + +#pragma endregion + +#pragma region Public Methods + +[[nodiscard]] +NTSTATUS ConsoleControl::Control(_In_ ControlType ConsoleCommand, + _In_reads_bytes_(ConsoleInformationLength) PVOID ConsoleInformation, + _In_ DWORD ConsoleInformationLength) +{ +#ifdef CON_USERPRIVAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef NTSTATUS(WINAPI *PfnConsoleControl)(ControlType Command, PVOID Information, DWORD Length); + + static PfnConsoleControl pfn = (PfnConsoleControl)GetProcAddress(_hUser32, "ConsoleControl"); + + if (pfn != nullptr) + { + return pfn(ConsoleCommand, ConsoleInformation, ConsoleInformationLength); + } + } + + return STATUS_UNSUCCESSFUL; +#else + return ConsoleControl(ConsoleCommand, ConsoleInformation, ConsoleInformationLength); +#endif +} + +BOOL ConsoleControl::EnterReaderModeHelper(_In_ HWND hwnd) +{ +#ifdef CON_USERPRIVAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef BOOL(WINAPI *PfnEnterReaderModeHelper)(HWND hwnd); + + static PfnEnterReaderModeHelper pfn = (PfnEnterReaderModeHelper)GetProcAddress(_hUser32, "EnterReaderModeHelper"); + + if (pfn != nullptr) + { + return pfn(hwnd); + } + } + + return FALSE; +#else + return EnterReaderModeHelper(hwnd); +#endif +} + +BOOL ConsoleControl::TranslateMessageEx(const MSG *pmsg, + _In_ UINT flags) +{ +#ifdef CON_USERPRIVAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef BOOL(WINAPI *PfnTranslateMessageEx)(const MSG *pmsg, UINT flags); + + static PfnTranslateMessageEx pfn = (PfnTranslateMessageEx)GetProcAddress(_hUser32, "TranslateMessageEx"); + + if (pfn != nullptr) + { + return pfn(pmsg, flags); + } + } + + return FALSE; +#else + return TranslateMessageEx(pmsg, flags); +#endif +} + +#pragma endregion + +#ifdef CON_USERPRIVAPI_INDIRECT +ConsoleControl::ConsoleControl() +{ + _hUser32 = LoadLibraryW(L"user32.dll"); +} + +ConsoleControl::~ConsoleControl() +{ + if (_hUser32 != nullptr) + { + FreeLibrary(_hUser32); + _hUser32 = nullptr; + } +} +#endif diff --git a/src/interactivity/win32/ConsoleControl.hpp b/src/interactivity/win32/ConsoleControl.hpp new file mode 100644 index 000000000..b03bce56b --- /dev/null +++ b/src/interactivity/win32/ConsoleControl.hpp @@ -0,0 +1,73 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- userdpiapi.hpp + +Abstract: +- This module is used for abstracting calls to private user32 DLL APIs to break the build system dependency. + +Author(s): +- Michael Niksa (MiNiksa) July-2016 +--*/ +#pragma once + +#include "..\inc\IConsoleControl.hpp" + +// Uncomment to build publically targeted scenarios. +//#define CON_USERPRIVAPI_INDIRECT + +// Used by TranslateMessageEx to purposefully return false to certain WM_KEYDOWN/WM_CHAR messages +#define TM_POSTCHARBREAKS 0x0002 + +// Used by window structures to place our special frozen-console painting data +#define GWL_CONSOLE_WNDALLOC (3 * sizeof(DWORD)) + +// Used for pre-resize querying of the new scaled size of a window when the DPI is about to change. +#define WM_GETDPISCALEDSIZE 0x02E4 + +namespace Microsoft::Console::Interactivity::Win32 +{ + class ConsoleControl final : public IConsoleControl + { + public: + enum ControlType { + ConsoleSetVDMCursorBounds, + ConsoleNotifyConsoleApplication, + ConsoleFullscreenSwitch, + ConsoleSetCaretInfo, + ConsoleSetReserveKeys, + ConsoleSetForeground, + ConsoleSetWindowOwner, + ConsoleEndTask, + }; + + // IConsoleControl Members + [[nodiscard]] + NTSTATUS NotifyConsoleApplication(_In_ DWORD dwProcessId); + [[nodiscard]] + NTSTATUS SetForeground(_In_ HANDLE hProcess, _In_ BOOL fForeground); + [[nodiscard]] + NTSTATUS EndTask(_In_ HANDLE hProcessId, _In_ DWORD dwEventType, _In_ ULONG ulCtrlFlags); + + // Public Members + [[nodiscard]] + NTSTATUS Control(_In_ ConsoleControl::ControlType ConsoleCommand, + _In_reads_bytes_(ConsoleInformationLength) PVOID ConsoleInformation, + _In_ DWORD ConsoleInformationLength); + + BOOL EnterReaderModeHelper(_In_ HWND hwnd); + + BOOL TranslateMessageEx(const MSG *pmsg, + _In_ UINT flags); + +#ifdef CON_USERPRIVAPI_INDIRECT + ConsoleControl(); + ~ConsoleControl(); + + private: + HMODULE _hUser32; +#endif + }; +} diff --git a/src/interactivity/win32/ConsoleInputThread.cpp b/src/interactivity/win32/ConsoleInputThread.cpp new file mode 100644 index 000000000..95bce033b --- /dev/null +++ b/src/interactivity/win32/ConsoleInputThread.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ConsoleInputThread.hpp" + +#include "WindowIo.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +// Routine Description: +// - Starts the Win32-specific console input thread. +HANDLE ConsoleInputThread::Start() +{ + HANDLE hThread = nullptr; + DWORD dwThreadId = (DWORD) -1; + + hThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)ConsoleInputThreadProcWin32, + nullptr, + 0, + &dwThreadId); + + if (hThread) + { + _hThread = hThread; + _dwThreadId = dwThreadId; + } + + return hThread; +} diff --git a/src/interactivity/win32/ConsoleInputThread.hpp b/src/interactivity/win32/ConsoleInputThread.hpp new file mode 100644 index 000000000..d560b73d6 --- /dev/null +++ b/src/interactivity/win32/ConsoleInputThread.hpp @@ -0,0 +1,27 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ConsoleInputThread.hpp + +Abstract: +- Win32 implementation of the IConsoleInputThread interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#include "precomp.h" + +#include "..\inc\IConsoleInputThread.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class ConsoleInputThread final : public IConsoleInputThread + { + public: + ~ConsoleInputThread() = default; + HANDLE Start(); + }; +} diff --git a/src/interactivity/win32/CustomWindowMessages.h b/src/interactivity/win32/CustomWindowMessages.h new file mode 100644 index 000000000..b582f96f6 --- /dev/null +++ b/src/interactivity/win32/CustomWindowMessages.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// Custom window messages +#define CM_SET_WINDOW_SIZE (WM_USER + 2) +#define CM_BEEP (WM_USER + 3) +#define CM_UPDATE_SCROLL_BARS (WM_USER + 4) +#define CM_UPDATE_TITLE (WM_USER + 5) +// CM_MODE_TRANSITION is hard-coded to WM_USER + 6 in kernel\winmgr.c +// unused (CM_MODE_TRANSITION) (WM_USER + 6) +// unused (CM_CONSOLE_SHUTDOWN) (WM_USER + 7) +// unused (CM_HIDE_WINDOW) (WM_USER + 8) +#define CM_CONIME_CREATE (WM_USER+9) +#define CM_SET_CONSOLEIME_WINDOW (WM_USER+10) +#define CM_WAIT_CONIME_PROCESS (WM_USER+11) +// unused CM_SET_IME_CODEPAGE (WM_USER+12) +// unused CM_SET_NLSMODE (WM_USER+13) +// unused CM_GET_NLSMODE (WM_USER+14) +#define CM_CONIME_KL_ACTIVATE (WM_USER+15) +#define CM_CONSOLE_MSG (WM_USER+16) +#define CM_UPDATE_EDITKEYS (WM_USER+17) + +#ifdef DBG +#define CM_SET_KEY_STATE (WM_USER+18) +#define CM_SET_KEYBOARD_LAYOUT (WM_USER+19) +#endif diff --git a/src/interactivity/win32/InputServices.cpp b/src/interactivity/win32/InputServices.cpp new file mode 100644 index 000000000..ece52e084 --- /dev/null +++ b/src/interactivity/win32/InputServices.cpp @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "InputServices.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Interactivity::Win32; + +UINT InputServices::MapVirtualKeyW(UINT uCode, UINT uMapType) +{ + return ::MapVirtualKeyW(uCode, uMapType); +} + +SHORT InputServices::VkKeyScanW(WCHAR ch) +{ + return ::VkKeyScanW(ch); +} + +SHORT InputServices::GetKeyState(int nVirtKey) +{ + return ::GetKeyState(nVirtKey); +} + +BOOL InputServices::TranslateCharsetInfo(DWORD * lpSrc, LPCHARSETINFO lpCs, DWORD dwFlags) +{ + return ::TranslateCharsetInfo(lpSrc, lpCs, dwFlags); +} diff --git a/src/interactivity/win32/InputServices.hpp b/src/interactivity/win32/InputServices.hpp new file mode 100644 index 000000000..321070e26 --- /dev/null +++ b/src/interactivity/win32/InputServices.hpp @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- InputServices.hpp + +Abstract: +- Win32 implementation of the IInputServices interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#include "..\inc\IInputServices.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class InputServices final : public IInputServices + { + public: + // Inherited via IInputServices + ~InputServices() = default; + UINT MapVirtualKeyW(UINT uCode, UINT uMapType); + SHORT VkKeyScanW(WCHAR ch); + SHORT GetKeyState(int nVirtKey); + BOOL TranslateCharsetInfo(DWORD * lpSrc, LPCHARSETINFO lpCs, DWORD dwFlags); + }; +} diff --git a/src/interactivity/win32/SystemConfigurationProvider.cpp b/src/interactivity/win32/SystemConfigurationProvider.cpp new file mode 100644 index 000000000..cea6fcf07 --- /dev/null +++ b/src/interactivity/win32/SystemConfigurationProvider.cpp @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "SystemConfigurationProvider.hpp" + +#include "icon.hpp" +#include "..\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +UINT SystemConfigurationProvider::GetCaretBlinkTime() +{ + return ::GetCaretBlinkTime(); +} + +bool SystemConfigurationProvider::IsCaretBlinkingEnabled() +{ + return GetSystemMetrics(SM_CARETBLINKINGENABLED) ? true : false; +} + +int SystemConfigurationProvider::GetNumberOfMouseButtons() +{ + return GetSystemMetrics(SM_CMOUSEBUTTONS); +} + +ULONG SystemConfigurationProvider::GetCursorWidth() +{ + ULONG width; + if (SystemParametersInfoW(SPI_GETCARETWIDTH, 0, &width, FALSE)) + { + return width; + } + else + { + LOG_LAST_ERROR(); + return s_DefaultCursorWidth; + } +} + +ULONG SystemConfigurationProvider::GetNumberOfWheelScrollLines() +{ + ULONG lines; + SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &lines, FALSE); + + return lines; +} + +ULONG SystemConfigurationProvider::GetNumberOfWheelScrollCharacters() +{ + ULONG characters; + SystemParametersInfoW(SPI_GETWHEELSCROLLCHARS, 0, &characters, FALSE); + + return characters; +} + +void SystemConfigurationProvider::GetSettingsFromLink( + _Inout_ Settings* pLinkSettings, + _Inout_updates_bytes_(*pdwTitleLength) LPWSTR pwszTitle, + _Inout_ PDWORD pdwTitleLength, + _In_ PCWSTR pwszCurrDir, + _In_ PCWSTR pwszAppName) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WCHAR wszIconLocation[MAX_PATH] = { 0 }; + int iIconIndex = 0; + + pLinkSettings->SetCodePage(ServiceLocator::LocateGlobals().uiOEMCP); + + // Did we get started from a link? + if (pLinkSettings->GetStartupFlags() & STARTF_TITLEISLINKNAME) + { + if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) + { + size_t const cch = *pdwTitleLength / sizeof(wchar_t); + + gci.SetLinkTitle(std::wstring(pwszTitle, cch)); + + wchar_t* const linkNameForCsi = new(std::nothrow) wchar_t[gci.GetLinkTitle().length()+1]{0}; + if (linkNameForCsi) + { + gci.GetLinkTitle().copy(linkNameForCsi, gci.GetLinkTitle().length()); + } + + CONSOLE_STATE_INFO csi = pLinkSettings->CreateConsoleStateInfo(); + csi.LinkTitle = linkNameForCsi; + WCHAR wszShortcutTitle[MAX_PATH]; + BOOL fReadConsoleProperties; + WORD wShowWindow = pLinkSettings->GetShowWindow(); + DWORD dwHotKey = pLinkSettings->GetHotKey(); + + int iShowWindow; + WORD wHotKey; + NTSTATUS Status = ShortcutSerialization::s_GetLinkValues(&csi, + &fReadConsoleProperties, + wszShortcutTitle, + ARRAYSIZE(wszShortcutTitle), + wszIconLocation, + ARRAYSIZE(wszIconLocation), + &iIconIndex, + &iShowWindow, + &wHotKey); + + // Convert results back to appropriate types and set. + if (SUCCEEDED(IntToWord(iShowWindow, &wShowWindow))) + { + pLinkSettings->SetShowWindow(wShowWindow); + } + + dwHotKey = wHotKey; + pLinkSettings->SetHotKey(dwHotKey); + + // if we got a title, use it. even on overall link value load failure, the title will be correct if + // filled out. + if (wszShortcutTitle[0] != L'\0') + { + // guarantee null termination to make OACR happy. + wszShortcutTitle[ARRAYSIZE(wszShortcutTitle) - 1] = L'\0'; + StringCbCopyW(pwszTitle, *pdwTitleLength, wszShortcutTitle); + + // OACR complains about the use of a DWORD here, so roundtrip through a size_t + size_t cbTitleLength; + if (SUCCEEDED(StringCbLengthW(pwszTitle, *pdwTitleLength, &cbTitleLength))) + { + // don't care about return result -- the buffer is guaranteed null terminated to at least + // the length of Title + (void)SizeTToDWord(cbTitleLength, pdwTitleLength); + } + } + + if (NT_SUCCESS(Status) && fReadConsoleProperties) + { + // copy settings + pLinkSettings->InitFromStateInfo(&csi); + + // since we were launched via shortcut, make sure we don't let the invoker's STARTUPINFO pollute the + // shortcut's settings + pLinkSettings->UnsetStartupFlag(STARTF_USESIZE | STARTF_USECOUNTCHARS); + } + else + { + // if we didn't find any console properties, or otherwise failed to load link properties, pretend + // like we weren't launched from a shortcut -- this allows us to at least try to find registry + // settings based on title. + pLinkSettings->UnsetStartupFlag(STARTF_TITLEISLINKNAME); + } + CoUninitialize(); + } + } + + // Go get the icon + if (wszIconLocation[0] == L'\0') + { + // search for the application along the path so that we can load its icons (if we didn't find one explicitly in + // the shortcut) + const DWORD dwLinkLen = SearchPathW(pwszCurrDir, pwszAppName, nullptr, ARRAYSIZE(wszIconLocation), wszIconLocation, nullptr); + + // If we cannot find the application in the path, then try to fall back and see if the window title is a valid path and use that. + if (dwLinkLen <= 0 || dwLinkLen > sizeof(wszIconLocation)) + { + if (PathFileExistsW(pwszTitle) && (wcslen(pwszTitle) < sizeof(wszIconLocation))) + { + StringCchCopyW(wszIconLocation, ARRAYSIZE(wszIconLocation), pwszTitle); + } + else + { + // If all else fails, just stick the app name into the path and try to resolve just the app name. + StringCchCopyW(wszIconLocation, ARRAYSIZE(wszIconLocation), pwszAppName); + } + } + } + + if (wszIconLocation[0] != L'\0') + { + LOG_IF_FAILED(Icon::Instance().LoadIconsFromPath(wszIconLocation, iIconIndex)); + } + + if (!IsValidCodePage(pLinkSettings->GetCodePage())) + { + // make sure we don't leave this function with an invalid codepage + pLinkSettings->SetCodePage(ServiceLocator::LocateGlobals().uiOEMCP); + } +} diff --git a/src/interactivity/win32/SystemConfigurationProvider.hpp b/src/interactivity/win32/SystemConfigurationProvider.hpp new file mode 100644 index 000000000..b39cbad79 --- /dev/null +++ b/src/interactivity/win32/SystemConfigurationProvider.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- SystemConfigurationProvider.hpp + +Abstract: +- Win32 implementation of the ISystemConfigurationProvider interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#pragma once + +#include "precomp.h" + +#include "..\inc\ISystemConfigurationProvider.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class SystemConfigurationProvider final : public ISystemConfigurationProvider + { + public: + ~SystemConfigurationProvider() = default; + + bool IsCaretBlinkingEnabled(); + + UINT GetCaretBlinkTime(); + int GetNumberOfMouseButtons(); + ULONG GetCursorWidth() override; + ULONG GetNumberOfWheelScrollLines(); + ULONG GetNumberOfWheelScrollCharacters(); + + void GetSettingsFromLink(_Inout_ Settings* pLinkSettings, + _Inout_updates_bytes_(*pdwTitleLength) LPWSTR pwszTitle, + _Inout_ PDWORD pdwTitleLength, + _In_ PCWSTR pwszCurrDir, + _In_ PCWSTR pwszAppName); + + private: + static const ULONG s_DefaultCursorWidth = 1; + }; +} diff --git a/src/interactivity/win32/UiaTextRange.cpp b/src/interactivity/win32/UiaTextRange.cpp new file mode 100644 index 000000000..1c2233e5e --- /dev/null +++ b/src/interactivity/win32/UiaTextRange.cpp @@ -0,0 +1,2290 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "UiaTextRange.hpp" +#include "../inc/ServiceLocator.hpp" + +#include "window.hpp" +#include "windowdpiapi.hpp" +#include "../host/tracing.hpp" + +#include "../host/selection.hpp" +#include "../host/search.h" + + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Interactivity::Win32::UiaTextRangeTracing; + +// toggle these for additional logging in a debug build +//#define UIATEXTRANGE_DEBUG_MSGS 1 +#undef UIATEXTRANGE_DEBUG_MSGS + +IdType UiaTextRange::id = 0; + +UiaTextRange::MoveState::MoveState(const UiaTextRange& range, + const MovementDirection direction) : + StartScreenInfoRow{ UiaTextRange::_endpointToScreenInfoRow(range.GetStart()) }, + StartColumn{ UiaTextRange::_endpointToColumn(range.GetStart()) }, + EndScreenInfoRow{ UiaTextRange::_endpointToScreenInfoRow(range.GetEnd()) }, + EndColumn{ UiaTextRange::_endpointToColumn(range.GetEnd()) }, + Direction{ direction } +{ + if (direction == MovementDirection::Forward) + { + LimitingRow = UiaTextRange::_getLastScreenInfoRowIndex(); + FirstColumnInRow = UiaTextRange::_getFirstColumnIndex(); + LastColumnInRow = UiaTextRange::_getLastColumnIndex(); + Increment = MovementIncrement::Forward; + } + else + { + LimitingRow = UiaTextRange::_getFirstScreenInfoRowIndex(); + FirstColumnInRow = UiaTextRange::_getLastColumnIndex(); + LastColumnInRow = UiaTextRange::_getFirstColumnIndex(); + Increment = MovementIncrement::Backward; + } +} + +UiaTextRange::MoveState::MoveState(const ScreenInfoRow startScreenInfoRow, + const Column startColumn, + const ScreenInfoRow endScreenInfoRow, + const Column endColumn, + const ScreenInfoRow limitingRow, + const Column firstColumnInRow, + const Column lastColumnInRow, + const MovementIncrement increment, + const MovementDirection direction) : + StartScreenInfoRow{ startScreenInfoRow }, + StartColumn{ startColumn }, + EndScreenInfoRow{ endScreenInfoRow }, + EndColumn{ endColumn }, + LimitingRow{ limitingRow }, + FirstColumnInRow{ firstColumnInRow }, + LastColumnInRow{ lastColumnInRow }, + Increment{ increment }, + Direction{ direction } +{ +} + +#if _DEBUG +#include +// This is a debugging function that prints out the current +// relationship between screen info rows, text buffer rows, and +// endpoints. +void UiaTextRange::_outputRowConversions() +{ + try + { + unsigned int totalRows = _getTotalRows(); + OutputDebugString(L"screenBuffer\ttextBuffer\tendpoint\n"); + for (unsigned int i = 0; i < totalRows; ++i) + { + std::wstringstream ss; + ss << i << "\t" << _screenInfoRowToTextBufferRow (i) << "\t" << _screenInfoRowToEndpoint(i) << "\n"; + std::wstring str = ss.str(); + OutputDebugString(str.c_str()); + } + OutputDebugString(L"\n"); + } + catch(...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +void UiaTextRange::_outputObjectState() +{ + std::wstringstream ss; + ss << "Object State"; + ss << " _id: " << _id; + ss << " _start: " << _start; + ss << " _end: " << _end; + ss << " _degenerate: " << _degenerate; + + std::wstring str = ss.str(); + OutputDebugString(str.c_str()); + OutputDebugString(L"\n"); +} +#endif // _DEBUG + +std::deque UiaTextRange::GetSelectionRanges(_In_ IRawElementProviderSimple* pProvider) +{ + std::deque ranges; + + // get the selection rects + const auto rectangles = Selection::Instance().GetSelectionRects(); + + // create a range for each row + for (const auto& rect : rectangles) + { + ScreenInfoRow currentRow = rect.Top; + Endpoint start = _screenInfoRowToEndpoint(currentRow) + rect.Left; + Endpoint end = _screenInfoRowToEndpoint(currentRow) + rect.Right; + UiaTextRange* range = UiaTextRange::Create(pProvider, + start, + end, + false); + if (range == nullptr) + { + // something when wrong, clean up and throw + while (!ranges.empty()) + { + UiaTextRange* temp = ranges[0]; + ranges.pop_front(); + temp->Release(); + } + throw E_INVALIDARG; + } + else + { + ranges.push_back(range); + } + } + return ranges; +} + + +UiaTextRange* UiaTextRange::Create(_In_ IRawElementProviderSimple* const pProvider) +{ + UiaTextRange* range = nullptr;; + try + { + range = new UiaTextRange(pProvider); + } + catch (...) + { + range = nullptr; + } + + if (range) + { + pProvider->AddRef(); + } + return range; +} + +UiaTextRange* UiaTextRange::Create(_In_ IRawElementProviderSimple* const pProvider, + const Cursor& cursor) +{ + UiaTextRange* range = nullptr; + try + { + range = new UiaTextRange(pProvider, cursor); + } + catch (...) + { + range = nullptr; + } + + if (range) + { + pProvider->AddRef(); + } + return range; +} + +UiaTextRange* UiaTextRange::Create(_In_ IRawElementProviderSimple* const pProvider, + const Endpoint start, + const Endpoint end, + const bool degenerate) +{ + UiaTextRange* range = nullptr; + try + { + range = new UiaTextRange(pProvider, + start, + end, + degenerate); + } + catch (...) + { + range = nullptr; + } + + if (range) + { + pProvider->AddRef(); + } + return range; +} + +UiaTextRange* UiaTextRange::Create(_In_ IRawElementProviderSimple* const pProvider, + const UiaPoint point) +{ + UiaTextRange* range = nullptr; + try + { + range = new UiaTextRange(pProvider, point); + } + catch (...) + { + range = nullptr; + } + + if (range) + { + pProvider->AddRef(); + } + return range; +} + + +// degenerate range constructor. +UiaTextRange::UiaTextRange(_In_ IRawElementProviderSimple* const pProvider) : + _cRefs{ 1 }, + _pProvider{ THROW_HR_IF_NULL(E_INVALIDARG, pProvider) }, + _start{ 0 }, + _end{ 0 }, + _degenerate{ true } +{ + _id = id; + ++id; + + // tracing + ApiMsgConstructor apiMsg; + apiMsg.Id = _id; + Tracing::s_TraceUia(nullptr, ApiCall::Constructor, &apiMsg); +} + +UiaTextRange::UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const Cursor& cursor) : + UiaTextRange(pProvider) +{ + _degenerate = true; + _start = _screenInfoRowToEndpoint(cursor.GetPosition().Y) + cursor.GetPosition().X; + _end = _start; + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Constructor\n"); + _outputObjectState(); +#endif +} + +UiaTextRange::UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const Endpoint start, + const Endpoint end, + const bool degenerate) : + UiaTextRange(pProvider) +{ + THROW_HR_IF(E_INVALIDARG, !degenerate && start > end); + + _degenerate = degenerate; + _start = start; + _end = degenerate ? start : end; + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Constructor\n"); + _outputObjectState(); +#endif +} + +// returns a degenerate text range of the start of the row closest to the y value of point +UiaTextRange::UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const UiaPoint point) : + UiaTextRange(pProvider) +{ + POINT clientPoint; + clientPoint.x = static_cast(point.x); + clientPoint.y = static_cast(point.y); + // get row that point resides in + const IConsoleWindow* const pIConsoleWindow = _getIConsoleWindow(); + const Window* const pWindow = static_cast(pIConsoleWindow); + const RECT windowRect = pWindow->GetWindowRect(); + const SMALL_RECT viewport = _getViewport().ToInclusive(); + ScreenInfoRow row; + if (clientPoint.y <= windowRect.top) + { + row = viewport.Top; + } + else if (clientPoint.y >= windowRect.bottom) + { + row = viewport.Bottom; + } + else + { + // change point coords to pixels relative to window + HWND hwnd = _getWindowHandle(); + ScreenToClient(hwnd, &clientPoint); + + const SCREEN_INFORMATION& _pScreenInfo = _getScreenInfo(); + const COORD currentFontSize = _pScreenInfo.GetScreenFontSize(); + row = (clientPoint.y / currentFontSize.Y) + viewport.Top; + } + _start = _screenInfoRowToEndpoint(row); + _end = _start; + _degenerate = true; + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Constructor\n"); + _outputObjectState(); +#endif +} + +UiaTextRange::UiaTextRange(const UiaTextRange& a) : + _cRefs{ 1 }, + _pProvider{ a._pProvider }, + _start{ a._start }, + _end{ a._end }, + _degenerate{ a._degenerate } +{ + (static_cast(_pProvider))->AddRef(); + _id = id; + ++id; + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Copy Constructor\n"); + _outputObjectState(); +#endif +} + +UiaTextRange::~UiaTextRange() +{ + (static_cast(_pProvider))->Release(); +} + +const IdType UiaTextRange::GetId() const +{ + return _id; +} + +const Endpoint UiaTextRange::GetStart() const +{ + return _start; +} + +const Endpoint UiaTextRange::GetEnd() const +{ + return _end; +} + +// Routine Description: +// - returns true if the range is currently degenerate (empty range). +// Arguments: +// - +// Return Value: +// - true if range is degenerate, false otherwise. +const bool UiaTextRange::IsDegenerate() const +{ + return _degenerate; +} + +#pragma region IUnknown + +IFACEMETHODIMP_(ULONG) UiaTextRange::AddRef() +{ + Tracing::s_TraceUia(this, ApiCall::AddRef, nullptr); + return InterlockedIncrement(&_cRefs); +} + +IFACEMETHODIMP_(ULONG) UiaTextRange::Release() +{ + Tracing::s_TraceUia(this, ApiCall::Release, nullptr); + + const long val = InterlockedDecrement(&_cRefs); + if (val == 0) + { + delete this; + } + return val; +} + +IFACEMETHODIMP UiaTextRange::QueryInterface(_In_ REFIID riid, _COM_Outptr_result_maybenull_ void** ppInterface) +{ + Tracing::s_TraceUia(this, ApiCall::QueryInterface, nullptr); + + if (riid == __uuidof(IUnknown)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(ITextRangeProvider)) + { + *ppInterface = static_cast(this); + } + else + { + *ppInterface = nullptr; + return E_NOINTERFACE; + } + + (static_cast(*ppInterface))->AddRef(); + return S_OK; +} + +#pragma endregion + +#pragma region ITextRangeProvider + +IFACEMETHODIMP UiaTextRange::Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) +{ + try + { + *ppRetVal = new UiaTextRange(*this); + } + catch(...) + { + *ppRetVal = nullptr; + return wil::ResultFromCaughtException(); + } + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Clone\n"); + std::wstringstream ss; + ss << _id << L" cloned to " << (static_cast(*ppRetVal))->_id; + std::wstring str = ss.str(); + OutputDebugString(str.c_str()); + OutputDebugString(L"\n"); +#endif + // tracing + ApiMsgClone apiMsg; + apiMsg.CloneId = static_cast(*ppRetVal)->GetId(); + Tracing::s_TraceUia(this, ApiCall::Clone, &apiMsg); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + *pRetVal = FALSE; + UiaTextRange* other = static_cast(pRange); + if (other) + { + *pRetVal = !!(_start == other->GetStart() && + _end == other->GetEnd() && + _degenerate == other->IsDegenerate()); + } + // tracing + ApiMsgCompare apiMsg; + apiMsg.OtherId = other->GetId(); + apiMsg.Equal = !!*pRetVal; + Tracing::s_TraceUia(this, ApiCall::Compare, &apiMsg); + + return S_OK; +} + + +IFACEMETHODIMP UiaTextRange::CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint, + _In_ ITextRangeProvider* pTargetRange, + _In_ TextPatternRangeEndpoint targetEndpoint, + _Out_ int* pRetVal) +{ + // get the text range that we're comparing to + UiaTextRange* range = static_cast(pTargetRange); + if (range == nullptr) + { + return E_INVALIDARG; + } + + // get endpoint value that we're comparing to + Endpoint theirValue; + if (targetEndpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + theirValue = range->GetStart(); + } + else + { + theirValue = range->GetEnd() + 1; + } + + // get the values of our endpoint + Endpoint ourValue; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + ourValue = _start; + } + else + { + ourValue = _end + 1; + } + + // compare them + *pRetVal = std::clamp(static_cast(ourValue) - static_cast(theirValue), -1, 1); + + // tracing + ApiMsgCompareEndpoints apiMsg; + apiMsg.OtherId = range->GetId(); + apiMsg.Endpoint = endpoint; + apiMsg.TargetEndpoint = targetEndpoint; + apiMsg.Result = *pRetVal; + Tracing::s_TraceUia(this, ApiCall::CompareEndpoints, &apiMsg); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::ExpandToEnclosingUnit(_In_ TextUnit unit) +{ + ServiceLocator::LocateGlobals().getConsoleInformation().LockConsole(); + auto Unlock = wil::scope_exit([&] + { + ServiceLocator::LocateGlobals().getConsoleInformation().UnlockConsole(); + }); + + ApiMsgExpandToEnclosingUnit apiMsg; + apiMsg.Unit = unit; + apiMsg.OriginalStart = _start; + apiMsg.OriginalEnd = _end; + + try + { + const ScreenInfoRow topRow = _getFirstScreenInfoRowIndex(); + const ScreenInfoRow bottomRow = _getLastScreenInfoRowIndex(); + + if (unit == TextUnit::TextUnit_Character) + { + _end = _start; + } + else if (unit <= TextUnit::TextUnit_Line) + { + // expand to line + _start = _textBufferRowToEndpoint(_endpointToTextBufferRow(_start)); + _end = _start + _getLastColumnIndex(); + FAIL_FAST_IF(!(_start <= _end)); + } + else + { + // expand to document + _start = _screenInfoRowToEndpoint(topRow); + _end = _screenInfoRowToEndpoint(bottomRow) + _getLastColumnIndex(); + } + + _degenerate = false; + + Tracing::s_TraceUia(this, ApiCall::ExpandToEnclosingUnit, &apiMsg); + + return S_OK; + } + CATCH_RETURN(); +} + +// we don't support this currently +IFACEMETHODIMP UiaTextRange::FindAttribute(_In_ TEXTATTRIBUTEID /*textAttributeId*/, + _In_ VARIANT /*val*/, + _In_ BOOL /*searchBackward*/, + _Outptr_result_maybenull_ ITextRangeProvider** /*ppRetVal*/) +{ + Tracing::s_TraceUia(this, ApiCall::FindAttribute, nullptr); + return E_NOTIMPL; +} + +IFACEMETHODIMP UiaTextRange::FindText(_In_ BSTR text, + _In_ BOOL searchBackward, + _In_ BOOL ignoreCase, + _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::FindText, nullptr); + + *ppRetVal = nullptr; + try + { + const std::wstring wstr{ text, SysStringLen(text) }; + const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive; + + auto searchDirection = Search::Direction::Forward; + Endpoint searchAnchor = _start; + if (searchBackward) + { + searchDirection = Search::Direction::Backward; + searchAnchor = _end; + } + + Search searcher{ _getScreenInfo(), wstr, searchDirection, sensitivity, _endpointToCoord(searchAnchor) }; + + HRESULT hr = S_OK; + if (searcher.FindNext()) + { + const auto foundLocation = searcher.GetFoundLocation(); + const Endpoint start = _coordToEndpoint(foundLocation.first); + const Endpoint end = _coordToEndpoint(foundLocation.second); + // make sure what was found is within the bounds of the current range + if ((searchDirection == Search::Direction::Forward && end < _end) || + (searchDirection == Search::Direction::Backward && start > _start)) + { + hr = Clone(ppRetVal); + if (SUCCEEDED(hr)) + { + UiaTextRange& range = static_cast(**ppRetVal); + range._start = start; + range._end = end; + range._degenerate = false; + } + } + } + return hr; + } + CATCH_RETURN(); +} + +IFACEMETHODIMP UiaTextRange::GetAttributeValue(_In_ TEXTATTRIBUTEID textAttributeId, + _Out_ VARIANT* pRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetAttributeValue, nullptr); + if (textAttributeId == UIA_IsReadOnlyAttributeId) + { + pRetVal->vt = VT_BOOL; + pRetVal->boolVal = VARIANT_FALSE; + } + else + { + pRetVal->vt = VT_UNKNOWN; + UiaGetReservedNotSupportedValue(&pRetVal->punkVal); + } + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + *ppRetVal = nullptr; + + + try + { + // vector to put coords into. they go in as four doubles in the + // order: left, top, width, height. each line will have its own + // set of coords. + std::vector coords; + const TextBufferRow startRow = _endpointToTextBufferRow(_start); + + if (_degenerate && _isScreenInfoRowInViewport(startRow)) + { + _addScreenInfoRowBoundaries(_textBufferRowToScreenInfoRow(startRow), coords); + } + else + { + const unsigned int totalRowsInRange = _rowCountInRange(); + for (unsigned int i = 0; i < totalRowsInRange; ++i) + { + ScreenInfoRow screenInfoRow = _textBufferRowToScreenInfoRow(startRow + i); + if (!_isScreenInfoRowInViewport(screenInfoRow)) + { + continue; + } + _addScreenInfoRowBoundaries(screenInfoRow, coords); + } + } + + // convert to a safearray + *ppRetVal = SafeArrayCreateVector(VT_R8, 0, static_cast(coords.size())); + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + HRESULT hr; + for (LONG i = 0; i < static_cast(coords.size()); ++i) + { + hr = SafeArrayPutElement(*ppRetVal, &i, &coords[i]); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + } + } + CATCH_RETURN(); + + Tracing::s_TraceUia(this, ApiCall::GetBoundingRectangles, nullptr); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetBoundingRectangles, nullptr); + return _pProvider->QueryInterface(IID_PPV_ARGS(ppRetVal)); +} + +IFACEMETHODIMP UiaTextRange::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + std::wstring wstr = L""; + + if (maxLength < -1) + { + return E_INVALIDARG; + } + // the caller must pass in a value for the max length of the text + // to retrieve. a value of -1 means they don't want the text + // truncated. + const bool getPartialText = maxLength != -1; + + if (!_degenerate) + { + try + { + const ScreenInfoRow startScreenInfoRow = _endpointToScreenInfoRow(_start); + const Column startColumn = _endpointToColumn(_start); + const ScreenInfoRow endScreenInfoRow = _endpointToScreenInfoRow(_end); + const Column endColumn = _endpointToColumn(_end); + const unsigned int totalRowsInRange = _rowCountInRange(); + const TextBuffer& textBuffer = _getTextBuffer(); + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + std::wstringstream ss; + ss << L"---Initial span start=" << _start << L" and end=" << _end << L"\n"; + ss << L"----Retrieving sr:" << startScreenInfoRow << L" sc:" << startColumn << L" er:" << endScreenInfoRow << L" ec:" << endColumn << L"\n"; + OutputDebugString(ss.str().c_str()); +#endif + + ScreenInfoRow currentScreenInfoRow; + for (unsigned int i = 0; i < totalRowsInRange; ++i) + { + currentScreenInfoRow = startScreenInfoRow + i; + const ROW& row = textBuffer.GetRowByOffset(currentScreenInfoRow); + if (row.GetCharRow().ContainsText()) + { + const size_t rowRight = row.GetCharRow().MeasureRight(); + size_t startIndex = 0; + size_t endIndex = rowRight; + if (currentScreenInfoRow == startScreenInfoRow) + { + startIndex = startColumn; + } + if (currentScreenInfoRow == endScreenInfoRow) + { + // prevent the end from going past the last non-whitespace char in the row + endIndex = std::min(static_cast(endColumn + 1), rowRight); + } + + // if startIndex >= endIndex then _start is + // further to the right than the last + // non-whitespace char in the row so there + // wouldn't be any text to grab. + if (startIndex < endIndex) + { + wstr += row.GetText().substr(startIndex, endIndex - startIndex); + } + } + + if (currentScreenInfoRow != endScreenInfoRow) + { + wstr += L"\r\n"; + } + + if (getPartialText && wstr.size() > static_cast(maxLength)) + { + wstr.resize(maxLength); + break; + } + } + } + CATCH_RETURN(); + } + + *pRetVal = SysAllocString(wstr.c_str()); + + // tracing + ApiMsgGetText apiMsg; + apiMsg.Text = wstr.c_str(); + Tracing::s_TraceUia(this, ApiCall::GetText, &apiMsg); + +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + std::wstringstream ss; + ss << L"--------Retrieved Text Max Length(" << maxLength << L") [" << _id << L"]: " << wstr.c_str() << "\n"; + OutputDebugString(ss.str().c_str()); +#endif + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::Move(_In_ TextUnit unit, + _In_ int count, + _Out_ int* pRetVal) +{ + ServiceLocator::LocateGlobals().getConsoleInformation().LockConsole(); + auto Unlock = wil::scope_exit([&] + { + ServiceLocator::LocateGlobals().getConsoleInformation().UnlockConsole(); + }); + + *pRetVal = 0; + if (count == 0) + { + return S_OK; + } + + ApiMsgMove apiMsg; + apiMsg.OriginalStart = _start; + apiMsg.OriginalEnd = _end; + apiMsg.Unit = unit; + apiMsg.RequestedCount = count; +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"Move\n"); + _outputObjectState(); + + std::wstringstream ss; + ss << L" count: " << count; + std::wstring data = ss.str(); + OutputDebugString(data.c_str()); + OutputDebugString(L"\n"); + _outputRowConversions(); +#endif + + auto moveFunc = &_moveByDocument; + if (unit == TextUnit::TextUnit_Character) + { + moveFunc = &_moveByCharacter; + + } + else if (unit <= TextUnit::TextUnit_Line) + { + moveFunc = &_moveByLine; + } + + MovementDirection moveDirection = (count > 0) ? MovementDirection::Forward : MovementDirection::Backward; + std::pair newEndpoints; + + try + { + MoveState moveState{ *this, moveDirection }; + newEndpoints = moveFunc(count, + moveState, + pRetVal); + } + CATCH_RETURN(); + + _start = newEndpoints.first; + _end = newEndpoints.second; + + // a range can't be degenerate after both endpoints have been + // moved. + _degenerate = false; + + // tracing + apiMsg.MovedCount = *pRetVal; + Tracing::s_TraceUia(this, ApiCall::Move, &apiMsg); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, + _In_ TextUnit unit, + _In_ int count, + _Out_ int* pRetVal) +{ + ServiceLocator::LocateGlobals().getConsoleInformation().LockConsole(); + auto Unlock = wil::scope_exit([&] + { + ServiceLocator::LocateGlobals().getConsoleInformation().UnlockConsole(); + }); + + *pRetVal = 0; + if (count == 0) + { + return S_OK; + } + + ApiMsgMoveEndpointByUnit apiMsg; + apiMsg.OriginalStart = _start; + apiMsg.OriginalEnd = _end; + apiMsg.Endpoint = endpoint; + apiMsg.Unit = unit; + apiMsg.RequestedCount = count; +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"MoveEndpointByUnit\n"); + _outputObjectState(); + + std::wstringstream ss; + ss << L" endpoint: " << endpoint; + ss << L" count: " << count; + std::wstring data = ss.str(); + OutputDebugString(data.c_str()); + OutputDebugString(L"\n"); + _outputRowConversions(); +#endif + + MovementDirection moveDirection = (count > 0) ? MovementDirection::Forward : MovementDirection::Backward; + + auto moveFunc = &_moveEndpointByUnitDocument; + if (unit == TextUnit::TextUnit_Character) + { + moveFunc = &_moveEndpointByUnitCharacter; + } + else if (unit <= TextUnit::TextUnit_Line) + { + moveFunc = &_moveEndpointByUnitLine; + } + + std::tuple moveResults; + try + { + MoveState moveState{ *this, moveDirection }; + moveResults = moveFunc(count, endpoint, moveState, pRetVal); + } + CATCH_RETURN(); + + _start = std::get<0>(moveResults); + _end = std::get<1>(moveResults); + _degenerate = std::get<2>(moveResults); + + // tracing + apiMsg.MovedCount = *pRetVal; + Tracing::s_TraceUia(this, ApiCall::MoveEndpointByUnit, &apiMsg); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint, + _In_ ITextRangeProvider* pTargetRange, + _In_ TextPatternRangeEndpoint targetEndpoint) +{ + ServiceLocator::LocateGlobals().getConsoleInformation().LockConsole(); + auto Unlock = wil::scope_exit([&] + { + ServiceLocator::LocateGlobals().getConsoleInformation().UnlockConsole(); + }); + + UiaTextRange* range = static_cast(pTargetRange); + if (range == nullptr) + { + return E_INVALIDARG; + } + + ApiMsgMoveEndpointByRange apiMsg; + apiMsg.OriginalEnd = _start; + apiMsg.OriginalEnd = _end; + apiMsg.Endpoint = endpoint; + apiMsg.TargetEndpoint = targetEndpoint; + apiMsg.OtherId = range->GetId(); +#if defined(_DEBUG) && defined(UIATEXTRANGE_DEBUG_MSGS) + OutputDebugString(L"MoveEndpointByRange\n"); + _outputObjectState(); + + std::wstringstream ss; + ss << L" endpoint: " << endpoint; + ss << L" targetRange: " << range->_id; + ss << L" targetEndpoint: " << targetEndpoint; + std::wstring data = ss.str(); + OutputDebugString(data.c_str()); + OutputDebugString(L"\n"); + _outputRowConversions(); +#endif + + // get the value that we're updating to + Endpoint targetEndpointValue; + if (targetEndpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + targetEndpointValue = range->GetStart(); + + // If we're moving our end relative to their start, we actually have to back up one from + // their start position because this operation treats it as exclusive. + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End) + { + if (targetEndpointValue > 0) + { + targetEndpointValue--; + } + } + } + else + { + targetEndpointValue = range->GetEnd(); + + // If we're moving our start relative to their end, we actually have to sit one after + // their end position as it was stored inclusive and we're doing this as an exclusive operation. + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + targetEndpointValue++; + } + } + + // convert then endpoints to screen info rows/columns + ScreenInfoRow startScreenInfoRow; + Column startColumn; + ScreenInfoRow endScreenInfoRow; + Column endColumn; + ScreenInfoRow targetScreenInfoRow; + Column targetColumn; + try + { + startScreenInfoRow = _endpointToScreenInfoRow(_start); + startColumn = _endpointToColumn(_start); + endScreenInfoRow = _endpointToScreenInfoRow(_end); + endColumn = _endpointToColumn(_end); + targetScreenInfoRow = _endpointToScreenInfoRow(targetEndpointValue); + targetColumn = _endpointToColumn(targetEndpointValue); + } + CATCH_RETURN(); + + // set endpoint value and check for crossed endpoints + bool crossedEndpoints = false; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + _start = targetEndpointValue; + if (_compareScreenCoords(endScreenInfoRow, endColumn, targetScreenInfoRow, targetColumn) == -1) + { + // endpoints were crossed + _end = _start; + crossedEndpoints = true; + } + } + else + { + _end = targetEndpointValue; + if (_compareScreenCoords(startScreenInfoRow, startColumn, targetScreenInfoRow, targetColumn) == 1) + { + // endpoints were crossed + _start = _end; + crossedEndpoints = true; + } + } + _degenerate = crossedEndpoints; + + Tracing::s_TraceUia(this, ApiCall::MoveEndpointByRange, &apiMsg); + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::Select() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + + if (_degenerate) + { + // calling Select on a degenerate range should clear any current selections + Selection::Instance().ClearSelection(); + } + else + { + COORD coordStart; + COORD coordEnd; + + coordStart.X = static_cast(_endpointToColumn(_start)); + coordStart.Y = static_cast(_endpointToScreenInfoRow(_start)); + + coordEnd.X = static_cast(_endpointToColumn(_end)); + coordEnd.Y = static_cast(_endpointToScreenInfoRow(_end)); + + Selection::Instance().SelectNewRegion(coordStart, coordEnd); + } + + Tracing::s_TraceUia(this, ApiCall::Select, nullptr); + return S_OK; +} + +// we don't support this +IFACEMETHODIMP UiaTextRange::AddToSelection() +{ + Tracing::s_TraceUia(this, ApiCall::AddToSelection, nullptr); + return E_NOTIMPL; +} + +// we don't support this +IFACEMETHODIMP UiaTextRange::RemoveFromSelection() +{ + Tracing::s_TraceUia(this, ApiCall::RemoveFromSelection, nullptr); + return E_NOTIMPL; +} + +IFACEMETHODIMP UiaTextRange::ScrollIntoView(_In_ BOOL alignToTop) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + SMALL_RECT oldViewport; + unsigned int viewportHeight; + // range rows + ScreenInfoRow startScreenInfoRow; + ScreenInfoRow endScreenInfoRow; + // screen buffer rows + ScreenInfoRow topRow; + ScreenInfoRow bottomRow; + try + { + oldViewport = _getViewport().ToInclusive(); + viewportHeight = _getViewportHeight(oldViewport); + // range rows + startScreenInfoRow = _endpointToScreenInfoRow(_start); + endScreenInfoRow = _endpointToScreenInfoRow(_end); + // screen buffer rows + topRow = _getFirstScreenInfoRowIndex(); + bottomRow = _getLastScreenInfoRowIndex(); + } + CATCH_RETURN(); + + SMALL_RECT newViewport = oldViewport; + + // there's a bunch of +1/-1s here for setting the viewport. These + // are to account for the inclusivity of the viewport boundaries. + if (alignToTop) + { + // determine if we can align the start row to the top + if (startScreenInfoRow + viewportHeight <= bottomRow) + { + // we can align to the top + newViewport.Top = static_cast(startScreenInfoRow); + newViewport.Bottom = static_cast(startScreenInfoRow + viewportHeight - 1); + } + else + { + // we can align to the top so we'll just move the viewport + // to the bottom of the screen buffer + newViewport.Bottom = static_cast(bottomRow); + newViewport.Top = static_cast(bottomRow - viewportHeight + 1); + } + } + else + { + // we need to align to the bottom + // check if we can align to the bottom + if (endScreenInfoRow >= viewportHeight) + { + // we can align to bottom + newViewport.Bottom = static_cast(endScreenInfoRow); + newViewport.Top = static_cast(endScreenInfoRow - viewportHeight + 1); + } + else + { + // we can't align to bottom so we'll move the viewport to + // the top of the screen buffer + newViewport.Top = static_cast(topRow); + newViewport.Bottom = static_cast(topRow + viewportHeight - 1); + } + + } + + FAIL_FAST_IF(!(newViewport.Top >= static_cast(topRow))); + FAIL_FAST_IF(!(newViewport.Bottom <= static_cast(bottomRow))); + FAIL_FAST_IF(!(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport))); + + try + { + IConsoleWindow* pIConsoleWindow = _getIConsoleWindow(); + pIConsoleWindow->ChangeViewport(newViewport); + } + CATCH_RETURN(); + + + // tracing + ApiMsgScrollIntoView apiMsg; + apiMsg.AlignToTop = !!alignToTop; + Tracing::s_TraceUia(this, ApiCall::ScrollIntoView, &apiMsg); + + return S_OK; +} + +IFACEMETHODIMP UiaTextRange::GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetChildren, nullptr); + // we don't have any children + *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 0); + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + return S_OK; +} + +#pragma endregion + +// Routine Description: +// - Gets the current viewport +// Arguments: +// - +// Return Value: +// - The screen info's current viewport +const Microsoft::Console::Types::Viewport& UiaTextRange::_getViewport() +{ + return _getScreenInfo().GetViewport(); +} + +// Routine Description: +// - Gets the current window +// Arguments: +// - +// Return Value: +// - The current window. May return nullptr if there is no current +// window. +IConsoleWindow* const UiaTextRange::_getIConsoleWindow() +{ + IConsoleWindow* const pIConsoleWindow = ServiceLocator::LocateConsoleWindow(); + THROW_HR_IF_NULL(E_POINTER, pIConsoleWindow); + return pIConsoleWindow; +} + +// Routine Description: +// - gets the current window handle +// Arguments: +// - +// Return Value +// - the current window handle +HWND UiaTextRange::_getWindowHandle() +{ + return _getIConsoleWindow()->GetWindowHandle(); +} + +// Routine Description: +// - gets the current screen info +// Arguments: +// - +// Return Value +// - the current screen info. May return nullptr. +SCREEN_INFORMATION& UiaTextRange::_getScreenInfo() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + THROW_HR_IF(E_POINTER, !gci.HasActiveOutputBuffer()); + return gci.GetActiveOutputBuffer().GetActiveBuffer(); +} + +// Routine Description: +// - gets the current output text buffer +// Arguments: +// - +// Return Value +// - the current output text buffer. May return nullptr. +TextBuffer& UiaTextRange::_getTextBuffer() +{ + return _getScreenInfo().GetTextBuffer(); +} + +// Routine Description: +// - Gets the number of rows in the output text buffer. +// Arguments: +// - +// Return Value: +// - The number of rows +const unsigned int UiaTextRange::_getTotalRows() +{ + return _getTextBuffer().TotalRowCount(); +} + +// Routine Description: +// - gets the current screen buffer size. +// Arguments: +// - +// Return Value: +// - The screen buffer size +const COORD UiaTextRange::_getScreenBufferCoords() +{ + return _getScreenInfo().GetBufferSize().Dimensions(); +} + + +// Routine Description: +// - Gets the width of the screen buffer rows +// Arguments: +// - +// Return Value: +// - The row width +const unsigned int UiaTextRange::_getRowWidth() +{ + // make sure that we can't leak a 0 + return std::max(static_cast(_getScreenBufferCoords().X), 1u); +} + +// Routine Description: +// - calculates the column refered to by the endpoint. +// Arguments: +// - endpoint - the endpoint to translate +// Return Value: +// - the column value +const Column UiaTextRange::_endpointToColumn(const Endpoint endpoint) +{ + return endpoint % _getRowWidth(); +} + +// Routine Description: +// - converts an Endpoint into its equivalent text buffer row. +// Arguments: +// - endpoint - the endpoint to convert +// Return Value: +// - the text buffer row value +const TextBufferRow UiaTextRange::_endpointToTextBufferRow(const Endpoint endpoint) +{ + return endpoint / _getRowWidth(); +} + +// Routine Description: +// - counts the number of rows that are fully or partially part of the +// range. +// Arguments: +// - +// Return Value: +// - The number of rows in the range. +const unsigned int UiaTextRange::_rowCountInRange() const +{ + if (_degenerate) + { + return 0; + } + + const ScreenInfoRow startScreenInfoRow = _endpointToScreenInfoRow(_start); + const Column startColumn = _endpointToColumn(_start); + const ScreenInfoRow endScreenInfoRow = _endpointToScreenInfoRow(_end); + const Column endColumn = _endpointToColumn(_end); + + FAIL_FAST_IF(!(_compareScreenCoords(startScreenInfoRow, startColumn, endScreenInfoRow, endColumn) <= 0)); + + // + 1 to balance subtracting ScreenInfoRows from each other + return endScreenInfoRow - startScreenInfoRow + 1; +} + +// Routine Description: +// - Converts a TextBufferRow to a ScreenInfoRow. +// Arguments: +// - row - the TextBufferRow to convert +// Return Value: +// - the equivalent ScreenInfoRow. +const ScreenInfoRow UiaTextRange::_textBufferRowToScreenInfoRow(const TextBufferRow row) +{ + const int firstRowIndex = _getTextBuffer().GetFirstRowIndex(); + return _normalizeRow(row - firstRowIndex); +} + +// Routine Description: +// - Converts a ScreenInfoRow to a ViewportRow. Uses the default +// viewport for the conversion. +// Arguments: +// - row - the ScreenInfoRow to convert +// Return Value: +// - the equivalent ViewportRow. +const ViewportRow UiaTextRange::_screenInfoRowToViewportRow(const ScreenInfoRow row) +{ + const SMALL_RECT viewport = _getViewport().ToInclusive(); + return _screenInfoRowToViewportRow(row, viewport); +} + +// Routine Description: +// - Converts a ScreenInfoRow to a ViewportRow. +// Arguments: +// - row - the ScreenInfoRow to convert +// - viewport - the viewport to use for the conversion +// Return Value: +// - the equivalent ViewportRow. +const ViewportRow UiaTextRange::_screenInfoRowToViewportRow(const ScreenInfoRow row, + const SMALL_RECT viewport) +{ + return row - viewport.Top; +} + +// Routine Description: +// - normalizes the row index to within the bounds of the output +// buffer. The output buffer stores the text in a circular buffer so +// this method makes sure that we circle around gracefully. +// Arguments: +// - the non-normalized row index +// Return Value: +// - the normalized row index +const Row UiaTextRange::_normalizeRow(const Row row) +{ + const unsigned int totalRows = _getTotalRows(); + return ((row + totalRows) % totalRows); +} + +// Routine Description: +// - Gets the viewport height, measured in char rows. +// Arguments: +// - viewport - The viewport to measure +// Return Value: +// - The viewport height +const unsigned int UiaTextRange::_getViewportHeight(const SMALL_RECT viewport) +{ + FAIL_FAST_IF(!(viewport.Bottom >= viewport.Top)); + // + 1 because COORD is inclusive on both sides so subtracting top + // and bottom gets rid of 1 more then it should. + return viewport.Bottom - viewport.Top + 1; +} + +// Routine Description: +// - Gets the viewport width, measured in char columns. +// Arguments: +// - viewport - The viewport to measure +// Return Value: +// - The viewport width +const unsigned int UiaTextRange::_getViewportWidth(const SMALL_RECT viewport) +{ + FAIL_FAST_IF(!(viewport.Right >= viewport.Left)); + + // + 1 because COORD is inclusive on both sides so subtracting left + // and right gets rid of 1 more then it should. + return (viewport.Right - viewport.Left + 1); +} + +// Routine Description: +// - checks if the row is currently visible in the viewport. Uses the +// default viewport. +// Arguments: +// - row - the screen info row to check +// Return Value: +// - true if the row is within the bounds of the viewport +const bool UiaTextRange::_isScreenInfoRowInViewport(const ScreenInfoRow row) +{ + return _isScreenInfoRowInViewport(row, _getViewport().ToInclusive()); +} + +// Routine Description: +// - checks if the row is currently visible in the viewport +// Arguments: +// - row - the row to check +// - viewport - the viewport to use for the bounds +// Return Value: +// - true if the row is within the bounds of the viewport +const bool UiaTextRange::_isScreenInfoRowInViewport(const ScreenInfoRow row, + const SMALL_RECT viewport) +{ + ViewportRow viewportRow = _screenInfoRowToViewportRow(row, viewport); + return viewportRow >= 0 && + viewportRow < static_cast(_getViewportHeight(viewport)); +} + +// Routine Description: +// - Converts a ScreenInfoRow to a TextBufferRow. +// Arguments: +// - row - the ScreenInfoRow to convert +// Return Value: +// - the equivalent TextBufferRow. +const TextBufferRow UiaTextRange::_screenInfoRowToTextBufferRow(const ScreenInfoRow row) +{ + const TextBufferRow firstRowIndex = _getTextBuffer().GetFirstRowIndex(); + return _normalizeRow(row + firstRowIndex); +} + +// Routine Description: +// - Converts a TextBufferRow to an Endpoint. +// Arguments: +// - row - the TextBufferRow to convert +// Return Value: +// - the equivalent Endpoint, starting at the beginning of the TextBufferRow. +const Endpoint UiaTextRange::_textBufferRowToEndpoint(const TextBufferRow row) +{ + return _getRowWidth() * row; +} + +// Routine Description: +// - Converts a ScreenInfoRow to an Endpoint. +// Arguments: +// - row - the ScreenInfoRow to convert +// Return Value: +// - the equivalent Endpoint. +const Endpoint UiaTextRange::_screenInfoRowToEndpoint(const ScreenInfoRow row) +{ + return _textBufferRowToEndpoint(_screenInfoRowToTextBufferRow(row)); +} + +// Routine Description: +// - Converts an Endpoint to an ScreenInfoRow. +// Arguments: +// - endpoint - the endpoint to convert +// Return Value: +// - the equivalent ScreenInfoRow. +const ScreenInfoRow UiaTextRange::_endpointToScreenInfoRow(const Endpoint endpoint) +{ + return _textBufferRowToScreenInfoRow(_endpointToTextBufferRow(endpoint)); +} + +// Routine Description: +// - adds the relevant coordinate points from screenInfoRow to coords. +// Arguments: +// - screenInfoRow - row to calculate coordinate positions from +// - coords - vector to add the calucated coords to +// Return Value: +// - +// Notes: +// - alters coords. may throw an exception. +void UiaTextRange::_addScreenInfoRowBoundaries(const ScreenInfoRow screenInfoRow, + _Inout_ std::vector& coords) const +{ + const SCREEN_INFORMATION& screenInfo = _getScreenInfo(); + const COORD currentFontSize = screenInfo.GetScreenFontSize(); + + POINT topLeft; + POINT bottomRight; + + if (_endpointToScreenInfoRow(_start) == screenInfoRow) + { + // start is somewhere in this row so we start from its position + topLeft.x = _endpointToColumn(_start) * currentFontSize.X; + } + else + { + // otherwise we start from the beginning of the row + topLeft.x = 0; + } + + topLeft.y = _screenInfoRowToViewportRow(screenInfoRow) * currentFontSize.Y; + + if (_endpointToScreenInfoRow(_end) == screenInfoRow) + { + // the endpoints are on the same row + bottomRight.x = (_endpointToColumn(_end) + 1) * currentFontSize.X; + } + else + { + // _end is not on this row so span to the end of the row + bottomRight.x = _getViewportWidth(_getViewport().ToInclusive()) * currentFontSize.X; + } + + // we add the font height only once here because we are adding each line individually + bottomRight.y = topLeft.y + currentFontSize.Y; + + // convert the coords to be relative to the screen instead of + // the client window + HWND hwnd = _getWindowHandle(); + ClientToScreen(hwnd, &topLeft); + ClientToScreen(hwnd, &bottomRight); + + const LONG width = bottomRight.x - topLeft.x; + const LONG height = bottomRight.y - topLeft.y; + + // insert the coords + coords.push_back(topLeft.x); + coords.push_back(topLeft.y); + coords.push_back(width); + coords.push_back(height); +} + +// Routine Description: +// - returns the index of the first row of the screen info +// Arguments: +// - +// Return Value: +// - the index of the first row (0-indexed) of the screen info +const unsigned int UiaTextRange::_getFirstScreenInfoRowIndex() +{ + return 0; +} + +// Routine Description: +// - returns the index of the last row of the screen info +// Arguments: +// - +// Return Value: +// - the index of the last row (0-indexed) of the screen info +const unsigned int UiaTextRange::_getLastScreenInfoRowIndex() +{ + return _getTotalRows() - 1; +} + + +// Routine Description: +// - returns the index of the first column of the screen info rows +// Arguments: +// - +// Return Value: +// - the index of the first column (0-indexed) of the screen info rows +const Column UiaTextRange::_getFirstColumnIndex() +{ + return 0; +} + +// Routine Description: +// - returns the index of the last column of the screen info rows +// Arguments: +// - +// Return Value: +// - the index of the last column (0-indexed) of the screen info rows +const Column UiaTextRange::_getLastColumnIndex() +{ + return _getRowWidth() - 1; +} + +// Routine Description: +// - Compares two sets of screen info coordinates +// Arguments: +// - rowA - the row index of the first position +// - colA - the column index of the first position +// - rowB - the row index of the second position +// - colB - the column index of the second position +// Return Value: +// -1 if A < B +// 1 if A > B +// 0 if A == B +const int UiaTextRange::_compareScreenCoords(const ScreenInfoRow rowA, + const Column colA, + const ScreenInfoRow rowB, + const Column colB) +{ + FAIL_FAST_IF(!(rowA >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(rowA <= _getLastScreenInfoRowIndex())); + + FAIL_FAST_IF(!(colA >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(colA <= _getLastColumnIndex())); + + FAIL_FAST_IF(!(rowB >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(rowB <= _getLastScreenInfoRowIndex())); + + FAIL_FAST_IF(!(colB >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(colB <= _getLastColumnIndex())); + + if (rowA < rowB) + { + return -1; + } + else if (rowA > rowB) + { + return 1; + } + // rowA == rowB + else if (colA < colB) + { + return -1; + } + else if (colA > colB) + { + return 1; + } + // colA == colB + return 0; +} + +// Routine Description: +// - calculates new Endpoints if they were to be moved moveCount times +// by character. +// Arguments: +// - moveCount - the number of times to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - a pair of endpoints of the form +std::pair UiaTextRange::_moveByCharacter(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + if (moveState.Direction == MovementDirection::Forward) + { + return _moveByCharacterForward(moveCount, moveState, pAmountMoved); + } + else + { + return _moveByCharacterBackward(moveCount, moveState, pAmountMoved); + } +} + +std::pair UiaTextRange::_moveByCharacterForward(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + int count = moveCount; + ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; + Column currentColumn = moveState.StartColumn; + + for (int i = 0; i < abs(count); ++i) + { + // get the current row's right + const ROW& row = _getTextBuffer().GetRowByOffset(currentScreenInfoRow); + const size_t right = row.GetCharRow().MeasureRight(); + + // check if we're at the edge of the screen info buffer + if (currentScreenInfoRow == moveState.LimitingRow && + currentColumn + 1>= right) + { + break; + } + else if (currentColumn + 1 >= right) + { + // we're at the edge of a row and need to go to the next one + currentColumn = moveState.FirstColumnInRow; + currentScreenInfoRow += static_cast(moveState.Increment); + } + else + { + // moving somewhere away from the edges of a row + currentColumn += static_cast(moveState.Increment); + } + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + + Endpoint start = _screenInfoRowToEndpoint(currentScreenInfoRow) + currentColumn; + Endpoint end = start; + return std::make_pair(std::move(start), std::move(end)); +} + +std::pair UiaTextRange::_moveByCharacterBackward(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + int count = moveCount; + ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; + Column currentColumn = moveState.StartColumn; + + for (int i = 0; i < abs(count); ++i) + { + // check if we're at the edge of the screen info buffer + if (currentScreenInfoRow == moveState.LimitingRow && + currentColumn == moveState.LastColumnInRow) + { + break; + } + else if (currentColumn == moveState.LastColumnInRow) + { + // we're at the edge of a row and need to go to the + // next one. move to the cell with the last non-whitespace charactor + + currentScreenInfoRow += static_cast(moveState.Increment); + // get the right cell for the next row + const ROW& row = _getTextBuffer().GetRowByOffset(currentScreenInfoRow); + const size_t right = row.GetCharRow().MeasureRight(); + currentColumn = static_cast((right == 0) ? 0 : right - 1); + } + else + { + // moving somewhere away from the edges of a row + currentColumn += static_cast(moveState.Increment); + } + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + + Endpoint start = _screenInfoRowToEndpoint(currentScreenInfoRow) + currentColumn; + Endpoint end = start; + return std::make_pair(std::move(start), std::move(end)); +} + +// Routine Description: +// - calculates new Endpoints if they were to be moved moveCount times +// by line. +// Arguments: +// - moveCount - the number of times to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - a pair of endpoints of the form +std::pair UiaTextRange::_moveByLine(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + Endpoint start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + Endpoint end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + ScreenInfoRow currentScreenInfoRow = moveState.StartScreenInfoRow; + // we don't want to move the range if we're already in the + // limiting row and trying to move off the end of the screen buffer + const bool illegalMovement = (currentScreenInfoRow == moveState.LimitingRow && + ((moveCount < 0 && moveState.Increment == MovementIncrement::Backward) || + (moveCount > 0 && moveState.Increment == MovementIncrement::Forward))); + + if (moveCount != 0 && !illegalMovement) + { + // move the range + for (int i = 0; i < abs(moveCount); ++i) + { + if (currentScreenInfoRow == moveState.LimitingRow) + { + break; + } + currentScreenInfoRow += static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + start = _screenInfoRowToEndpoint(currentScreenInfoRow); + end = start + _getLastColumnIndex(); + } + + return std::make_pair(std::move(start), std::move(end)); +} + +// Routine Description: +// - calculates new Endpoints if they were to be moved moveCount times +// by document. +// Arguments: +// - moveCount - the number of times to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - a pair of endpoints of the form +std::pair UiaTextRange::_moveByDocument(const int /*moveCount*/, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + // We can't move by anything larger than a line, so move by document will apply and will + // just report that it can't do that. + *pAmountMoved = 0; + + // We then have to return the same endpoints as what we initially had so nothing happens. + Endpoint start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + Endpoint end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + + return std::make_pair(std::move(start), std::move(end)); +} + +// Routine Description: +// - calculates new Endpoints/degenerate state if the indicated +// endpoint was moved moveCount times by character. +// Arguments: +// - moveCount - the number of times to move +// - endpoint - the endpoint to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - A tuple of elements of the form +std::tuple UiaTextRange::_moveEndpointByUnitCharacter(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + if (moveState.Direction == MovementDirection::Forward) + { + return _moveEndpointByUnitCharacterForward(moveCount, endpoint, moveState, pAmountMoved); + } + else + { + return _moveEndpointByUnitCharacterBackward(moveCount, endpoint, moveState, pAmountMoved); + } +} + +std::tuple +UiaTextRange::_moveEndpointByUnitCharacterForward(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + int count = moveCount; + ScreenInfoRow currentScreenInfoRow; + Column currentColumn; + + // set current location vars + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + currentScreenInfoRow = moveState.StartScreenInfoRow; + currentColumn = moveState.StartColumn; + } + else + { + currentScreenInfoRow = moveState.EndScreenInfoRow; + currentColumn = moveState.EndColumn; + } + + for (int i = 0; i < abs(count); ++i) + { + // get the current row's right + const ROW& row = _getTextBuffer().GetRowByOffset(currentScreenInfoRow); + const size_t right = row.GetCharRow().MeasureRight(); + + // check if we're at the edge of the screen info buffer + if (currentScreenInfoRow == moveState.LimitingRow && + currentColumn + 1 >= right) + { + break; + } + else if (currentColumn + 1 >= right) + { + // we're at the edge of a row and need to go to the next one + currentColumn = moveState.FirstColumnInRow; + currentScreenInfoRow += static_cast(moveState.Increment); + } + else + { + // moving somewhere away from the edges of a row + currentColumn += static_cast(moveState.Increment); + } + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + + // translate the row back to an endpoint and handle any crossed endpoints + Endpoint convertedEndpoint = _screenInfoRowToEndpoint(currentScreenInfoRow) + currentColumn; + Endpoint start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + Endpoint end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + bool degenerate = false; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + start = convertedEndpoint; + if (_compareScreenCoords(currentScreenInfoRow, + currentColumn, + moveState.EndScreenInfoRow, + moveState.EndColumn) == 1) + { + end = start; + degenerate = true; + } + } + else + { + end = convertedEndpoint; + if (_compareScreenCoords(currentScreenInfoRow, + currentColumn, + moveState.StartScreenInfoRow, + moveState.StartColumn) == -1) + { + start = end; + degenerate = true; + } + } + return std::make_tuple(start, end, degenerate); +} + +std::tuple +UiaTextRange::_moveEndpointByUnitCharacterBackward(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + int count = moveCount; + ScreenInfoRow currentScreenInfoRow; + Column currentColumn; + + // set current location vars + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + currentScreenInfoRow = moveState.StartScreenInfoRow; + currentColumn = moveState.StartColumn; + } + else + { + currentScreenInfoRow = moveState.EndScreenInfoRow; + currentColumn = moveState.EndColumn; + } + + for (int i = 0; i < abs(count); ++i) + { + // check if we're at the edge of the screen info buffer + if (currentScreenInfoRow == moveState.LimitingRow && + currentColumn == moveState.LastColumnInRow) + { + break; + } + else if (currentColumn == moveState.LastColumnInRow) + { + // we're at the edge of a row and need to go to the + // next one. move to the cell with the last non-whitespace charactor + + currentScreenInfoRow += static_cast(moveState.Increment); + // get the right cell for the next row + const ROW& row = _getTextBuffer().GetRowByOffset(currentScreenInfoRow); + const size_t right = row.GetCharRow().MeasureRight(); + currentColumn = static_cast((right == 0) ? 0 : right - 1); + } + else + { + // moving somewhere away from the edges of a row + currentColumn += static_cast(moveState.Increment); + } + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + + // translate the row back to an endpoint and handle any crossed endpoints + Endpoint convertedEndpoint = _screenInfoRowToEndpoint(currentScreenInfoRow) + currentColumn; + Endpoint start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + Endpoint end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + bool degenerate = false; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + start = convertedEndpoint; + if (_compareScreenCoords(currentScreenInfoRow, + currentColumn, + moveState.EndScreenInfoRow, + moveState.EndColumn) == 1) + { + end = start; + degenerate = true; + } + } + else + { + end = convertedEndpoint; + if (_compareScreenCoords(currentScreenInfoRow, + currentColumn, + moveState.StartScreenInfoRow, + moveState.StartColumn) == -1) + { + start = end; + degenerate = true; + } + } + return std::make_tuple(start, end, degenerate); +} + +// Routine Description: +// - calculates new Endpoints/degenerate state if the indicated +// endpoint was moved moveCount times by line. +// Arguments: +// - moveCount - the number of times to move +// - endpoint - the endpoint to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - A tuple of elements of the form +std::tuple UiaTextRange::_moveEndpointByUnitLine(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + int count = moveCount; + ScreenInfoRow currentScreenInfoRow; + Column currentColumn; + bool forceDegenerate = false; + Endpoint start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + Endpoint end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + bool degenerate = false; + + if (moveCount == 0) + { + return std::make_tuple(start, end, degenerate); + } + + MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; + + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + currentScreenInfoRow = moveState.StartScreenInfoRow; + currentColumn = moveState.StartColumn; + } + else + { + currentScreenInfoRow = moveState.EndScreenInfoRow; + currentColumn = moveState.EndColumn; + } + + // check if we can't be moved anymore + if (currentScreenInfoRow == moveState.LimitingRow && + currentColumn == moveState.LastColumnInRow) + { + return std::make_tuple(start, end, degenerate); + } + else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start && + moveDirection == MovementDirection::Forward) + { + if (moveState.StartScreenInfoRow == moveState.LimitingRow) + { + // _start is somewhere on the limiting row but not at + // the very end. move to the end of the last row + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentColumn = _getLastColumnIndex(); + forceDegenerate = true; + } + if (moveState.StartColumn != _getFirstColumnIndex()) + { + // _start is somewhere in the middle of a row, so do a + // partial movement to the beginning of the next row + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentScreenInfoRow += static_cast(moveState.Increment); + currentColumn = _getFirstColumnIndex(); + } + } + else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start && + moveDirection == MovementDirection::Backward) + { + if (moveState.StartColumn != _getFirstColumnIndex()) + { + // moving backwards when we weren't already at the beginning of + // the row so move there first to align with the text unit boundary + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentColumn = _getFirstColumnIndex(); + } + } + else if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_End && + moveDirection == MovementDirection::Forward) + { + if (moveState.EndColumn != _getLastColumnIndex()) + { + // _end is not at the last column in a row, so we move + // forward to it with a partial movement + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentColumn = _getLastColumnIndex(); + } + } + else + { + // _end moving backwards + if (moveState.EndScreenInfoRow == moveState.LimitingRow) + { + // _end is somewhere on the limiting row but not at the + // front. move it there + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentColumn = _getFirstColumnIndex(); + forceDegenerate = true; + } + else if (moveState.EndColumn != _getLastColumnIndex()) + { + // _end is not at the last column in a row, so we move it + // backwards to it with a partial move + count -= static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + currentColumn = _getLastColumnIndex(); + currentScreenInfoRow += static_cast(moveState.Increment); + } + } + + FAIL_FAST_IF(!(currentColumn >= _getFirstColumnIndex())); + FAIL_FAST_IF(!(currentColumn <= _getLastColumnIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + + // move the row that the endpoint corresponds to + while (count != 0 && currentScreenInfoRow != moveState.LimitingRow) + { + count -= static_cast(moveState.Increment); + currentScreenInfoRow += static_cast(moveState.Increment); + *pAmountMoved += static_cast(moveState.Increment); + + FAIL_FAST_IF(!(currentScreenInfoRow >= _getFirstScreenInfoRowIndex())); + FAIL_FAST_IF(!(currentScreenInfoRow <= _getLastScreenInfoRowIndex())); + } + + // translate the row back to an endpoint and handle any crossed endpoints + Endpoint convertedEndpoint = _screenInfoRowToEndpoint(currentScreenInfoRow) + currentColumn; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + start = convertedEndpoint; + if (currentScreenInfoRow > moveState.EndScreenInfoRow || forceDegenerate) + { + degenerate = true; + end = start; + } + } + else + { + end = convertedEndpoint; + if (currentScreenInfoRow < moveState.StartScreenInfoRow || forceDegenerate) + { + degenerate = true; + start = end; + } + } + + return std::make_tuple(start, end, degenerate); +} + +// Routine Description: +// - calculates new Endpoints/degenerate state if the indicate +// endpoint was moved moveCount times by document. +// Arguments: +// - moveCount - the number of times to move +// - endpoint - the endpoint to move +// - moveState - values indicating the state of the console for the +// move operation +// - pAmountMoved - the number of times that the return values are "moved" +// Return Value: +// - A tuple of elements of the form +std::tuple UiaTextRange::_moveEndpointByUnitDocument(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved) +{ + *pAmountMoved = 0; + + Endpoint start; + Endpoint end; + bool degenerate = false; + if (endpoint == TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start) + { + if (moveCount < 0) + { + // moving _start backwards + start = _screenInfoRowToEndpoint(_getFirstScreenInfoRowIndex()) + _getFirstColumnIndex(); + end = _screenInfoRowToEndpoint(moveState.EndScreenInfoRow) + moveState.EndColumn; + if (!(moveState.StartScreenInfoRow == _getFirstScreenInfoRowIndex() && + moveState.StartColumn == _getFirstColumnIndex())) + { + *pAmountMoved += static_cast(moveState.Increment); + } + } + else + { + // moving _start forwards + start = _screenInfoRowToEndpoint(_getLastScreenInfoRowIndex()) + _getLastColumnIndex(); + end = start; + degenerate = true; + if (!(moveState.StartScreenInfoRow == _getLastScreenInfoRowIndex() && + moveState.StartColumn == _getLastColumnIndex())) + { + *pAmountMoved += static_cast(moveState.Increment); + } + } + } + else + { + if (moveCount < 0) + { + // moving _end backwards + end = _screenInfoRowToEndpoint(_getFirstScreenInfoRowIndex()) + _getFirstColumnIndex(); + start = end; + degenerate = true; + if (!(moveState.EndScreenInfoRow == _getFirstScreenInfoRowIndex() && + moveState.EndColumn == _getFirstColumnIndex())) + { + *pAmountMoved += static_cast(moveState.Increment); + } + } + else + { + // moving _end forwards + end = _screenInfoRowToEndpoint(_getLastScreenInfoRowIndex()) + _getLastColumnIndex(); + start = _screenInfoRowToEndpoint(moveState.StartScreenInfoRow) + moveState.StartColumn; + if (!(moveState.EndScreenInfoRow == _getLastScreenInfoRowIndex() && + moveState.EndColumn == _getLastColumnIndex())) + { + *pAmountMoved += static_cast(moveState.Increment); + } + } + } + + return std::make_tuple(start, end, degenerate); +} + +COORD UiaTextRange::_endpointToCoord(const Endpoint endpoint) +{ + return { gsl::narrow(_endpointToColumn(endpoint)), gsl::narrow(_endpointToScreenInfoRow(endpoint)) }; +} + +Endpoint UiaTextRange::_coordToEndpoint(const COORD coord) +{ + return _screenInfoRowToEndpoint(coord.Y) + coord.X; +} diff --git a/src/interactivity/win32/UiaTextRange.hpp b/src/interactivity/win32/UiaTextRange.hpp new file mode 100644 index 000000000..8af676a02 --- /dev/null +++ b/src/interactivity/win32/UiaTextRange.hpp @@ -0,0 +1,478 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- UiaTextRange.hpp + +Abstract: +- This module provides UI Automation access to the text of the console + window to support both automation tests and accessibility (screen + reading) applications. + +Author(s): +- Austin Diviness (AustDi) 2017 +--*/ + +#pragma once + +#include "precomp.h" + +#include "../inc/IConsoleWindow.hpp" +#include "../types/inc/viewport.hpp" +#include "../../buffer/out/cursor.h" + +#include +#include + +#ifdef UNIT_TESTING +class UiaTextRangeTests; +#endif + + +// The UiaTextRange deals with several data structures that have +// similar semantics. In order to keep the information from these data +// structures separated, each structure has its own naming for a +// row. +// +// There is the generic Row, which does not know which data structure +// the row came from. +// +// There is the ViewportRow, which is a 0-indexed row value from the +// viewport. The top row of the viewport is at 0, rows below the top +// row increase in value and rows above the top row get increasingly +// negative. +// +// ScreenInfoRow is a row from the screen info data structure. They +// start at 0 at the top of screen info buffer. Their positions do not +// change but their associated row in the text buffer does change each +// time a new line is written. +// +// TextBufferRow is a row from the text buffer. It is not a ROW +// struct, but rather the index of a row. This is also 0-indexed. A +// TextBufferRow with a value of 0 does not necessarily refer to the +// top row of the console. + +typedef int Row; +typedef int ViewportRow; +typedef unsigned int ScreenInfoRow; +typedef unsigned int TextBufferRow; + +typedef unsigned long long IdType; + +// A Column is a row agnostic value that refers to the column an +// endpoint is equivalent to. It is 0-indexed. +typedef unsigned int Column; + +// an endpoint is a char location in the text buffer. endpoint 0 is +// the first char of the 0th row in the text buffer row array. +typedef unsigned int Endpoint; + +namespace Microsoft::Console::Interactivity::Win32 +{ + + + class UiaTextRange final : public ITextRangeProvider + { + private: + static IdType id; + + protected: + // indicates which direction a movement operation + // is going + enum class MovementDirection + { + Forward, + Backward + }; + + // valid increment amounts for forward and + // backward movement + enum class MovementIncrement + { + Forward = 1, + Backward = -1 + }; + + // common information used by the variety of + // movement operations + struct MoveState + { + // screen/column position of _start + ScreenInfoRow StartScreenInfoRow; + Column StartColumn; + // screen/column position of _end + ScreenInfoRow EndScreenInfoRow; + Column EndColumn; + // last row in the direction being moved + ScreenInfoRow LimitingRow; + // first column in the direction being moved + Column FirstColumnInRow; + // last column in the direction being moved + Column LastColumnInRow; + // increment amount + MovementIncrement Increment; + // direction moving + MovementDirection Direction; + + MoveState(const UiaTextRange& range, + const MovementDirection direction); + + private: + MoveState(const ScreenInfoRow startScreenInfoRow, + const Column startColumn, + const ScreenInfoRow endScreenInfoRow, + const Column endColumn, + const ScreenInfoRow limitingRow, + const Column firstColumnInRow, + const Column lastColumnInRow, + const MovementIncrement increment, + const MovementDirection direction); + +#ifdef UNIT_TESTING + friend class ::UiaTextRangeTests; +#endif + }; + + public: + + static std::deque GetSelectionRanges(_In_ IRawElementProviderSimple* pProvider); + + // degenerate range + static UiaTextRange* Create(_In_ IRawElementProviderSimple* const pProvider); + + // degenerate range at cursor position + static UiaTextRange* Create(_In_ IRawElementProviderSimple* const pProvider, + const Cursor& cursor); + + // specific endpoint range + static UiaTextRange* Create(_In_ IRawElementProviderSimple* const pProvider, + const Endpoint start, + const Endpoint end, + const bool degenerate); + + // range from a UiaPoint + static UiaTextRange* Create(_In_ IRawElementProviderSimple* const pProvider, + const UiaPoint point); + + ~UiaTextRange(); + + + const IdType GetId() const; + const Endpoint GetStart() const; + const Endpoint GetEnd() const; + const bool IsDegenerate() const; + + // IUnknown methods + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, + _COM_Outptr_result_maybenull_ void** ppInterface); + + // ITextRangeProvider methods + IFACEMETHODIMP Clone(_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal); + IFACEMETHODIMP CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint, + _In_ ITextRangeProvider* pTargetRange, + _In_ TextPatternRangeEndpoint targetEndpoint, + _Out_ int* pRetVal); + IFACEMETHODIMP ExpandToEnclosingUnit(_In_ TextUnit unit); + IFACEMETHODIMP FindAttribute(_In_ TEXTATTRIBUTEID textAttributeId, + _In_ VARIANT val, + _In_ BOOL searchBackward, + _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP FindText(_In_ BSTR text, + _In_ BOOL searchBackward, + _In_ BOOL ignoreCase, + _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP GetAttributeValue(_In_ TEXTATTRIBUTEID textAttributeId, + _Out_ VARIANT* pRetVal); + IFACEMETHODIMP GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal); + IFACEMETHODIMP GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal); + IFACEMETHODIMP GetText(_In_ int maxLength, + _Out_ BSTR* pRetVal); + IFACEMETHODIMP Move(_In_ TextUnit unit, + _In_ int count, + _Out_ int* pRetVal); + IFACEMETHODIMP MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, + _In_ TextUnit unit, + _In_ int count, + _Out_ int* pRetVal); + IFACEMETHODIMP MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint, + _In_ ITextRangeProvider* pTargetRange, + _In_ TextPatternRangeEndpoint targetEndpoint); + IFACEMETHODIMP Select(); + IFACEMETHODIMP AddToSelection(); + IFACEMETHODIMP RemoveFromSelection(); + IFACEMETHODIMP ScrollIntoView(_In_ BOOL alignToTop); + IFACEMETHODIMP GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal); + + protected: +#if _DEBUG + void _outputRowConversions(); + void _outputObjectState(); +#endif + + IRawElementProviderSimple* const _pProvider; + + private: + // degenerate range + UiaTextRange(_In_ IRawElementProviderSimple* const pProvider); + + // degenerate range at cursor position + UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const Cursor& cursor); + + // specific endpoint range + UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const Endpoint start, + const Endpoint end, + const bool degenerate); + + // range from a UiaPoint + UiaTextRange(_In_ IRawElementProviderSimple* const pProvider, + const UiaPoint point); + + UiaTextRange(const UiaTextRange& a); + + // used to debug objects passed back and forth + // between the provider and the client + IdType _id; + + // Ref counter for COM object + ULONG _cRefs; + + // measure units in the form [_start, _end]. _start + // may be a bigger number than _end if the range + // wraps around the end of the text buffer. + // + // In this scenario, _start <= _end + // 0 ............... N (text buffer line indices) + // s-----e (_start to _end) + // + // In this scenario, _start >= end + // 0 ............... N (text buffer line indices) + // ---e s----- (_start to _end) + // + Endpoint _start; + Endpoint _end; + + // The msdn documentation (and hence this class) talks a bunch about a + // degenerate range. A range is degenerate if it contains + // no text (both the start and end endpoints are the same). Note that + // a degenerate range may have a position in the text. We indicate a + // degenerate range internally with a bool. If a range is degenerate + // then both endpoints will contain the same value. + bool _degenerate; + + static const Microsoft::Console::Types::Viewport& _getViewport(); + static HWND _getWindowHandle(); + static IConsoleWindow* const _getIConsoleWindow(); + static SCREEN_INFORMATION& _getScreenInfo(); + static TextBuffer& _getTextBuffer(); + static const COORD _getScreenBufferCoords(); + + static const unsigned int _getTotalRows(); + static const unsigned int _getRowWidth(); + + static const unsigned int _getFirstScreenInfoRowIndex(); + static const unsigned int _getLastScreenInfoRowIndex(); + + static const Column _getFirstColumnIndex(); + static const Column _getLastColumnIndex(); + + const unsigned int _rowCountInRange() const; + + static const TextBufferRow _endpointToTextBufferRow(const Endpoint endpoint); + static const ScreenInfoRow _textBufferRowToScreenInfoRow(const TextBufferRow row); + + static const TextBufferRow _screenInfoRowToTextBufferRow(const ScreenInfoRow row); + static const Endpoint _textBufferRowToEndpoint(const TextBufferRow row); + + static const ScreenInfoRow _endpointToScreenInfoRow(const Endpoint endpoint); + static const Endpoint _screenInfoRowToEndpoint(const ScreenInfoRow row); + + static COORD _endpointToCoord(const Endpoint endpoint); + static Endpoint _coordToEndpoint(const COORD coord); + + static const Column _endpointToColumn(const Endpoint endpoint); + + static const Row _normalizeRow(const Row row); + + static const ViewportRow _screenInfoRowToViewportRow(const ScreenInfoRow row); + static const ViewportRow _screenInfoRowToViewportRow(const ScreenInfoRow row, + const SMALL_RECT viewport); + + static const bool _isScreenInfoRowInViewport(const ScreenInfoRow row); + static const bool _isScreenInfoRowInViewport(const ScreenInfoRow row, + const SMALL_RECT viewport); + + static const unsigned int _getViewportHeight(const SMALL_RECT viewport); + static const unsigned int _getViewportWidth(const SMALL_RECT viewport); + + void _addScreenInfoRowBoundaries(const ScreenInfoRow screenInfoRow, + _Inout_ std::vector& coords) const; + + static const int _compareScreenCoords(const ScreenInfoRow rowA, + const Column colA, + const ScreenInfoRow rowB, + const Column colB); + + static std::pair _moveByCharacter(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::pair _moveByCharacterForward(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::pair _moveByCharacterBackward(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::pair _moveByLine(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::pair _moveByDocument(const int moveCount, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::tuple + _moveEndpointByUnitCharacter(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::tuple + _moveEndpointByUnitCharacterForward(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::tuple + _moveEndpointByUnitCharacterBackward(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::tuple + _moveEndpointByUnitLine(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved); + + static std::tuple + _moveEndpointByUnitDocument(const int moveCount, + const TextPatternRangeEndpoint endpoint, + const MoveState moveState, + _Out_ int* const pAmountMoved); + +#ifdef UNIT_TESTING + friend class ::UiaTextRangeTests; +#endif + }; + + namespace UiaTextRangeTracing + { + + enum class ApiCall + { + Constructor, + AddRef, + Release, + QueryInterface, + Clone, + Compare, + CompareEndpoints, + ExpandToEnclosingUnit, + FindAttribute, + FindText, + GetAttributeValue, + GetBoundingRectangles, + GetEnclosingElement, + GetText, + Move, + MoveEndpointByUnit, + MoveEndpointByRange, + Select, + AddToSelection, + RemoveFromSelection, + ScrollIntoView, + GetChildren + }; + + struct IApiMsg + { + }; + + struct ApiMsgConstructor : public IApiMsg + { + IdType Id; + }; + + struct ApiMsgClone : public IApiMsg + { + IdType CloneId; + }; + + struct ApiMsgCompare : public IApiMsg + { + IdType OtherId; + bool Equal; + }; + + struct ApiMsgCompareEndpoints : public IApiMsg + { + IdType OtherId; + TextPatternRangeEndpoint Endpoint; + TextPatternRangeEndpoint TargetEndpoint; + int Result; + }; + + struct ApiMsgExpandToEnclosingUnit : public IApiMsg + { + TextUnit Unit; + Endpoint OriginalStart; + Endpoint OriginalEnd; + }; + + struct ApiMsgGetText : IApiMsg + { + const wchar_t* Text; + }; + + struct ApiMsgMove : IApiMsg + { + Endpoint OriginalStart; + Endpoint OriginalEnd; + TextUnit Unit; + int RequestedCount; + int MovedCount; + }; + + struct ApiMsgMoveEndpointByUnit : IApiMsg + { + Endpoint OriginalStart; + Endpoint OriginalEnd; + TextPatternRangeEndpoint Endpoint; + TextUnit Unit; + int RequestedCount; + int MovedCount; + }; + + struct ApiMsgMoveEndpointByRange : IApiMsg + { + Endpoint OriginalStart; + Endpoint OriginalEnd; + TextPatternRangeEndpoint Endpoint; + TextPatternRangeEndpoint TargetEndpoint; + IdType OtherId; + }; + + struct ApiMsgScrollIntoView : IApiMsg + { + bool AlignToTop; + }; + } +} diff --git a/src/interactivity/win32/WindowIme.cpp b/src/interactivity/win32/WindowIme.cpp new file mode 100644 index 000000000..132273ee0 --- /dev/null +++ b/src/interactivity/win32/WindowIme.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\inc\ServiceLocator.hpp" + +#include "window.hpp" + +#pragma hdrstop + +// Routine Description: +// - This method gives a rectangle to where the command edit line text is currently rendered +// such that the IME suggestion window can pop up in a suitable location adjacent to the given rectangle. +// Arguments: +// - +// Return Value: +// - Rectangle specifying current command line edit area. +RECT GetImeSuggestionWindowPos() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& screenBuffer = gci.GetActiveOutputBuffer(); + + const COORD coordFont = screenBuffer.GetCurrentFont().GetSize(); + COORD coordCursor = screenBuffer.GetTextBuffer().GetCursor().GetPosition(); + + // Adjust the cursor position to be relative to the viewport. + // This means that if the cursor is at row 30 in the buffer but the viewport is showing rows 20-40 right now on screen + // that the "relative" position is that it is on the 11th line from the top (or 10th by index). + // Correct by subtracting the top/left corner from the cursor's position. + SMALL_RECT const srViewport = screenBuffer.GetViewport().ToInclusive(); + coordCursor.X -= srViewport.Left; + coordCursor.Y -= srViewport.Top; + + // Map the point to be just under the current cursor position. Convert from coordinate to pixels using font. + POINT ptSuggestion; + ptSuggestion.x = (coordCursor.X + 1) * coordFont.X; + ptSuggestion.y = (coordCursor.Y) * coordFont.Y; + + // Adjust client point to screen point via HWND. + ClientToScreen(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), &ptSuggestion); + + // Move into suggestion rectangle. + RECT rcSuggestion = { 0 }; + rcSuggestion.top = rcSuggestion.bottom = ptSuggestion.y; + rcSuggestion.left = rcSuggestion.right = ptSuggestion.x; + + // Add 1 line height and a few characters of width to represent the area where we're writing text. + // This could be more exact by looking up the CONVAREA but it works well enough this way. + // If there is a future issue with the pop-up window, tweak these metrics. + rcSuggestion.bottom += coordFont.Y; + rcSuggestion.right += (coordFont.X * 10); + + return rcSuggestion; +} diff --git a/src/interactivity/win32/WindowMetrics.cpp b/src/interactivity/win32/WindowMetrics.cpp new file mode 100644 index 000000000..325a6c105 --- /dev/null +++ b/src/interactivity/win32/WindowMetrics.cpp @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "WindowMetrics.hpp" + +#include "windowdpiapi.hpp" +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +// The following default masks are used in creating windows +#define CONSOLE_WINDOW_FLAGS (WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL) +#define CONSOLE_WINDOW_EX_FLAGS (WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES | WS_EX_APPWINDOW ) + +using namespace Microsoft::Console::Interactivity::Win32; + +// Routine Description: +// - Gets the minimum possible client rectangle in pixels. +// - Purely based on system metrics. Doesn't compensate for potential scroll bars. +// Arguments: +// - +// Return Value: +// - RECT of client area positions in pixels. +RECT WindowMetrics::GetMinClientRectInPixels() +{ + // prepare rectangle + RECT rc; + SetRectEmpty(&rc); + + // set bottom/right dimensions to represent minimum window width/height + rc.right = GetSystemMetrics(SM_CXMIN); + rc.bottom = GetSystemMetrics(SM_CYMIN); + + // convert to client rect + ConvertWindowRectToClientRect(&rc); + + // there is no scroll bar subtraction here as the minimum window dimensions can be expanded wider to hold a scroll bar if necessary + + return rc; +} + +// Routine Description: +// - Gets the maximum possible client rectangle in pixels. +// - This leaves space for potential scroll bars to be visible within the window (which are non-client area pixels when rendered) +// - This is a measurement of the inner area of the window, not including the non-client frame area and not including scroll bars. +// Arguments: +// - +// Return Value: +// - RECT of client area positions in pixels. +RECT WindowMetrics::GetMaxClientRectInPixels() +{ + // This will retrieve the outer window rect. We need the client area to calculate characters. + RECT rc = GetMaxWindowRectInPixels(); + + // convert to client rect + ConvertWindowRectToClientRect(&rc); + + return rc; +} + +// Routine Description: +// - Gets the maximum possible window rectangle in pixels. Based on the monitor the window is on or the primary monitor if no window exists yet. +// Arguments: +// - +// Return Value: +// - RECT containing the left, right, top, and bottom positions from the desktop origin in pixels. Measures the outer edges of the potential window. +RECT WindowMetrics::GetMaxWindowRectInPixels() +{ + RECT rc; + SetRectEmpty(&rc); + return GetMaxWindowRectInPixels(&rc, nullptr); +} + +// Routine Description: +// - Gets the maximum possible window rectangle in pixels. Based on the monitor the window is on or the primary monitor if no window exists yet. +// Arguments: +// - prcSuggested - If we were given a suggested rectangle for where the window is going, we can pass it in here to find out the max size on that monitor. +// - If this value is zero and we had a valid window handle, we'll use that instead. Otherwise the value of 0 will make us use the primary monitor. +// - pDpiSuggested - The dpi that matches the suggested rect. We will attempt to compute this during the function, but if we fail for some reason, +// - the original value passed in will be left untouched. +// Return Value: +// - RECT containing the left, right, top, and bottom positions from the desktop origin in pixels. Measures the outer edges of the potential window. +RECT WindowMetrics::GetMaxWindowRectInPixels(const RECT * const prcSuggested, _Out_opt_ UINT * pDpiSuggested) +{ + // prepare rectangle + RECT rc = *prcSuggested; + + // use zero rect to compare. + RECT rcZero; + SetRectEmpty(&rcZero); + + // First get the monitor pointer from either the active window or the default location (0,0,0,0) + HMONITOR hMonitor = nullptr; + + // NOTE: We must use the nearest monitor because sometimes the system moves the window around into strange spots while performing snap and Win+D operations. + // Those operations won't work correctly if we use MONITOR_DEFAULTTOPRIMARY. + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow == nullptr || (TRUE != EqualRect(&rc, &rcZero))) + { + // For invalid window handles or when we were passed a non-zero suggestion rectangle, get the monitor from the rect. + hMonitor = MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST); + } + else + { + // Otherwise, get the monitor from the window handle. + hMonitor = MonitorFromWindow(pWindow->GetWindowHandle(), MONITOR_DEFAULTTONEAREST); + } + + // If for whatever reason there is no monitor, we're going to give back whatever we got since we can't figure anything out. + // We won't adjust the DPI either. That's OK. DPI doesn't make much sense with no display. + if (nullptr == hMonitor) + { + return rc; + } + + // Now obtain the monitor pixel dimensions + MONITORINFO MonitorInfo = { 0 }; + MonitorInfo.cbSize = sizeof(MONITORINFO); + + GetMonitorInfoW(hMonitor, &MonitorInfo); + + // We have to make a correction to the work area. If we actually consume the entire work area (by maximizing the window) + // The window manager will render the borders off-screen. + // We need to pad the work rectangle with the border dimensions to represent the actual max outer edges of the window rect. + WINDOWINFO wi = { 0 }; + wi.cbSize = sizeof(WINDOWINFO); + GetWindowInfo(pWindow ? pWindow->GetWindowHandle() : nullptr, &wi); + + if (pWindow != nullptr && pWindow->IsInFullscreen()) + { + // In full screen mode, we will consume the whole monitor with no chrome. + rc = MonitorInfo.rcMonitor; + } + else + { + // In non-full screen, we want to only use the work area (avoiding the task bar space) + rc = MonitorInfo.rcWork; + rc.top -= wi.cyWindowBorders; + rc.bottom += wi.cyWindowBorders; + rc.left -= wi.cxWindowBorders; + rc.right += wi.cxWindowBorders; + } + + if (pDpiSuggested != nullptr) + { + UINT monitorDpiX; + UINT monitorDpiY; + if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &monitorDpiX, &monitorDpiY))) + { + *pDpiSuggested = monitorDpiX; + } + else + { + *pDpiSuggested = ServiceLocator::LocateGlobals().dpi; + } + } + + return rc; +} + +// Routine Description: +// - Converts a client rect (inside not including non-client area) into a window rect (the outside edge dimensions) +// - NOTE: This one uses the current global DPI for calculations. +// Arguments: +// - prc - Rectangle to be adjusted from window to client +// - dwStyle - Style flags +// - fMenu - Whether or not a menu is present for this window +// - dwExStyle - Extended Style flags +// Return Value: +// - BOOL if adjustment was successful. (See AdjustWindowRectEx definition for details). +BOOL WindowMetrics::AdjustWindowRectEx(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle) +{ + return ServiceLocator::LocateHighDpiApi()->AdjustWindowRectExForDpi(prc, dwStyle, fMenu, dwExStyle, ServiceLocator::LocateGlobals().dpi); +} + +// Routine Description: +// - Converts a client rect (inside not including non-client area) into a window rect (the outside edge dimensions) +// Arguments: +// - prc - Rectangle to be adjusted from window to client +// - dwStyle - Style flags +// - fMenu - Whether or not a menu is present for this window +// - dwExStyle - Extended Style flags +// - iDpi - The DPI to use for scaling. +// Return Value: +// - BOOL if adjustment was successful. (See AdjustWindowRectEx definition for details). +BOOL WindowMetrics::AdjustWindowRectEx(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle, const int iDpi) +{ + return ServiceLocator::LocateHighDpiApi()->AdjustWindowRectExForDpi(prc, dwStyle, fMenu, dwExStyle, iDpi); +} + +// Routine Description: +// - Converts a client rect (inside not including non-client area) into a window rect (the outside edge dimensions) +// - This is a helper to call AdjustWindowRectEx. +// - It finds the appropriate window styles for the active window or uses the defaults from our class registration. +// - It does NOT compensate for scrollbars or menus. +// Arguments: +// - prc - Pointer to rectangle to be adjusted from client to window positions. +// Return Value: +// - +void WindowMetrics::ConvertClientRectToWindowRect(_Inout_ RECT* const prc) +{ + ConvertRect(prc, ConvertRectangle::CLIENT_TO_WINDOW); +} + +// Routine Description: +// - Converts a window rect (the outside edge dimensions) into a client rect (inside not including non-client area) +// - This is a helper to call UnadjustWindowRectEx. +// - It finds the appropriate window styles for the active window or uses the defaults from our class registration. +// - It does NOT compensate for scrollbars or menus. +// Arguments: +// - prc - Pointer to rectangle to be adjusted from window to client positions. +// Return Value: +// - +void WindowMetrics::ConvertWindowRectToClientRect(_Inout_ RECT* const prc) +{ + ConvertRect(prc, ConvertRectangle::WINDOW_TO_CLIENT); +} + +// Routine Description: +// - Converts a window rect (the outside edge dimensions) into a client rect (inside not including non-client area) +// - Effectively the opposite math of "AdjustWindowRectEx" +// - See: http://blogs.msdn.com/b/oldnewthing/archive/2013/10/17/10457292.aspx +// Arguments: +// - prc - Rectangle to be adjusted from window to client +// - dwStyle - Style flags +// - fMenu - Whether or not a menu is present for this window +// - dwExStyle - Extended Style flags +// Return Value: +// - BOOL if adjustment was successful. (See AdjustWindowRectEx definition for details). +BOOL WindowMetrics::UnadjustWindowRectEx(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle) +{ + RECT rc; + SetRectEmpty(&rc); + BOOL fRc = AdjustWindowRectEx(&rc, dwStyle, fMenu, dwExStyle); + if (fRc) + { + prc->left -= rc.left; + prc->top -= rc.top; + prc->right -= rc.right; + prc->bottom -= rc.bottom; + } + return fRc; +} + +// Routine Description: +// - To reduce code duplication, this will do the style lookup and forwards/backwards calls for client/window rectangle translation. +// - Only really intended for use by the related static methods. +// Arguments: +// - prc - Pointer to rectangle to be adjusted from client to window positions. +// - crDirection - specifies which conversion to perform +// Return Value: +// - +void WindowMetrics::ConvertRect(_Inout_ RECT* const prc, const ConvertRectangle crDirection) +{ + // collect up current window style (if available) for adjustment + DWORD dwStyle = 0; + DWORD dwExStyle = 0; + + IConsoleWindow *pWindow = ServiceLocator::LocateConsoleWindow(); + if (pWindow != nullptr) + { + dwStyle = GetWindowStyle(pWindow->GetWindowHandle()); + dwExStyle = GetWindowExStyle(pWindow->GetWindowHandle()); + } + else + { + dwStyle = CONSOLE_WINDOW_FLAGS; + dwExStyle = CONSOLE_WINDOW_EX_FLAGS; + } + + switch (crDirection) + { + case CLIENT_TO_WINDOW: + { + // ask system to adjust our client rectangle into a window rectangle using the given style + AdjustWindowRectEx(prc, dwStyle, false, dwExStyle); + break; + } + case WINDOW_TO_CLIENT: + { + // ask system to adjust our window rectangle into a client rectangle using the given style + UnadjustWindowRectEx(prc, dwStyle, false, dwExStyle); + break; + } + default: + { + FAIL_FAST_HR(E_NOTIMPL); // not implemented + } + } +} diff --git a/src/interactivity/win32/WindowMetrics.hpp b/src/interactivity/win32/WindowMetrics.hpp new file mode 100644 index 000000000..042d1b3f5 --- /dev/null +++ b/src/interactivity/win32/WindowMetrics.hpp @@ -0,0 +1,58 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WindowMetrics.hpp + +Abstract: +- Win32 implementation of the IWindowMetrics interface. + +Author(s): +- Hernan Gatta (HeGatta) 29-Mar-2017 +--*/ + +#include "..\inc\IWindowMetrics.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class WindowMetrics final : public IWindowMetrics + { + public: + // IWindowMetrics Members + ~WindowMetrics() = default; + RECT GetMinClientRectInPixels(); + RECT GetMaxClientRectInPixels(); + + // Public Members + RECT GetMaxWindowRectInPixels(); + RECT GetMaxWindowRectInPixels(const RECT * const prcSuggested, _Out_opt_ UINT * pDpiSuggested); + + BOOL AdjustWindowRectEx(_Inout_ LPRECT prc, + const DWORD dwStyle, + const BOOL fMenu, + const DWORD dwExStyle); + BOOL AdjustWindowRectEx(_Inout_ LPRECT prc, + const DWORD dwStyle, + const BOOL fMenu, + const DWORD dwExStyle, + const int iDpi); + + void ConvertClientRectToWindowRect(_Inout_ RECT * const prc); + void ConvertWindowRectToClientRect(_Inout_ RECT * const prc); + + private: + enum ConvertRectangle + { + CLIENT_TO_WINDOW, + WINDOW_TO_CLIENT + }; + + BOOL UnadjustWindowRectEx(_Inout_ LPRECT prc, + const DWORD dwStyle, + const BOOL fMenu, + const DWORD dwExStyle); + + void ConvertRect(_Inout_ RECT* const prc, const ConvertRectangle crDirection); + }; +} diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp new file mode 100644 index 000000000..be4703bbd --- /dev/null +++ b/src/interactivity/win32/clipboard.hpp @@ -0,0 +1,57 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- clipboard.hpp + +Abstract: +- This module is used for clipboard operations + +Author(s): +- Michael Niksa (MiNiksa) 10-Apr-2014 +- Paul Campbell (PaulCam) 10-Apr-2014 + +Revision History: +- From components of clipbrd.h/.c +--*/ + +#pragma once + +#include "precomp.h" + +#include "..\..\host\screenInfo.hpp" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class Clipboard + { + public: + static Clipboard& Instance(); + + void Copy(_In_ bool const fAlsoCopyHtml = false); + void StringPaste(_In_reads_(cchData) PCWCHAR pwchData, + const size_t cchData); + void Paste(); + + private: + std::deque> TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData, + const size_t cchData); + + void StoreSelectionToClipboard(_In_ bool const fAlsoCopyHtml); + + TextBuffer::TextAndColor RetrieveTextFromBuffer(const SCREEN_INFORMATION& screenInfo, + const bool lineSelection, + const std::vector& selectionRects); + + void CopyHTMLToClipboard(const TextBuffer::TextAndColor& rows); + std::string GenHTML(const TextBuffer::TextAndColor & rows); + void CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, _In_ bool const fAlsoCopyHtml); + + bool FilterCharacterOnPaste(_Inout_ WCHAR * const pwch); + +#ifdef UNIT_TESTING + friend class ClipboardTests; +#endif + }; +} diff --git a/src/interactivity/win32/coninteractivitywin32.rcv b/src/interactivity/win32/coninteractivitywin32.rcv new file mode 100644 index 000000000..0171b6f8e --- /dev/null +++ b/src/interactivity/win32/coninteractivitywin32.rcv @@ -0,0 +1,5 @@ +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Interactivity Win32" +#define VER_INTERNALNAME_STR "ConInteractivityWin32" +#define VER_ORIGINALFILENAME_STR "CONINTERACTIVITYWIN32.DLL" diff --git a/src/interactivity/win32/consoleKeyInfo.cpp b/src/interactivity/win32/consoleKeyInfo.cpp new file mode 100644 index 000000000..e4de34464 --- /dev/null +++ b/src/interactivity/win32/consoleKeyInfo.cpp @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "consoleKeyInfo.hpp" + +// The following data structures are a hack to work around the fact that +// MapVirtualKey does not return the correct virtual key code in many cases. +// we store the correct info (from the keydown message) in the CONSOLE_KEY_INFO +// structure when a keydown message is translated. Then when we receive a +// wm_[sys][dead]char message, we retrieve it and clear out the record. + +#define CONSOLE_FREE_KEY_INFO 0 +#define CONSOLE_MAX_KEY_INFO 32 + +typedef struct _CONSOLE_KEY_INFO +{ + HWND hWnd; + WORD wVirtualKeyCode; + WORD wVirtualScanCode; +} CONSOLE_KEY_INFO, *PCONSOLE_KEY_INFO; + +CONSOLE_KEY_INFO ConsoleKeyInfo[CONSOLE_MAX_KEY_INFO]; + +void StoreKeyInfo(_In_ PMSG msg) +{ + UINT i; + + for (i = 0; i < CONSOLE_MAX_KEY_INFO; i++) + { + if (ConsoleKeyInfo[i].hWnd == CONSOLE_FREE_KEY_INFO || ConsoleKeyInfo[i].hWnd == msg->hwnd) + { + break; + } + } + + if (i != CONSOLE_MAX_KEY_INFO) + { + ConsoleKeyInfo[i].hWnd = msg->hwnd; + ConsoleKeyInfo[i].wVirtualKeyCode = LOWORD(msg->wParam); + ConsoleKeyInfo[i].wVirtualScanCode = (BYTE)(HIWORD(msg->lParam)); + } + else + { + RIPMSG0(RIP_WARNING, "ConsoleKeyInfo buffer is full"); + } +} + +void RetrieveKeyInfo(_In_ HWND hWnd, _Out_ PWORD pwVirtualKeyCode, _Inout_ PWORD pwVirtualScanCode, _In_ BOOL FreeKeyInfo) +{ + UINT i; + + for (i = 0; i < CONSOLE_MAX_KEY_INFO; i++) + { + if (ConsoleKeyInfo[i].hWnd == hWnd) + { + break; + } + } + + if (i != CONSOLE_MAX_KEY_INFO) + { + *pwVirtualKeyCode = ConsoleKeyInfo[i].wVirtualKeyCode; + *pwVirtualScanCode = ConsoleKeyInfo[i].wVirtualScanCode; + if (FreeKeyInfo) + { + ConsoleKeyInfo[i].hWnd = CONSOLE_FREE_KEY_INFO; + } + } + else + { + *pwVirtualKeyCode = (WORD)MapVirtualKeyW(*pwVirtualScanCode, 3); + } +} + +void ClearKeyInfo(const HWND hWnd) +{ + for (UINT i = 0; i < CONSOLE_MAX_KEY_INFO; i++) + { + if (ConsoleKeyInfo[i].hWnd == hWnd) + { + ConsoleKeyInfo[i].hWnd = CONSOLE_FREE_KEY_INFO; + } + } +} diff --git a/src/interactivity/win32/consoleKeyInfo.hpp b/src/interactivity/win32/consoleKeyInfo.hpp new file mode 100644 index 000000000..48654069c --- /dev/null +++ b/src/interactivity/win32/consoleKeyInfo.hpp @@ -0,0 +1,31 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- consoleKeyInfo.hpp + +Abstract: +- This module represents a queue of stored WM_KEYDOWN messages + that we will match up with the WM_CHARs that arrive later in the window message queue + after being posted by TranslateMessageEx. + This is necessary because the scan code data that arrives on WM_CHAR cannot be accurately recreated + later and may be necessary for us to place into the input queue that client applications can read. +- This class can be removed completely when the console takes over complete handling of WM_KEYDOWN translation. +- The future vision for WM_KEYDOWN translation would be to instead use the export ToUnicode/ToUnicodeEx to + create a console-internal version of what TranslateMessageEx does, but instead of posting the product back into + the window message queue (and needing this class to help line it up later) we would just immediately dispatch it to + our WM_CHAR routines while we still have the context. + +Author(s): +- Michael Niksa (MiNiksa) 11-Jan-2017 + +Revision History: +- From components of input.h/input.c by Therese Stowell (ThereseS) 1990-1991 +--*/ + +#pragma once + +void StoreKeyInfo(_In_ PMSG msg); +void RetrieveKeyInfo(_In_ HWND hWnd, _Out_ PWORD pwVirtualKeyCode, _Inout_ PWORD pwVirtualScanCode, _In_ BOOL FreeKeyInfo); +void ClearKeyInfo(const HWND hWnd); diff --git a/src/interactivity/win32/dirs b/src/interactivity/win32/dirs new file mode 100644 index 000000000..ec33b3b0c --- /dev/null +++ b/src/interactivity/win32/dirs @@ -0,0 +1,3 @@ +DIRS= \ + lib \ + ut_interactivity_win32 \ diff --git a/src/interactivity/win32/find.cpp b/src/interactivity/win32/find.cpp new file mode 100644 index 000000000..e46010972 --- /dev/null +++ b/src/interactivity/win32/find.cpp @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "find.h" +#include "resource.h" +#include "window.hpp" + +#include "..\..\host\dbcs.h" +#include "..\..\host\handle.h" +#include "..\..\host\search.h" + +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +INT_PTR FindDialogProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // This bool is used to track which option - up or down - was used to perform the last search. That way, the next time the + // find dialog is opened, it will default to the last used option. + static bool fFindSearchUp = true; + WCHAR szBuf[SEARCH_STRING_LENGTH + 1]; + switch (Message) + { + case WM_INITDIALOG: + SetWindowLongPtrW(hWnd, DWLP_USER, lParam); + SendDlgItemMessageW(hWnd, ID_CONSOLE_FINDSTR, EM_LIMITTEXT, ARRAYSIZE(szBuf) - 1, 0); + CheckRadioButton(hWnd, ID_CONSOLE_FINDUP, ID_CONSOLE_FINDDOWN, (fFindSearchUp? ID_CONSOLE_FINDUP : ID_CONSOLE_FINDDOWN)); + return TRUE; + case WM_COMMAND: + { + switch (LOWORD(wParam)) + { + case IDOK: + { + USHORT const StringLength = (USHORT) GetDlgItemTextW(hWnd, ID_CONSOLE_FINDSTR, szBuf, ARRAYSIZE(szBuf)); + if (StringLength == 0) + { + break; + } + bool const IgnoreCase = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDCASE) == 0; + bool const Reverse = IsDlgButtonChecked(hWnd, ID_CONSOLE_FINDDOWN) == 0; + fFindSearchUp = !!Reverse; + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + + std::wstring wstr(szBuf, StringLength); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + Search search(ScreenInfo, + wstr, + Reverse ? Search::Direction::Backward : Search::Direction::Forward, + IgnoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive); + + if (search.FindNext()) + { + Telemetry::Instance().LogFindDialogNextClicked(StringLength, (Reverse != 0), (IgnoreCase == 0)); + search.Select(); + return TRUE; + } + else + { + // The string wasn't found. + ScreenInfo.SendNotifyBeep(); + } + break; + } + case IDCANCEL: + Telemetry::Instance().FindDialogClosed(); + EndDialog(hWnd, 0); + return TRUE; + } + break; + } + default: + break; + } + return FALSE; +} + +void DoFind() +{ + Globals& g = ServiceLocator::LocateGlobals(); + IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); + + UnlockConsole(); + if (pWindow != nullptr) + { + HWND const hwnd = pWindow->GetWindowHandle(); + + ++g.uiDialogBoxCount; + DialogBoxParamW(g.hInstance, MAKEINTRESOURCE(ID_CONSOLE_FINDDLG), hwnd, (DLGPROC)FindDialogProc, (LPARAM) nullptr); + --g.uiDialogBoxCount; + } +} diff --git a/src/interactivity/win32/find.h b/src/interactivity/win32/find.h new file mode 100644 index 000000000..faed10477 --- /dev/null +++ b/src/interactivity/win32/find.h @@ -0,0 +1,19 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- find.h + +Abstract: +- This file implements the search functionality. + +Author: +- Jerry Shea (jerrysh) 1-May-1997 + +Revision History: +--*/ + +#pragma once + +void DoFind(); diff --git a/src/interactivity/win32/icon.cpp b/src/interactivity/win32/icon.cpp new file mode 100644 index 000000000..c65762d29 --- /dev/null +++ b/src/interactivity/win32/icon.cpp @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "icon.hpp" + +#include "window.hpp" + +#include "..\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +Icon::Icon() : + _fInitialized(false), + _hDefaultIcon(nullptr), + _hDefaultSmIcon(nullptr), + _hIcon(nullptr), + _hSmIcon(nullptr) +{ + +} + +Icon::~Icon() +{ + // Do not destroy icon handles. They're shared icons as they were loaded from LoadIcon/LoadImage. + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms648063(v=vs.85).aspx + + // Do destroy icons from ExtractIconEx. They're not shared. + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms648069(v=vs.85).aspx + _DestroyNonDefaultIcons(); +} + +// Routine Description: +// - Returns the singleton instance of the Icon +// Arguments: +// - +// Return Value: +// - Reference to the singleton. +Icon& Icon::Instance() +{ + static Icon i; + return i; +} + +// Routine Description: +// - Gets the requested icons. Will return default icons if no icons have been set. +// Arguments: +// - phIcon - The large icon representation. +// - phSmIcon - The small icon representation. +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::GetIcons(_Out_opt_ HICON* const phIcon, _Out_opt_ HICON* const phSmIcon) +{ + HRESULT hr = S_OK; + + if (nullptr != phIcon) + { + hr = _GetAvailableIconFromReference(_hIcon, _hDefaultIcon, phIcon); + } + + if (SUCCEEDED(hr)) + { + if (nullptr != phSmIcon) + { + hr = _GetAvailableIconFromReference(_hSmIcon, _hDefaultSmIcon, phSmIcon); + } + } + + return hr; +} + +// Routine Description: +// - Sets custom icons onto the class or resets the icons to defaults. Use a null handle to reset an icon to its default value. +// Arguments: +// - hIcon - The large icon handle or null to reset to default +// - hSmIcon - The small icon handle or null to reset to default +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::SetIcons(const HICON hIcon, const HICON hSmIcon) +{ + HRESULT hr = _SetIconFromReference(_hIcon, hIcon); + + if (SUCCEEDED(hr)) + { + hr = _SetIconFromReference(_hSmIcon, hSmIcon); + } + + if (SUCCEEDED(hr)) + { + HICON hNewIcon; + HICON hNewSmIcon; + + hr = GetIcons(&hNewIcon, &hNewSmIcon); + + if (SUCCEEDED(hr)) + { + // Special case. If we had a non-default big icon and a default small icon, set the small icon to null when updating the window. + // This will cause the large one to be stretched and used as the small one. + if (hNewIcon != _hDefaultIcon && hNewSmIcon == _hDefaultSmIcon) + { + hNewSmIcon = nullptr; + } + + PostMessageW(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), WM_SETICON, ICON_BIG, (LPARAM)hNewIcon); + PostMessageW(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), WM_SETICON, ICON_SMALL, (LPARAM)hNewSmIcon); + } + } + + return hr; +} + +// Routine Description: +// - Loads icons from a given path on the file system. +// - Will only load one icon from the file. +// Arguments: +// - pwszIconLocation - File system path to extract icons from +// - nIconIndex - Index offset of the icon within the file +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::LoadIconsFromPath(_In_ PCWSTR pwszIconLocation, const int nIconIndex) +{ + HRESULT hr = S_OK; + + // Return value is count of icons extracted, which is redundant with filling the pointers. + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms648069(v=vs.85).aspx + ExtractIconExW(pwszIconLocation, nIconIndex, &_hIcon, &_hSmIcon, 1); + + // If the large icon failed, then ensure that we use the defaults. + if (_hIcon == nullptr) + { + _DestroyNonDefaultIcons(); // ensure handles are destroyed/null + hr = E_FAIL; + } + + return hr; +} + +// Routine Description: +// - Workaround for an oddity in WM_GETICON. +// - If you never call WM_SETICON and the system would have to look into the window class to get the icon, +// then any call to WM_GETICON will return NULL for the specified icon instead of returning the window class value. +// By calling WM_SETICON once, we ensure that third-party apps calling WM_GETICON will receive the icon we specify. +// Arguments: +// - hwnd - Handle to apply message workaround to. +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::ApplyWindowMessageWorkaround(const HWND hwnd) +{ + HICON hIcon; + HICON hSmIcon; + + HRESULT hr = GetIcons(&hIcon, &hSmIcon); + + if (SUCCEEDED(hr)) + { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon); + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hSmIcon); + } + + return hr; +} + +// Routine Description: +// - Initializes the icon class by loading the default icons. +// Arguments: +// - +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::_Initialize() +{ + HRESULT hr = S_OK; + + if (!_fInitialized) + { + #pragma warning(push) + #pragma warning(disable:4302) // typecast warning from MAKEINTRESOURCE + _hDefaultIcon = LoadIconW(nullptr, MAKEINTRESOURCE(IDI_APPLICATION)); + #pragma warning(pop) + + if (_hDefaultIcon == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + + if (SUCCEEDED(hr)) + { + #pragma warning(push) + #pragma warning(disable:4302) // typecast warning from MAKEINTRESOURCE + _hDefaultSmIcon = (HICON)LoadImageW(nullptr, + MAKEINTRESOURCE(IDI_APPLICATION), + IMAGE_ICON, + GetSystemMetrics(SM_CXSMICON), + GetSystemMetrics(SM_CYSMICON), + LR_SHARED + ); + #pragma warning(pop) + + if (_hDefaultSmIcon == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + } + + if (SUCCEEDED(hr)) + { + _fInitialized = true; + } + } + + return hr; +} + +// Routine Description: +// - Frees any non-default icon handles we may have loaded from a path on the file system +// Arguments: +// - +// Return Value: +// - +void Icon::_DestroyNonDefaultIcons() +{ + _FreeIconFromReference(_hIcon); + _FreeIconFromReference(_hSmIcon); +} + +// Routine Description: +// - Helper method to choose one of the two given references to fill the pointer with. +// - It will choose the specific icon if it is available and otherwise fall back to the default icon. +// Arguments: +// - hIconRef - reference to the specific icon handle inside this class +// - hDefaultIconRef - reference to the default icon handle inside this class +// - phIcon - pointer to receive the chosen icon handle +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::_GetAvailableIconFromReference(_In_ HICON& hIconRef, _In_ HICON& hDefaultIconRef, _Out_ HICON* const phIcon) +{ + HRESULT hr = S_OK; + + // expecting hIconRef to be pointing to either the regular or small custom handles + FAIL_FAST_IF(!(&hIconRef == &_hIcon || &hIconRef == &_hSmIcon)); + + // expecting hDefaultIconRef to be pointing to either the regular or small default handles + FAIL_FAST_IF(!(&hDefaultIconRef == &_hDefaultIcon || &hDefaultIconRef == &_hDefaultSmIcon)); + + if (hIconRef != nullptr) + { + *phIcon = hIconRef; + } + else + { + hr = _GetDefaultIconFromReference(hDefaultIconRef, phIcon); + } + + return hr; +} + +// Routine Description: +// - Helper method to initialize and retrieve a default icon. +// Arguments: +// - hIconRef - Either the small or large icon handle references within this class +// - phIcon - The pointer to fill with the icon if it is available. +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::_GetDefaultIconFromReference(_In_ HICON& hIconRef, _Out_ HICON* const phIcon) +{ + // expecting hIconRef to be pointing to either the regular or small default handles + FAIL_FAST_IF(!(&hIconRef == &_hDefaultIcon || &hIconRef == &_hDefaultSmIcon)); + + HRESULT hr = _Initialize(); + + if (SUCCEEDED(hr)) + { + *phIcon = hIconRef; + } + + return hr; +} + +// Routine Description: +// - Helper method to set an icon handle into the given reference to an icon within this class. +// - This will appropriately call to free existing custom icons. +// Arguments: +// - hIconRef - Either the small or large icon handle references within this class +// - hNewIcon - The new icon handle to replace the reference with. +// Return Value: +// - S_OK or HRESULT failure code. +[[nodiscard]] +HRESULT Icon::_SetIconFromReference(_In_ HICON& hIconRef, const HICON hNewIcon) +{ + // expecting hIconRef to be pointing to either the regular or small custom handles + FAIL_FAST_IF(!(&hIconRef == &_hIcon || &hIconRef == &_hSmIcon)); + + HRESULT hr = S_OK; + + // Only set icon if something changed + if (hNewIcon != hIconRef) + { + // If we had an existing custom icon, free it. + _FreeIconFromReference(hIconRef); + + // If we were given a non-null icon, store it. + if (hNewIcon != nullptr) + { + hIconRef = hNewIcon; + } + + // Otherwise, we'll default back to using the default icon. Get method will handle this. + } + + return hr; +} + +// Routine Description: +// - Helper method to free a specific icon reference to a specific icon within this class. +// - WARNING: Do not use with the default icons. They do not need to be released. +// Arguments: +// - hIconRef - Either the small or large specific icon handle references within this class +// Return Value: +// - None +void Icon::_FreeIconFromReference(_In_ HICON& hIconRef) +{ + // expecting hIconRef to be pointing to either the regular or small custom handles + FAIL_FAST_IF(!(&hIconRef == &_hIcon || &hIconRef == &_hSmIcon)); + + if (hIconRef != nullptr) + { + DestroyIcon(hIconRef); + hIconRef = nullptr; + } +} diff --git a/src/interactivity/win32/icon.hpp b/src/interactivity/win32/icon.hpp new file mode 100644 index 000000000..b32a94185 --- /dev/null +++ b/src/interactivity/win32/icon.hpp @@ -0,0 +1,63 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- icon.hpp + +Abstract: +- This module is used for managing icons + +Author(s): +- Michael Niksa (miniksa) 14-Oct-2014 +- Paul Campbell (paulcam) 14-Oct-2014 +--*/ + +#pragma once + +namespace Microsoft::Console::Interactivity::Win32 +{ + class Icon sealed + { + public: + static Icon& Instance(); + + [[nodiscard]] + HRESULT GetIcons(_Out_opt_ HICON* const phIcon, _Out_opt_ HICON* const phSmIcon); + [[nodiscard]] + HRESULT SetIcons(const HICON hIcon, const HICON hSmIcon); + + [[nodiscard]] + HRESULT LoadIconsFromPath(_In_ PCWSTR pwszIconLocation, const int nIconIndex); + + [[nodiscard]] + HRESULT ApplyWindowMessageWorkaround(const HWND hwnd); + + protected: + Icon(); + ~Icon(); + Icon(Icon const&) = delete; + void operator=(Icon const&) = delete; + + private: + [[nodiscard]] + HRESULT _Initialize(); + + void _DestroyNonDefaultIcons(); + + // Helper methods + [[nodiscard]] + HRESULT _GetAvailableIconFromReference(_In_ HICON& hIconRef, _In_ HICON& hDefaultIconRef, _Out_ HICON* const phIcon); + [[nodiscard]] + HRESULT _GetDefaultIconFromReference(_In_ HICON& hIconRef, _Out_ HICON* const phIcon); + [[nodiscard]] + HRESULT _SetIconFromReference(_In_ HICON& hIconRef, const HICON hNewIcon); + void _FreeIconFromReference(_In_ HICON& hIconRef); + + bool _fInitialized; + HICON _hDefaultIcon; + HICON _hDefaultSmIcon; + HICON _hIcon; + HICON _hSmIcon; + }; +} diff --git a/src/interactivity/win32/lib/sources b/src/interactivity/win32/lib/sources new file mode 100644 index 000000000..8c5b478fe --- /dev/null +++ b/src/interactivity/win32/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConInteractivityWin32Lib +TARGETTYPE = LIBRARY diff --git a/src/interactivity/win32/lib/win32.LIB.vcxproj b/src/interactivity/win32/lib/win32.LIB.vcxproj new file mode 100644 index 000000000..659a55448 --- /dev/null +++ b/src/interactivity/win32/lib/win32.LIB.vcxproj @@ -0,0 +1,72 @@ + + + + + + _WINDLL;%(PreprocessorDefinitions) + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {06EC74CB-9A12-429C-B551-8532EC964726} + Win32Proj + win32 + InteractivityWin32 + ConInteractivityWin32Lib + + + + ..\..\..\inc;%(AdditionalIncludeDirectories) + + + + + + \ No newline at end of file diff --git a/src/interactivity/win32/lib/win32.LIB.vcxproj.filters b/src/interactivity/win32/lib/win32.LIB.vcxproj.filters new file mode 100644 index 000000000..900e08aa1 --- /dev/null +++ b/src/interactivity/win32/lib/win32.LIB.vcxproj.filters @@ -0,0 +1,147 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/interactivity/win32/menu.cpp b/src/interactivity/win32/menu.cpp new file mode 100644 index 000000000..0c24753ab --- /dev/null +++ b/src/interactivity/win32/menu.cpp @@ -0,0 +1,610 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include + +#include "menu.hpp" +#include "icon.hpp" +#include "window.hpp" + +#include "..\..\host\dbcs.h" +#include "..\..\host\getset.h" +#include "..\..\host\handle.h" +#include "..\..\host\misc.h" +#include "..\..\host\server.h" +#include "..\..\host\scrolling.hpp" +#include "..\..\host\telemetry.hpp" + +#include "..\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +CONST WCHAR gwszPropertiesDll[] = L"\\console.dll"; +CONST WCHAR gwszRelativePropertiesDll[] = L".\\console.dll"; + +#pragma hdrstop + +// Initialize static Menu variables. +Menu* Menu::s_Instance = nullptr; + +#pragma region Public Methods + +Menu::Menu(HMENU hMenu, HMENU hHeirMenu) : + _hMenu(hMenu), + _hHeirMenu(hHeirMenu) +{ + +} + +// Routine Description: +// - This routine allocates and initializes the system menu for the console +// Arguments: +// - hWnd - The handle to the console's window +// Return Value: +// - STATUS_SUCCESS or suitable NT error code +[[nodiscard]] +NTSTATUS Menu::CreateInstance(HWND hWnd) +{ + NTSTATUS status = STATUS_SUCCESS; + HMENU hMenu = nullptr; + HMENU hHeirMenu = nullptr; + + int ItemLength; + WCHAR ItemString[32]; + + // This gets the title bar menu. + hMenu = GetSystemMenu(hWnd, FALSE); + hHeirMenu = LoadMenuW(ServiceLocator::LocateGlobals().hInstance, + MAKEINTRESOURCE(ID_CONSOLE_SYSTEMMENU)); + + Menu *pNewMenu = new(std::nothrow) Menu(hMenu, hHeirMenu); + status = NT_TESTNULL(pNewMenu); + + if (NT_SUCCESS(status)) + { + // Load the submenu to the system menu. + if (hHeirMenu) + { + ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, + ID_CONSOLE_EDIT, + ItemString, + ARRAYSIZE(ItemString)); + if (ItemLength) + { + // Append the clipboard menu to system menu. + AppendMenuW(hMenu, + MF_POPUP | MF_STRING, + (UINT_PTR)hHeirMenu, + ItemString); + } + } + + // Edit the accelerators off of the standard items. + // - Edits the indicated control to one word. + // - Trim the Accelerator key text off of the end of the standard menu + // items because we don't support the accelerators. + ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, + SC_CLOSE, + ItemString, + ARRAYSIZE(ItemString)); + if (ItemLength != 0) + { + MENUITEMINFO mii = { 0 }; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING | MIIM_BITMAP; + mii.dwTypeData = ItemString; + mii.hbmpItem = HBMMENU_POPUP_CLOSE; + + SetMenuItemInfoW(hMenu, SC_CLOSE, FALSE, &mii); + } + + // Add other items to the system menu. + ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, + ID_CONSOLE_DEFAULTS, + ItemString, + ARRAYSIZE(ItemString)); + if (ItemLength) + { + AppendMenuW(hMenu, + MF_STRING, + ID_CONSOLE_DEFAULTS, + ItemString); + } + + ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, + ID_CONSOLE_CONTROL, + ItemString, + ARRAYSIZE(ItemString)); + if (ItemLength) + { + AppendMenuW(hMenu, + MF_STRING, + ID_CONSOLE_CONTROL, + ItemString); + } + + // Set the singleton instance. + Menu::s_Instance = pNewMenu; + } + + return status; +} + +Menu* Menu::Instance() +{ + return Menu::s_Instance; +} + +Menu::~Menu() +{ + +} + +// Routine Description: +// - this initializes the system menu when a WM_INITMENU message is read. +void Menu::Initialize() +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + HMENU const hMenu = _hMenu; + HMENU const hHeirMenu = _hHeirMenu; + + // If the console is iconic, disable Mark and Scroll. + if (gci.Flags & CONSOLE_IS_ICONIC) + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_MARK, MF_GRAYED); + EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); + } + else + { + // if the console is not iconic + // if there are no scroll bars + // or we're in mark mode + // disable scroll + // else + // enable scroll + // + // if we're in scroll mode + // disable mark + // else + // enable mark + + if (gci.GetActiveOutputBuffer().IsMaximizedBoth() || gci.Flags & CONSOLE_SELECTING) + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); + } + else + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_ENABLED); + } + + if (Scrolling::s_IsInScrollMode()) + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); + } + else + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_ENABLED); + } + } + + // If we're selecting or scrolling, disable paste. Otherwise, enable it. + if (gci.Flags & (CONSOLE_SELECTING | CONSOLE_SCROLLING)) + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_PASTE, MF_GRAYED); + } + else + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_PASTE, MF_ENABLED); + } + + // If app has active selection, enable copy. Otherwise, disable it. + if (gci.Flags & CONSOLE_SELECTING && Selection::Instance().IsAreaSelected()) + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_COPY, MF_ENABLED); + } + else + { + EnableMenuItem(hHeirMenu, ID_CONSOLE_COPY, MF_GRAYED); + } + + // Enable move if not iconic. + if (gci.Flags & CONSOLE_IS_ICONIC) + { + EnableMenuItem(hMenu, SC_MOVE, MF_GRAYED); + } + else + { + EnableMenuItem(hMenu, SC_MOVE, MF_ENABLED); + } + + // Enable settings. + EnableMenuItem(hMenu, ID_CONSOLE_CONTROL, MF_ENABLED); +} + +#pragma endregion + +#pragma region Public Static Methods + +// Displays the properties dialog and updates the window state as necessary. +void Menu::s_ShowPropertiesDialog(HWND const hwnd, BOOL const Defaults) +{ + CONSOLE_STATE_INFO StateInfo = { 0 }; + if (!Defaults) + { + THROW_IF_FAILED(Menu::s_GetConsoleState(&StateInfo)); + StateInfo.UpdateValues = FALSE; + } + + // The Property sheet is going to copy the data from the values passed in + // to it, and potentially overwrite StateInfo.*Title. + // However, we just allocated wchar_t[]'s for these values. + // Stash the pointers to the arrays we just allocated, so we can free those + // arrays correctly. + const wchar_t* const allocatedOriginalTitle = StateInfo.OriginalTitle; + const wchar_t* const allocatedLinkTitle = StateInfo.LinkTitle; + + StateInfo.hWnd = hwnd; + StateInfo.Defaults = Defaults; + StateInfo.fIsV2Console = TRUE; + + UnlockConsole(); + + // First try to find the console.dll next to the launched exe, else default to /windows/system32/console.dll + HANDLE hLibrary = LoadLibraryExW(gwszRelativePropertiesDll, 0, 0); + bool fLoadedDll = hLibrary != nullptr; + if (!fLoadedDll) + { + WCHAR wszFilePath[MAX_PATH + 1] = { 0 }; + UINT const len = GetSystemDirectoryW(wszFilePath, ARRAYSIZE(wszFilePath)); + if (len < ARRAYSIZE(wszFilePath)) + { + if (SUCCEEDED(StringCchCatW(wszFilePath, ARRAYSIZE(wszFilePath) - len, gwszPropertiesDll))) + { + wszFilePath[ARRAYSIZE(wszFilePath) - 1] = UNICODE_NULL; + + hLibrary = LoadLibraryExW(wszFilePath, 0, LOAD_WITH_ALTERED_SEARCH_PATH); + fLoadedDll = hLibrary != nullptr; + } + } + } + + if (fLoadedDll) + { + APPLET_PROC const pfnCplApplet = (APPLET_PROC)GetProcAddress((HMODULE)hLibrary, "CPlApplet"); + if (pfnCplApplet != nullptr) + { + (*pfnCplApplet) (hwnd, CPL_INIT, 0, 0); + (*pfnCplApplet) (hwnd, CPL_DBLCLK, (LPARAM)& StateInfo, 0); + (*pfnCplApplet) (hwnd, CPL_EXIT, 0, 0); + } + + LOG_IF_WIN32_BOOL_FALSE(FreeLibrary((HMODULE)hLibrary)); + } + + LockConsole(); + + if (!Defaults && StateInfo.UpdateValues) + { + Menu::s_PropertiesUpdate(&StateInfo); + } + + // s_GetConsoleState may have created new wchar_t[]s for the title and link title. + // delete them before they're leaked. + if (allocatedOriginalTitle != nullptr) + { + delete[] allocatedOriginalTitle; + } + if (allocatedLinkTitle != nullptr) + { + delete[] allocatedLinkTitle; + } +} + +[[nodiscard]] +HRESULT Menu::s_GetConsoleState(CONSOLE_STATE_INFO * const pStateInfo) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + pStateInfo->ScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); + pStateInfo->WindowSize = ScreenInfo.GetViewport().Dimensions(); + + const RECT rcWindow = ServiceLocator::LocateConsoleWindow()->GetWindowRect(); + pStateInfo->WindowPosX = rcWindow.left; + pStateInfo->WindowPosY = rcWindow.top; + + const FontInfo& currentFont = ScreenInfo.GetCurrentFont(); + pStateInfo->FontFamily = currentFont.GetFamily(); + pStateInfo->FontSize = currentFont.GetUnscaledSize(); + pStateInfo->FontWeight = currentFont.GetWeight(); + StringCchCopyW(pStateInfo->FaceName, ARRAYSIZE(pStateInfo->FaceName), currentFont.GetFaceName()); + + const Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor(); + pStateInfo->CursorSize = cursor.GetSize(); + pStateInfo->CursorColor = cursor.GetColor(); + pStateInfo->CursorType = static_cast(cursor.GetType()); + + // Retrieve small icon for use in displaying the dialog + LOG_IF_FAILED(Icon::Instance().GetIcons(nullptr, &pStateInfo->hIcon)); + + pStateInfo->QuickEdit = !!(gci.Flags & CONSOLE_QUICK_EDIT_MODE); + pStateInfo->AutoPosition = !!(gci.Flags & CONSOLE_AUTO_POSITION); + pStateInfo->InsertMode = gci.GetInsertMode(); + pStateInfo->ScreenAttributes = gci.GetFillAttribute(); + pStateInfo->PopupAttributes = gci.GetPopupFillAttribute(); + + // Ensure that attributes are only describing colors to the properties dialog + WI_ClearAllFlags(pStateInfo->ScreenAttributes, ~(FG_ATTRS | BG_ATTRS)); + WI_ClearAllFlags(pStateInfo->PopupAttributes, ~(FG_ATTRS | BG_ATTRS)); + + pStateInfo->HistoryBufferSize = gci.GetHistoryBufferSize(); + pStateInfo->NumberOfHistoryBuffers = gci.GetNumberOfHistoryBuffers(); + pStateInfo->HistoryNoDup = !!(gci.Flags & CONSOLE_HISTORY_NODUP); + + memmove(pStateInfo->ColorTable, gci.GetColorTable(), gci.GetColorTableSize() * sizeof(COLORREF)); + + // Create mutable copies of the titles so the propsheet can do something with them. + if (gci.GetOriginalTitle().length() > 0) + { + pStateInfo->OriginalTitle = new(std::nothrow) wchar_t[gci.GetOriginalTitle().length()+1]{UNICODE_NULL}; + RETURN_IF_NULL_ALLOC(pStateInfo->OriginalTitle); + gci.GetOriginalTitle().copy(pStateInfo->OriginalTitle, gci.GetOriginalTitle().length()); + } + else + { + pStateInfo->OriginalTitle = nullptr; + } + + if (gci.GetLinkTitle().length() > 0) + { + pStateInfo->LinkTitle = new(std::nothrow) wchar_t[gci.GetLinkTitle().length()+1]{UNICODE_NULL}; + RETURN_IF_NULL_ALLOC(pStateInfo->LinkTitle); + gci.GetLinkTitle().copy(pStateInfo->LinkTitle, gci.GetLinkTitle().length()); + } + else + { + pStateInfo->LinkTitle = nullptr; + } + + pStateInfo->CodePage = gci.OutputCP; + + // begin console v2 properties + pStateInfo->fIsV2Console = TRUE; + pStateInfo->fWrapText = gci.GetWrapText(); + pStateInfo->fFilterOnPaste = !!gci.GetFilterOnPaste(); + pStateInfo->fCtrlKeyShortcutsDisabled = gci.GetCtrlKeyShortcutsDisabled(); + pStateInfo->fLineSelection = gci.GetLineSelection(); + pStateInfo->bWindowTransparency = ServiceLocator::LocateConsoleWindow()->GetWindowOpacity(); + + pStateInfo->InterceptCopyPaste = gci.GetInterceptCopyPaste(); + + // Get the properties from the settings - CONSOLE_INFORMATION overloads + // these methods to implement IDefaultColorProvider + pStateInfo->DefaultForeground = gci.GetDefaultForegroundColor(); + pStateInfo->DefaultBackground = gci.GetDefaultBackgroundColor(); + + pStateInfo->TerminalScrolling = gci.IsTerminalScrolling(); + // end console v2 properties + return S_OK; +} + +HMENU Menu::s_GetMenuHandle() +{ + if (Menu::s_Instance) + { + return Menu::s_Instance->_hMenu; + } + else + { + return nullptr; + } +} + +HMENU Menu::s_GetHeirMenuHandle() +{ + if (Menu::s_Instance) + { + return Menu::s_Instance->_hHeirMenu; + } + else + { + return nullptr; + } +} + +#pragma endregion + +#pragma region Private Methods + +// Updates the console state from information sent by the properties dialog box. +void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); + + if (gci.OutputCP != pStateInfo->CodePage) + { + gci.OutputCP = pStateInfo->CodePage; + + SetConsoleCPInfo(TRUE); + } + + if (gci.CP != pStateInfo->CodePage) + { + gci.CP = pStateInfo->CodePage; + + SetConsoleCPInfo(FALSE); + } + + // begin V2 console properties + + // NOTE: We must set the wrap state before further manipulating the buffer/window. + // If we do not, the user will get a different result than the preview (e.g. we'll resize without scroll bars first then turn off wrapping.) + gci.SetWrapText(!!pStateInfo->fWrapText); + gci.SetFilterOnPaste(!!pStateInfo->fFilterOnPaste); + gci.SetCtrlKeyShortcutsDisabled(!!pStateInfo->fCtrlKeyShortcutsDisabled); + gci.SetLineSelection(!!pStateInfo->fLineSelection); + Selection::Instance().SetLineSelection(!!gci.GetLineSelection()); + + ServiceLocator::LocateConsoleWindow()->SetWindowOpacity(pStateInfo->bWindowTransparency); + ServiceLocator::LocateConsoleWindow()->ApplyWindowOpacity(); + // end V2 console properties + + // Apply font information (must come before all character calculations for window/buffer size). + FontInfo fiNewFont(pStateInfo->FaceName, static_cast(pStateInfo->FontFamily), pStateInfo->FontWeight, pStateInfo->FontSize, pStateInfo->CodePage); + + ScreenInfo.UpdateFont(&fiNewFont); + + const FontInfo& fontApplied = ScreenInfo.GetCurrentFont(); + + // Now make sure internal font state reflects the font chosen + gci.SetFontFamily(fontApplied.GetFamily()); + gci.SetFontSize(fontApplied.GetUnscaledSize()); + gci.SetFontWeight(fontApplied.GetWeight()); + gci.SetFaceName(fontApplied.GetFaceName(), LF_FACESIZE); + + // Set the cursor properties in the Settings + const auto cursorType = static_cast(pStateInfo->CursorType); + gci.SetCursorColor(pStateInfo->CursorColor); + gci.SetCursorType(cursorType); + + // Then also apply them to the buffer's cursor + ScreenInfo.SetCursorInformation(pStateInfo->CursorSize, + ScreenInfo.GetTextBuffer().GetCursor().IsVisible()); + ScreenInfo.SetCursorColor(pStateInfo->CursorColor, true); + ScreenInfo.SetCursorType(cursorType, true); + + gci.SetTerminalScrolling(pStateInfo->TerminalScrolling); + + { + // Requested window in characters + COORD coordWindow = pStateInfo->WindowSize; + + // Requested buffer in characters. + COORD coordBuffer = pStateInfo->ScreenBufferSize; + + // First limit the window so it cannot be bigger than the monitor. + // Maximum number of characters we could fit on the given monitor. + COORD const coordLargest = ScreenInfo.GetLargestWindowSizeInCharacters(); + + coordWindow.X = std::min(coordLargest.X, coordWindow.X); + coordWindow.Y = std::min(coordLargest.Y, coordWindow.Y); + + if (gci.GetWrapText()) + { + // Then if wrap text is on, the buffer width gets fixed to the window width value. + coordBuffer.X = coordWindow.X; + + // However, we're not done. The "max window size" is if we had no scroll bar. + // We need to adjust slightly more if there's space reserved for a vertical scroll bar + // which happens when the buffer Y is taller than the window Y. + if (coordBuffer.Y > coordWindow.Y) + { + // Since we need a scroll bar in the Y direction, clamp the buffer width to make sure that + // it is leaving appropriate space for a scroll bar. + COORD const coordScrollBars = ScreenInfo.GetScrollBarSizesInCharacters(); + SHORT const sMaxBufferWidthWithScroll = coordLargest.X - coordScrollBars.X; + + coordBuffer.X = std::min(coordBuffer.X, sMaxBufferWidthWithScroll); + } + } + + // Now adjust the buffer size first to whatever we want it to be if it's different than before. + const COORD coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); + if (coordBuffer.X != coordScreenBufferSize.X || + coordBuffer.Y != coordScreenBufferSize.Y) + { + CommandLine* const pCommandLine = &CommandLine::Instance(); + + pCommandLine->Hide(FALSE); + + LOG_IF_FAILED(ScreenInfo.ResizeScreenBuffer(coordBuffer, TRUE)); + + pCommandLine->Show(); + } + + // Finally, restrict window size to the maximum possible size for the given buffer now that it's processed. + COORD const coordMaxForBuffer = ScreenInfo.GetMaxWindowSizeInCharacters(); + + coordWindow.X = std::min(coordWindow.X, coordMaxForBuffer.X); + coordWindow.Y = std::min(coordWindow.Y, coordMaxForBuffer.Y); + + // Then finish by updating the window. This will update the window size, + // as well as the screen buffer's viewport. + ServiceLocator::LocateConsoleWindow()->UpdateWindowSize(coordWindow); + } + + if (pStateInfo->QuickEdit) + { + gci.Flags |= CONSOLE_QUICK_EDIT_MODE; + } + else + { + gci.Flags &= ~CONSOLE_QUICK_EDIT_MODE; + } + + if (pStateInfo->AutoPosition) + { + gci.Flags |= CONSOLE_AUTO_POSITION; + } + else + { + gci.Flags &= ~CONSOLE_AUTO_POSITION; + + POINT pt; + pt.x = pStateInfo->WindowPosX; + pt.y = pStateInfo->WindowPosY; + + ServiceLocator::LocateConsoleWindow()->UpdateWindowPosition(pt); + } + + if (gci.GetInsertMode() != !!pStateInfo->InsertMode) + { + ScreenInfo.SetCursorDBMode(false); + gci.SetInsertMode(pStateInfo->InsertMode != FALSE); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().SetInsertMode(gci.GetInsertMode()); + } + } + + gci.SetColorTable(pStateInfo->ColorTable, gci.GetColorTableSize()); + + // Ensure that attributes only contain color specification. + WI_ClearAllFlags(pStateInfo->ScreenAttributes, ~(FG_ATTRS | BG_ATTRS)); + WI_ClearAllFlags(pStateInfo->PopupAttributes, ~(FG_ATTRS | BG_ATTRS)); + + // Place our new legacy fill attributes in gci + // (recall they are already persisted to the reg/link by the propsheet + // when it was closed) + gci.SetFillAttribute(pStateInfo->ScreenAttributes); + gci.SetPopupFillAttribute(pStateInfo->PopupAttributes); + // Store our updated Default Color values + gci.SetDefaultForegroundColor(pStateInfo->DefaultForeground); + gci.SetDefaultBackgroundColor(pStateInfo->DefaultBackground); + + // Set the screen info's default text attributes to defaults - + ScreenInfo.SetDefaultAttributes(gci.GetDefaultAttributes(), { gci.GetPopupFillAttribute() }); + + CommandHistory::s_ResizeAll(pStateInfo->HistoryBufferSize); + gci.SetNumberOfHistoryBuffers(pStateInfo->NumberOfHistoryBuffers); + if (pStateInfo->HistoryNoDup) + { + gci.Flags |= CONSOLE_HISTORY_NODUP; + } + else + { + gci.Flags &= ~CONSOLE_HISTORY_NODUP; + } + + // Since edit keys are global state only stored once in the registry, post the message to the queue to reload + // those properties specifically from the registry in case they were changed. + ServiceLocator::LocateConsoleWindow()->PostUpdateExtendedEditKeys(); + + gci.ConsoleIme.RefreshAreaAttributes(); + + gci.SetInterceptCopyPaste(!!pStateInfo->InterceptCopyPaste); + +} + +#pragma endregion diff --git a/src/interactivity/win32/menu.hpp b/src/interactivity/win32/menu.hpp new file mode 100644 index 000000000..ca7e323a0 --- /dev/null +++ b/src/interactivity/win32/menu.hpp @@ -0,0 +1,52 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- menu.h + +Abstract: +- This module contains the definitions for console system menu + +Author: +- Therese Stowell (ThereseS) Feb-3-1992 (swiped from Win3.1) + +Revision History: +--*/ + +#pragma once + +#include "precomp.h" + +#include "resource.h" + +namespace Microsoft::Console::Interactivity::Win32 +{ + class Menu sealed + { + public: + Menu(_In_ HMENU hMenu, + _In_ HMENU hHeirMenu); + [[nodiscard]] + static NTSTATUS CreateInstance(_In_ HWND hWnd); + static Menu* Instance(); + ~Menu(); + + void Initialize(); + + static void s_ShowPropertiesDialog(const HWND hwnd, const BOOL Defaults); + [[nodiscard]] + static HRESULT s_GetConsoleState(_Out_ CONSOLE_STATE_INFO * const pStateInfo); + + static HMENU s_GetMenuHandle(); + static HMENU s_GetHeirMenuHandle(); + + private: + static void s_PropertiesUpdate(_In_ PCONSOLE_STATE_INFO pStateInfo); + + static Menu* s_Instance; + + HMENU _hMenu; // handle to system menu + HMENU _hHeirMenu; // handle to menu we append to system menu + }; +} diff --git a/src/interactivity/win32/precomp.cpp b/src/interactivity/win32/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/interactivity/win32/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/interactivity/win32/precomp.h b/src/interactivity/win32/precomp.h new file mode 100644 index 000000000..c97586320 --- /dev/null +++ b/src/interactivity/win32/precomp.h @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "..\..\host\precomp.h" diff --git a/src/interactivity/win32/res.rc b/src/interactivity/win32/res.rc new file mode 100644 index 000000000..22152feaf --- /dev/null +++ b/src/interactivity/win32/res.rc @@ -0,0 +1,19 @@ +/****************************** Module Header ******************************\ +* Module Name: res.rc +* +* Copyright (c) 1985-91, Microsoft Corporation +* +* Constants +* +* History: +* 08-21-91 Created. +\***************************************************************************/ + +#include +#include "resource.h" + +#ifndef EXTERNAL_BUILD +#include "coninteractivitywin32.rcv" +#include +#include +#endif diff --git a/src/interactivity/win32/resource.h b/src/interactivity/win32/resource.h new file mode 100644 index 000000000..404591ea3 --- /dev/null +++ b/src/interactivity/win32/resource.h @@ -0,0 +1,47 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- resource.h + +Abstract: +- This file contains resource identifiers for menu popups. + +Author(s): +- Michael Niksa (MiNiksa) 14-Oct-2014 +- Paul Campbell (PaulCam) 14-Oct-2014 +--*/ + +#pragma once + +// IDs of various STRINGTABLE entries +#define ID_CONSOLE_MSGCMDLINEF2 0x1008 +#define ID_CONSOLE_MSGCMDLINEF4 0x1009 +#define ID_CONSOLE_MSGCMDLINEF9 0x100A +#define ID_CONSOLE_MSGSELECTMODE 0x100B +#define ID_CONSOLE_MSGMARKMODE 0x100C +#define ID_CONSOLE_MSGSCROLLMODE 0x100D +#define ID_CONSOLE_FMT_WINDOWTITLE 0x100E +#define ID_CONSOLE_WIP_DESTINATIONNAME 0x100F + +// Menu Item strings +#define ID_CONSOLE_COPY 0xFFF0 +#define ID_CONSOLE_PASTE 0xFFF1 +#define ID_CONSOLE_MARK 0xFFF2 +#define ID_CONSOLE_SCROLL 0xFFF3 +#define ID_CONSOLE_FIND 0xFFF4 +#define ID_CONSOLE_SELECTALL 0xFFF5 +#define ID_CONSOLE_EDIT 0xFFF6 +#define ID_CONSOLE_CONTROL 0xFFF7 +#define ID_CONSOLE_DEFAULTS 0xFFF8 + +// MENU IDs +#define ID_CONSOLE_SYSTEMMENU 500 + +// DIALOG IDs +#define ID_CONSOLE_FINDDLG 600 +#define ID_CONSOLE_FINDSTR 601 +#define ID_CONSOLE_FINDCASE 602 +#define ID_CONSOLE_FINDUP 603 +#define ID_CONSOLE_FINDDOWN 604 diff --git a/src/interactivity/win32/screenInfoUiaProvider.cpp b/src/interactivity/win32/screenInfoUiaProvider.cpp new file mode 100644 index 000000000..750b4a15e --- /dev/null +++ b/src/interactivity/win32/screenInfoUiaProvider.cpp @@ -0,0 +1,623 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "screenInfoUiaProvider.hpp" +#include "../../host/screenInfo.hpp" +#include "../inc/ServiceLocator.hpp" + +#include "windowUiaProvider.hpp" +#include "window.hpp" +#include "windowdpiapi.hpp" + +#include "UiaTextRange.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Interactivity::Win32::ScreenInfoUiaProviderTracing; + +// A helper function to create a SafeArray Version of an int array of a specified length +SAFEARRAY* BuildIntSafeArray(_In_reads_(length) const int* const data, const int length) +{ + SAFEARRAY *psa = SafeArrayCreateVector(VT_I4, 0, length); + if (psa != nullptr) + { + for (long i = 0; i < length; i++) + { + if (FAILED(SafeArrayPutElement(psa, &i, (void *)&(data[i])))) + { + SafeArrayDestroy(psa); + psa = nullptr; + break; + } + } + } + + return psa; +} + +ScreenInfoUiaProvider::ScreenInfoUiaProvider(_In_ WindowUiaProvider* const pUiaParent) : + _pUiaParent(THROW_HR_IF_NULL(E_INVALIDARG, pUiaParent)), + _signalFiringMapping{}, + _cRefs(1) +{ + Tracing::s_TraceUia(nullptr, ApiCall::Constructor, nullptr); +} + +ScreenInfoUiaProvider::~ScreenInfoUiaProvider() +{ +} + +[[nodiscard]] +HRESULT ScreenInfoUiaProvider::Signal(_In_ EVENTID id) +{ + HRESULT hr = S_OK; + // check to see if we're already firing this particular event + if (_signalFiringMapping.find(id) != _signalFiringMapping.end() && + _signalFiringMapping[id] == true) + { + return hr; + } + + try + { + _signalFiringMapping[id] = true; + } + CATCH_RETURN(); + + IRawElementProviderSimple* pProvider = static_cast(this); + hr = UiaRaiseAutomationEvent(pProvider, id); + _signalFiringMapping[id] = false; + + // tracing + ApiMsgSignal apiMsg; + apiMsg.Signal = id; + Tracing::s_TraceUia(this, ApiCall::Signal, &apiMsg); + return hr; +} + +#pragma region IUnknown + +IFACEMETHODIMP_(ULONG) ScreenInfoUiaProvider::AddRef() +{ + Tracing::s_TraceUia(this, ApiCall::AddRef, nullptr); + return InterlockedIncrement(&_cRefs); +} + +IFACEMETHODIMP_(ULONG) ScreenInfoUiaProvider::Release() +{ + Tracing::s_TraceUia(this, ApiCall::Release, nullptr); + long val = InterlockedDecrement(&_cRefs); + if (val == 0) + { + delete this; + } + return val; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::QueryInterface(_In_ REFIID riid, + _COM_Outptr_result_maybenull_ void** ppInterface) +{ + Tracing::s_TraceUia(this, ApiCall::QueryInterface, nullptr); + if (riid == __uuidof(IUnknown)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(IRawElementProviderSimple)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(IRawElementProviderFragment)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(ITextProvider)) + { + *ppInterface = static_cast(this); + } + else + { + *ppInterface = nullptr; + return E_NOINTERFACE; + } + + (static_cast(*ppInterface))->AddRef(); + + return S_OK; +} + +#pragma endregion + +#pragma region IRawElementProviderSimple + +// Implementation of IRawElementProviderSimple::get_ProviderOptions. +// Gets UI Automation provider options. +IFACEMETHODIMP ScreenInfoUiaProvider::get_ProviderOptions(_Out_ ProviderOptions* pOptions) +{ + Tracing::s_TraceUia(this, ApiCall::GetProviderOptions, nullptr); + *pOptions = ProviderOptions_ServerSideProvider; + return S_OK; +} + +// Implementation of IRawElementProviderSimple::get_PatternProvider. +// Gets the object that supports ISelectionPattern. +IFACEMETHODIMP ScreenInfoUiaProvider::GetPatternProvider(_In_ PATTERNID patternId, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface) +{ + Tracing::s_TraceUia(this, ApiCall::GetPatternProvider, nullptr); + + *ppInterface = nullptr; + HRESULT hr = S_OK; + + if (patternId == UIA_TextPatternId) + { + hr = this->QueryInterface(__uuidof(ITextProvider), reinterpret_cast(ppInterface)); + if (FAILED(hr)) + { + *ppInterface = nullptr; + } + } + return hr; +} + +// Implementation of IRawElementProviderSimple::get_PropertyValue. +// Gets custom properties. +IFACEMETHODIMP ScreenInfoUiaProvider::GetPropertyValue(_In_ PROPERTYID propertyId, + _Out_ VARIANT* pVariant) +{ + Tracing::s_TraceUia(this, ApiCall::GetPropertyValue, nullptr); + + pVariant->vt = VT_EMPTY; + + // Returning the default will leave the property as the default + // so we only really need to touch it for the properties we want to implement + if (propertyId == UIA_ControlTypePropertyId) + { + // This control is the Document control type, implying that it is + // a complex document that supports text pattern + pVariant->vt = VT_I4; + pVariant->lVal = UIA_DocumentControlTypeId; + } + else if (propertyId == UIA_NamePropertyId) + { + // TODO: MSFT: 7960168 - These strings should be localized text in the final UIA work + pVariant->bstrVal = SysAllocString(L"Text Area"); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + } + else if (propertyId == UIA_AutomationIdPropertyId) + { + pVariant->bstrVal = SysAllocString(L"Text Area"); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + } + else if (propertyId == UIA_IsControlElementPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_IsContentElementPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_IsKeyboardFocusablePropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_HasKeyboardFocusPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_ProviderDescriptionPropertyId) + { + pVariant->bstrVal = SysAllocString(L"Microsoft Console Host: Screen Information Text Area"); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + } + else if (propertyId == UIA_IsEnabledPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::GetHostRawElementProvider, nullptr); + *ppProvider = nullptr; + + return S_OK; +} +#pragma endregion + +#pragma region IRawElementProviderFragment + +IFACEMETHODIMP ScreenInfoUiaProvider::Navigate(_In_ NavigateDirection direction, + _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) +{ + ApiMsgNavigate apiMsg; + apiMsg.Direction = direction; + Tracing::s_TraceUia(this, ApiCall::Navigate, &apiMsg); + *ppProvider = nullptr; + + if (direction == NavigateDirection_Parent) + { + try + { + _pUiaParent->QueryInterface(IID_PPV_ARGS(ppProvider)); + } + catch (...) + { + *ppProvider = nullptr; + return wil::ResultFromCaughtException(); + } + RETURN_IF_NULL_ALLOC(*ppProvider); + } + + // For the other directions the default of nullptr is correct + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId) +{ + Tracing::s_TraceUia(this, ApiCall::GetRuntimeId, nullptr); + // Root defers this to host, others must implement it... + *ppRuntimeId = nullptr; + + // AppendRuntimeId is a magic Number that tells UIAutomation to Append its own Runtime ID(From the HWND) + int rId[] = { UiaAppendRuntimeId, -1 }; + // BuildIntSafeArray is a custom function to hide the SafeArray creation + *ppRuntimeId = BuildIntSafeArray(rId, 2); + RETURN_IF_NULL_ALLOC(*ppRuntimeId); + + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::get_BoundingRectangle(_Out_ UiaRect* pRect) +{ + Tracing::s_TraceUia(this, ApiCall::GetBoundingRectangle, nullptr); + const IConsoleWindow* const pIConsoleWindow = _getIConsoleWindow(); + RETURN_HR_IF_NULL((HRESULT)UIA_E_ELEMENTNOTAVAILABLE, pIConsoleWindow); + + RECT rc = pIConsoleWindow->GetWindowRect(); + + pRect->left = rc.left; + pRect->top = rc.top; + pRect->width = rc.right - rc.left; + pRect->height = rc.bottom - rc.top; + + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots) +{ + Tracing::s_TraceUia(this, ApiCall::GetEmbeddedFragmentRoots, nullptr); + *ppRoots = nullptr; + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::SetFocus() +{ + Tracing::s_TraceUia(this, ApiCall::SetFocus, nullptr); + return Signal(UIA_AutomationFocusChangedEventId); +} + +IFACEMETHODIMP ScreenInfoUiaProvider::get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::GetFragmentRoot, nullptr); + try + { + _pUiaParent->QueryInterface(IID_PPV_ARGS(ppProvider)); + } + catch (...) + { + *ppProvider = nullptr; + return wil::ResultFromCaughtException(); + } + RETURN_IF_NULL_ALLOC(*ppProvider); + return S_OK; +} + +#pragma endregion + +#pragma region ITextProvider + +IFACEMETHODIMP ScreenInfoUiaProvider::GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + ApiMsgGetSelection apiMsg; + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + *ppRetVal = nullptr; + HRESULT hr = S_OK; + + if (!Selection::Instance().IsAreaSelected()) + { + apiMsg.AreaSelected = false; + apiMsg.SelectionRowCount = 1; + // return a degenerate range at the cursor position + SCREEN_INFORMATION& screenInfo = _getScreenInfo(); + const Cursor& cursor = screenInfo.GetTextBuffer().GetCursor(); + + // make a safe array + *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 1); + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + + IRawElementProviderSimple* pProvider; + hr = this->QueryInterface(IID_PPV_ARGS(&pProvider)); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + + UiaTextRange* range; + try + { + range = UiaTextRange::Create(pProvider, + cursor); + } + catch (...) + { + range = nullptr; + hr = wil::ResultFromCaughtException(); + } + (static_cast(pProvider))->Release(); + if (range == nullptr) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + + LONG currentIndex = 0; + hr = SafeArrayPutElement(*ppRetVal, ¤tIndex, reinterpret_cast(range)); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + } + else + { + // get the selection ranges + std::deque ranges; + IRawElementProviderSimple* pProvider; + RETURN_IF_FAILED(QueryInterface(IID_PPV_ARGS(&pProvider))); + try + { + ranges = UiaTextRange::GetSelectionRanges(pProvider); + } + catch (...) + { + hr = wil::ResultFromCaughtException(); + } + pProvider->Release(); + RETURN_IF_FAILED(hr); + + apiMsg.AreaSelected = true; + apiMsg.SelectionRowCount = static_cast(ranges.size()); + + // make a safe array + *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, static_cast(ranges.size())); + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + + // fill the safe array + for (LONG i = 0; i < static_cast(ranges.size()); ++i) + { + hr = SafeArrayPutElement(*ppRetVal, &i, reinterpret_cast(ranges[i])); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + while (!ranges.empty()) + { + UiaTextRange* pRange = ranges[0]; + ranges.pop_front(); + pRange->Release(); + } + return hr; + } + } + } + + Tracing::s_TraceUia(this, ApiCall::GetSelection, &apiMsg); + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::GetVisibleRanges(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetVisibleRanges, nullptr); + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + gci.LockConsole(); + auto Unlock = wil::scope_exit([&] + { + gci.UnlockConsole(); + }); + + const SCREEN_INFORMATION& screenInfo = _getScreenInfo(); + const auto viewport = screenInfo.GetViewport(); + const COORD screenBufferCoords = _getScreenBufferCoords(); + const int totalLines = screenBufferCoords.Y; + + // make a safe array + const size_t rowCount = viewport.Height(); + *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, static_cast(rowCount)); + if (*ppRetVal == nullptr) + { + return E_OUTOFMEMORY; + } + + // stuff each visible line in the safearray + for (size_t i = 0; i < rowCount; ++i) + { + const int lineNumber = (viewport.Top() + i) % totalLines; + const int start = lineNumber * screenBufferCoords.X; + // - 1 to get the last column in the row + const int end = start + screenBufferCoords.X - 1; + + IRawElementProviderSimple* pProvider; + HRESULT hr = this->QueryInterface(IID_PPV_ARGS(&pProvider)); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + + UiaTextRange* range; + try + { + range = UiaTextRange::Create(pProvider, + start, + end, + false); + } + catch (...) + { + range = nullptr; + hr = wil::ResultFromCaughtException(); + } + (static_cast(pProvider))->Release(); + + if (range == nullptr) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + + LONG currentIndex = static_cast(i); + hr = SafeArrayPutElement(*ppRetVal, ¤tIndex, reinterpret_cast(range)); + if (FAILED(hr)) + { + SafeArrayDestroy(*ppRetVal); + *ppRetVal = nullptr; + return hr; + } + } + return S_OK; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::RangeFromChild(_In_ IRawElementProviderSimple* /*childElement*/, + _COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::RangeFromChild, nullptr); + + IRawElementProviderSimple* pProvider; + RETURN_IF_FAILED(this->QueryInterface(IID_PPV_ARGS(&pProvider))); + + HRESULT hr = S_OK;; + try + { + *ppRetVal = UiaTextRange::Create(pProvider); + } + catch (...) + { + *ppRetVal = nullptr; + hr = wil::ResultFromCaughtException(); + } + (static_cast(pProvider))->Release(); + + return hr; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::RangeFromPoint(_In_ UiaPoint point, + _COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::RangeFromPoint, nullptr); + IRawElementProviderSimple* pProvider; + RETURN_IF_FAILED(this->QueryInterface(IID_PPV_ARGS(&pProvider))); + + HRESULT hr = S_OK; + try + { + *ppRetVal = UiaTextRange::Create(pProvider, + point); + } + catch(...) + { + *ppRetVal = nullptr; + hr = wil::ResultFromCaughtException(); + } + (static_cast(pProvider))->Release(); + + return hr; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::get_DocumentRange(_COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetDocumentRange, nullptr); + IRawElementProviderSimple* pProvider; + RETURN_IF_FAILED(this->QueryInterface(IID_PPV_ARGS(&pProvider))); + + HRESULT hr = S_OK; + try + { + *ppRetVal = UiaTextRange::Create(pProvider); + } + catch (...) + { + *ppRetVal = nullptr; + hr = wil::ResultFromCaughtException(); + } + (static_cast(pProvider))->Release(); + + if (*ppRetVal) + { + (*ppRetVal)->ExpandToEnclosingUnit(TextUnit::TextUnit_Document); + } + + return hr; +} + +IFACEMETHODIMP ScreenInfoUiaProvider::get_SupportedTextSelection(_Out_ SupportedTextSelection* pRetVal) +{ + Tracing::s_TraceUia(this, ApiCall::GetSupportedTextSelection, nullptr); + *pRetVal = SupportedTextSelection::SupportedTextSelection_Single; + return S_OK; +} + +#pragma endregion + +const COORD ScreenInfoUiaProvider::_getScreenBufferCoords() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetScreenBufferSize(); +} + +SCREEN_INFORMATION& ScreenInfoUiaProvider::_getScreenInfo() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + THROW_HR_IF(E_POINTER, !gci.HasActiveOutputBuffer()); + return gci.GetActiveOutputBuffer(); +} + +IConsoleWindow* const ScreenInfoUiaProvider::_getIConsoleWindow() +{ + return ServiceLocator::LocateConsoleWindow(); +} diff --git a/src/interactivity/win32/screenInfoUiaProvider.hpp b/src/interactivity/win32/screenInfoUiaProvider.hpp new file mode 100644 index 000000000..08bb5060b --- /dev/null +++ b/src/interactivity/win32/screenInfoUiaProvider.hpp @@ -0,0 +1,150 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- screenInfoUiaProvider.hpp + +Abstract: +- This module provides UI Automation access to the screen buffer to + support both automation tests and accessibility (screen reading) + applications. +- Based on examples, sample code, and guidance from + https://msdn.microsoft.com/en-us/library/windows/desktop/ee671596(v=vs.85).aspx + +Author(s): +- Michael Niksa (MiNiksa) 2017 +- Austin Diviness (AustDi) 2017 +--*/ + +#pragma once + +#include "precomp.h" + +// Forward declare, prevent circular ref. +class SCREEN_INFORMATION; + +namespace Microsoft::Console::Interactivity::Win32 +{ + class Window; + + class WindowUiaProvider; + + class ScreenInfoUiaProvider final : public IRawElementProviderSimple, + public IRawElementProviderFragment, + public ITextProvider + { + public: + ScreenInfoUiaProvider(_In_ WindowUiaProvider* const pUiaParent); + virtual ~ScreenInfoUiaProvider(); + + [[nodiscard]] + HRESULT Signal(_In_ EVENTID id); + + // IUnknown methods + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, + _COM_Outptr_result_maybenull_ void** ppInterface); + + // IRawElementProviderSimple methods + IFACEMETHODIMP get_ProviderOptions(_Out_ ProviderOptions* pOptions); + IFACEMETHODIMP GetPatternProvider(_In_ PATTERNID iid, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface); + IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID idProp, + _Out_ VARIANT* pVariant); + IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider); + + // IRawElementProviderFragment methods + IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, + _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider); + IFACEMETHODIMP GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId); + IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect); + IFACEMETHODIMP GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots); + IFACEMETHODIMP SetFocus(); + IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider); + + // ITextProvider + IFACEMETHODIMP GetSelection(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal); + IFACEMETHODIMP GetVisibleRanges(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal); + IFACEMETHODIMP RangeFromChild(_In_ IRawElementProviderSimple* childElement, + _COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP RangeFromPoint(_In_ UiaPoint point, + _COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP get_DocumentRange(_COM_Outptr_result_maybenull_ ITextRangeProvider** ppRetVal); + IFACEMETHODIMP get_SupportedTextSelection(_Out_ SupportedTextSelection* pRetVal); + + private: + + // Ref counter for COM object + ULONG _cRefs; + + // weak reference to uia parent + WindowUiaProvider* const _pUiaParent; + + // this is used to prevent the object from + // signaling an event while it is already in the + // process of signalling another event. + // This fixes a problem with JAWS where it would + // call a public method that calls + // UiaRaiseAutomationEvent to signal something + // happened, which JAWS then detects the signal + // and calls the same method in response, + // eventually overflowing the stack. + // We aren't using this as a cheap locking + // mechanism for multi-threaded code. + std::map _signalFiringMapping; + + const COORD _getScreenBufferCoords() const; + static SCREEN_INFORMATION& _getScreenInfo(); + static IConsoleWindow* const _getIConsoleWindow(); + }; + + namespace ScreenInfoUiaProviderTracing + { + enum class ApiCall + { + Constructor, + Signal, + AddRef, + Release, + QueryInterface, + GetProviderOptions, + GetPatternProvider, + GetPropertyValue, + GetHostRawElementProvider, + Navigate, + GetRuntimeId, + GetBoundingRectangle, + GetEmbeddedFragmentRoots, + SetFocus, + GetFragmentRoot, + GetSelection, + GetVisibleRanges, + RangeFromChild, + RangeFromPoint, + GetDocumentRange, + GetSupportedTextSelection + }; + + struct IApiMsg + { + }; + + struct ApiMsgSignal : public IApiMsg + { + EVENTID Signal; + }; + + struct ApiMsgNavigate : public IApiMsg + { + NavigateDirection Direction; + }; + + struct ApiMsgGetSelection : public IApiMsg + { + bool AreaSelected; + unsigned int SelectionRowCount; + }; + } +} diff --git a/src/interactivity/win32/sources.inc b/src/interactivity/win32/sources.inc new file mode 100644 index 000000000..5b226c76c --- /dev/null +++ b/src/interactivity/win32/sources.inc @@ -0,0 +1,67 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Interactivity for Win32 +# ------------------------------------- + +# This module provides user interaction with the standard +# windowing and input system used by classic Win32 platforms. + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DFE_SB + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\AccessibilityNotifier.cpp \ + ..\clipboard.cpp \ + ..\ConsoleControl.cpp \ + ..\ConsoleInputThread.cpp \ + ..\consoleKeyInfo.cpp \ + ..\find.cpp \ + ..\icon.cpp \ + ..\InputServices.cpp \ + ..\menu.cpp \ + ..\screenInfoUiaProvider.cpp \ + ..\SystemConfigurationProvider.cpp \ + ..\UiaTextRange.cpp \ + ..\window.cpp \ + ..\windowdpiapi.cpp \ + ..\windowime.cpp \ + ..\windowio.cpp \ + ..\WindowMetrics.cpp \ + ..\windowproc.cpp \ + ..\windowtheme.cpp \ + ..\windowUiaProvider.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + +TARGETLIBS = \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ diff --git a/src/interactivity/win32/ut_interactivity_win32/DefaultResource.rc b/src/interactivity/win32/ut_interactivity_win32/DefaultResource.rc new file mode 100644 index 000000000..85ec2648d --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/DefaultResource.rc @@ -0,0 +1,12 @@ +//Autogenerated file name + version resource file for Device Guard whitelisting effort + +#include +#include + +#define VER_FILETYPE VFT_UNKNOWN +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR ___TARGETNAME +#define VER_INTERNALNAME_STR ___TARGETNAME +#define VER_ORIGINALFILENAME_STR ___TARGETNAME + +#include "common.ver" diff --git a/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj new file mode 100644 index 000000000..ef2eb2419 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj @@ -0,0 +1,76 @@ + + + + + + + Create + + + + + {0cf235bd-2da0-407e-90ee-c467e8bbc714} + + + {ef3e32a7-5ff6-42b4-b6e2-96cd7d033f00} + + + {48d21369-3d7b-4431-9967-24e81292cf62} + + + {990f2657-8580-4828-943f-5dd657d11842} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {1c959542-bac2-4e55-9a6d-13251914cbb9} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + + + + + {d3b92829-26cb-411a-bda2-7f5da3d25dd4} + Win32Proj + InteractivityWin32UnitTests + Interactivity.Win32.Tests.Unit + Conhost.Interactivity.Win32.Unit.Tests + + + + ..;$(SolutionDir)src\inc;$(Solutiondir)src\inc\test;%(AdditionalIncludeDirectories) + + + + + + + \ No newline at end of file diff --git a/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj.filters b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj.filters new file mode 100644 index 000000000..b5674fcac --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/Interactivity.Win32.UnitTests.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + + + + \ No newline at end of file diff --git a/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp new file mode 100644 index 000000000..6f9a21f48 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/UiaTextRangeTests.cpp @@ -0,0 +1,1159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "..\..\inc\consoletaeftemplates.hpp" +#include "CommonState.hpp" + +#include "UiaTextRange.hpp" +#include "../../../buffer/out/textBuffer.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +using namespace Microsoft::Console::Interactivity::Win32; + + +// UiaTextRange takes an object that implements +// IRawElementProviderSimple as a constructor argument. Making a real +// one would involve setting up the window which we don't want to do +// for unit tests so instead we'll use this one. We don't care about +// it not doing anything for its implementation because it is not used +// during the unit tests below. +class DummyElementProvider final : public IRawElementProviderSimple +{ +public: + // IUnknown methods + IFACEMETHODIMP_(ULONG) AddRef() { return 1; } + IFACEMETHODIMP_(ULONG) Release() { return 1; } + IFACEMETHODIMP QueryInterface(_In_ REFIID /*riid*/, + _COM_Outptr_result_maybenull_ void** /*ppInterface*/) + { + return E_NOTIMPL; + }; + + // IRawElementProviderSimple methods + IFACEMETHODIMP get_ProviderOptions(_Out_ ProviderOptions* /*pOptions*/) + { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetPatternProvider(_In_ PATTERNID /*iid*/, + _COM_Outptr_result_maybenull_ IUnknown** /*ppInterface*/) + { + return E_NOTIMPL; + } + + IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID /*idProp*/, + _Out_ VARIANT* /*pVariant*/) + { + return E_NOTIMPL; + } + + IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** /*ppProvider*/) + { + return E_NOTIMPL; + } +}; + + +class UiaTextRangeTests +{ + TEST_CLASS(UiaTextRangeTests); + + CommonState* _state; + DummyElementProvider _dummyProvider; + SCREEN_INFORMATION* _pScreenInfo; + TextBuffer* _pTextBuffer; + UiaTextRange* _range; + + TEST_METHOD_SETUP(MethodSetup) + { + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // set up common state + _state = new CommonState(); + _state->PrepareGlobalFont(); + _state->PrepareGlobalScreenBuffer(); + _state->PrepareNewTextBufferInfo(); + + // set up pointers + _pScreenInfo = &gci.GetActiveOutputBuffer(); + _pTextBuffer = &_pScreenInfo->GetTextBuffer(); + + // fill text buffer with text + for (UINT i = 0; i < _pTextBuffer->TotalRowCount(); ++i) + { + ROW& row = _pTextBuffer->GetRowByOffset(i); + auto& charRow = row.GetCharRow(); + for (auto& cell : charRow) + { + cell.Char() = L'a'; + } + } + + // set up default range + _range = new UiaTextRange + { + &_dummyProvider, + 0, + 0, + false + }; + + return true; + } + + TEST_METHOD_CLEANUP(MethodCleanup) + { + _state->CleanupNewTextBufferInfo(); + _state->CleanupGlobalScreenBuffer(); + _state->CleanupGlobalFont(); + delete _state; + delete _range; + + _pScreenInfo = nullptr; + _pTextBuffer = nullptr; + return true; + } + + const size_t _getRowWidth() const + { + const CharRow& charRow = _pTextBuffer->_GetFirstRow().GetCharRow(); + return charRow.MeasureRight()- charRow.MeasureLeft() ; + } + + TEST_METHOD(DegenerateRangesDetected) + { + // make a degenerate range and verify that it reports degenerate + UiaTextRange degenerate + { + &_dummyProvider, + 20, + 19, + true + }; + VERIFY_IS_TRUE(degenerate.IsDegenerate()); + VERIFY_ARE_EQUAL(0u, degenerate._rowCountInRange()); + VERIFY_ARE_EQUAL(degenerate._start, degenerate._end); + + // make a non-degenerate range and verify that it reports as such + UiaTextRange notDegenerate1 + { + &_dummyProvider, + 20, + 20, + false + }; + VERIFY_IS_FALSE(notDegenerate1.IsDegenerate()); + VERIFY_ARE_EQUAL(1u, notDegenerate1._rowCountInRange()); + } + + TEST_METHOD(CanCheckIfScreenInfoRowIsInViewport) + { + // check a viewport that's one line tall + SMALL_RECT viewport; + viewport.Top = 0; + viewport.Bottom = 0; + + VERIFY_IS_TRUE(_range->_isScreenInfoRowInViewport(0, viewport)); + VERIFY_IS_FALSE(_range->_isScreenInfoRowInViewport(1, viewport)); + + // check a slightly larger viewport + viewport.Bottom = 5; + for (auto i = 0; i <= viewport.Bottom; ++i) + { + VERIFY_IS_TRUE(_range->_isScreenInfoRowInViewport(i, viewport), + NoThrowString().Format(L"%d should be in viewport", i)); + } + VERIFY_IS_FALSE(_range->_isScreenInfoRowInViewport(viewport.Bottom + 1, viewport)); + } + + TEST_METHOD(CanTranslateScreenInfoRowToViewport) + { + const int totalRows = _pTextBuffer->TotalRowCount(); + + SMALL_RECT viewport; + viewport.Top = 0; + viewport.Bottom = 10; + + std::vector> viewportSizes = + { + { 0, 10 }, // viewport at top + { 2, 10 }, // shifted viewport + { totalRows - 5, totalRows + 3 } // viewport with 0th row + }; + + for (auto it = viewportSizes.begin(); it != viewportSizes.end(); ++it) + { + viewport.Top = static_cast(it->first); + viewport.Bottom = static_cast(it->second); + for (int i = viewport.Top; _range->_isScreenInfoRowInViewport(i, viewport); ++i) + { + VERIFY_ARE_EQUAL(i - viewport.Top, _range->_screenInfoRowToViewportRow(i, viewport)); + } + } + + // ScreenInfoRows that are above the viewport return a + // negative value + viewport.Top = 5; + viewport.Bottom = 10; + + VERIFY_ARE_EQUAL(-1, _range->_screenInfoRowToViewportRow(4, viewport)); + VERIFY_ARE_EQUAL(-2, _range->_screenInfoRowToViewportRow(3, viewport)); + } + + TEST_METHOD(CanTranslateEndpointToTextBufferRow) + { + const auto rowWidth = _getRowWidth(); + for (auto i = 0; i < 300; ++i) + { + VERIFY_ARE_EQUAL(i / rowWidth, _range->_endpointToTextBufferRow(i)); + } + } + + TEST_METHOD(CanTranslateTextBufferRowToEndpoint) + { + const auto rowWidth = _getRowWidth(); + for (unsigned int i = 0; i < 5; ++i) + { + VERIFY_ARE_EQUAL(i * rowWidth, _range->_textBufferRowToEndpoint(i)); + // make sure that the translation is reversible + VERIFY_ARE_EQUAL(i , _range->_endpointToTextBufferRow(_range->_textBufferRowToEndpoint(i))); + } + } + + TEST_METHOD(CanTranslateTextBufferRowToScreenInfoRow) + { + const auto rowWidth = _getRowWidth(); + for (unsigned int i = 0; i < 5; ++i) + { + VERIFY_ARE_EQUAL(i , _range->_textBufferRowToScreenInfoRow(_range->_screenInfoRowToTextBufferRow(i))); + } + } + + TEST_METHOD(CanTranslateEndpointToColumn) + { + const auto rowWidth = _getRowWidth(); + for (auto i = 0; i < 300; ++i) + { + const auto column = i % rowWidth; + VERIFY_ARE_EQUAL(column, _range->_endpointToColumn(i)); + } + } + + TEST_METHOD(CanGetTotalRows) + { + const auto totalRows = _pTextBuffer->TotalRowCount(); + VERIFY_ARE_EQUAL(totalRows, + _range->_getTotalRows()); + } + + TEST_METHOD(CanGetRowWidth) + { + const auto rowWidth = _getRowWidth(); + VERIFY_ARE_EQUAL(rowWidth, _range->_getRowWidth()); + } + + TEST_METHOD(CanNormalizeRow) + { + const int totalRows = _pTextBuffer->TotalRowCount(); + std::vector> rowMappings = + { + { 0, 0 }, + { totalRows / 2, totalRows / 2 }, + { totalRows - 1, totalRows - 1 }, + { totalRows, 0 }, + { totalRows + 1, 1 }, + { -1, totalRows - 1} + }; + + for (auto it = rowMappings.begin(); it != rowMappings.end(); ++it) + { + VERIFY_ARE_EQUAL(static_cast(it->second), _range->_normalizeRow(it->first)); + } + } + + TEST_METHOD(CanGetViewportHeight) + { + SMALL_RECT viewport; + viewport.Top = 0; + viewport.Bottom = 0; + + // Viewports are inclusive, so Top == Bottom really means 1 row + VERIFY_ARE_EQUAL(1u, _range->_getViewportHeight(viewport)); + + // make the viewport 10 rows tall + viewport.Top = 3; + viewport.Bottom = 12; + VERIFY_ARE_EQUAL(10u, _range->_getViewportHeight(viewport)); + } + + TEST_METHOD(CanGetViewportWidth) + { + SMALL_RECT viewport; + viewport.Left = 0; + viewport.Right = 0; + + // Viewports are inclusive, Left == Right is really 1 column + VERIFY_ARE_EQUAL(1u, _range->_getViewportWidth(viewport)); + + // test a more normal size + viewport.Right = 300; + VERIFY_ARE_EQUAL(viewport.Right + 1u, _range->_getViewportWidth(viewport)); + } + + TEST_METHOD(CanCompareScreenCoords) + { + const std::vector> testData = + { + { 0, 0, 0, 0, 0 }, + { 5, 0, 5, 0, 0 }, + { 2, 3, 2, 3, 0 }, + { 0, 6, 0, 6, 0 }, + { 1, 5, 2, 5, -1 }, + { 5, 4, 7, 3, -1 }, + { 3, 4, 3, 5, -1 }, + { 2, 0, 1, 9, 1 }, + { 4, 5, 4, 3, 1 } + }; + + for (auto data : testData) + { + VERIFY_ARE_EQUAL(std::get<4>(data), UiaTextRange::_compareScreenCoords(std::get<0>(data), + std::get<1>(data), + std::get<2>(data), + std::get<3>(data))); + } + } + + TEST_METHOD(CanMoveByCharacter) + { + const Column firstColumnIndex = 0; + const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const ScreenInfoRow topRow = 0; + const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + + const std::vector> testData = + { + { + L"can't move backward from (0, 0)", + { + 0, 0, + 0, 2, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + 0, + 0u, + 0u + }, + + { + L"can move backward within a row", + { + 0, 1, + 0, 2, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + 0u, + 0u + }, + + { + L"can move forward in a row", + { + 2, 1, + 4, 5, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 5, + 5, + UiaTextRange::_screenInfoRowToEndpoint(2) + 6, + UiaTextRange::_screenInfoRowToEndpoint(2) + 6 + }, + + { + L"can't move past the last column in the last row", + { + bottomRow, lastColumnIndex, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 5, + 0, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex + }, + + { + L"can move to a new row when necessary when moving forward", + { + topRow, lastColumnIndex, + topRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 5, + 5, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 1) + 4, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 1) + 4 + }, + + { + L"can move to a new row when necessary when moving backward", + { + topRow + 1, firstColumnIndex, + topRow + 1, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -5, + -5, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + (lastColumnIndex - 4), + UiaTextRange::_screenInfoRowToEndpoint(topRow) + (lastColumnIndex - 4) + } + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + int amountMoved; + std::pair newEndpoints = UiaTextRange::_moveByCharacter(std::get<2>(data), + std::get<1>(data), + &amountMoved); + + VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); + VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); + VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); + + } + } + + TEST_METHOD(CanMoveByLine) + { + const Column firstColumnIndex = 0; + const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const ScreenInfoRow topRow = 0; + const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + + const std::vector> testData = + { + { + L"can't move backward from top row", + { + topRow, firstColumnIndex, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -4, + 0, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex + }, + + { + L"can move forward from top row", + { + topRow, firstColumnIndex, + topRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 4, + 4, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 4) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 4) + lastColumnIndex + }, + + { + L"can't move forward from bottom row", + { + bottomRow, firstColumnIndex, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 3, + 0, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex + }, + + { + L"can move backward from bottom row", + { + bottomRow, firstColumnIndex, + bottomRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -3, + -3, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow - 3) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow - 3) + lastColumnIndex + }, + + { + L"can't move backward when part of the top row is in the range", + { + topRow, firstColumnIndex + 5, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + 0, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex + 5, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex + }, + + { + L"can't move forward when part of the bottom row is in the range", + { + bottomRow, firstColumnIndex, + bottomRow, firstColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 0, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + firstColumnIndex + } + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + int amountMoved; + std::pair newEndpoints = UiaTextRange::_moveByLine(std::get<2>(data), + std::get<1>(data), + &amountMoved); + + VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); + VERIFY_ARE_EQUAL(std::get<4>(data), newEndpoints.first); + VERIFY_ARE_EQUAL(std::get<5>(data), newEndpoints.second); + } + } + + TEST_METHOD(CanMoveEndpointByUnitCharacter) + { + const Column firstColumnIndex = 0; + const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const ScreenInfoRow topRow = 0; + const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + + const std::vector> testData = + { + { + L"can't move _start past the beginning of the document when _start is positioned at the beginning", + { + topRow, firstColumnIndex, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex, + false + }, + + { + L"can partially move _start to the begining of the document when it is closer than the move count requested", + { + topRow, firstColumnIndex + 3, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -5, + -3, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex, + false + }, + + { + L"can't move _end past the begining of the document", + { + topRow, firstColumnIndex, + topRow, firstColumnIndex + 4, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -5, + -4, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + false + }, + + { + L"_start follows _end when passed during movement", + { + topRow, firstColumnIndex + 5, + topRow, firstColumnIndex + 10, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -7, + -7, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + 3, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + 3, + true + }, + + { + L"can't move _end past the beginning of the document when _end is positioned at the end", + { + bottomRow, firstColumnIndex, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"can partially move _end to the end of the document when it is closer than the move count requested", + { + topRow, firstColumnIndex, + bottomRow, lastColumnIndex - 3, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 5, + 3, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"can't move _start past the end of the document", + { + bottomRow, lastColumnIndex - 4, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 5, + 4, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"_end follows _start when passed during movement", + { + topRow, firstColumnIndex + 5, + topRow, firstColumnIndex + 10, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 7, + 7, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + 12, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + 12, + true + }, + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + std::tuple result; + int amountMoved; + result = UiaTextRange::_moveEndpointByUnitCharacter(std::get<2>(data), + std::get<4>(data), + std::get<1>(data), + &amountMoved); + + VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); + VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); + VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); + VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + } + } + + TEST_METHOD(CanMoveEndpointByUnitLine) + { + const Column firstColumnIndex = 0; + const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const ScreenInfoRow topRow = 0; + const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + + const std::vector> testData = + { + { + L"can move _end forward without affecting _start", + { + topRow, firstColumnIndex, + topRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 1) + lastColumnIndex, + false + }, + + { + L"can move _end backward without affecting _start", + { + topRow + 1, firstColumnIndex, + topRow + 5, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -2, + -2, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 1) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 3) + lastColumnIndex, + false + }, + + { + L"can move _start forward without affecting _end", + { + topRow + 1, firstColumnIndex, + topRow + 5, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 2, + 2, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 3) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 5) + lastColumnIndex, + false + }, + + { + L"can move _start backward without affecting _end", + { + topRow + 2, firstColumnIndex, + topRow + 5, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 1) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 5) + lastColumnIndex, + false + }, + + { + L"can move _start backwards when it's already on the top row", + { + topRow, lastColumnIndex, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex, + false + }, + + { + L"can't move _start backwards when it's at the start of the document already", + { + topRow, firstColumnIndex, + topRow, lastColumnIndex, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + lastColumnIndex, + false + }, + + { + L"can move _end forwards when it's on the bottom row", + { + topRow, firstColumnIndex, + bottomRow, lastColumnIndex - 3, + bottomRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"can't move _end forwards when it's at the end of the document already", + { + topRow, firstColumnIndex, + bottomRow, lastColumnIndex, + bottomRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"moving _start forward when it's already on the bottom row creates a degenerate range at the document end", + { + bottomRow, firstColumnIndex, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + true + }, + + { + L"moving _end backward when it's already on the top row creates a degenerate range at the document start", + { + topRow, firstColumnIndex + 4, + topRow, lastColumnIndex - 5, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + true + } + + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + std::tuple result; + int amountMoved; + result = UiaTextRange::_moveEndpointByUnitLine(std::get<2>(data), + std::get<4>(data), + std::get<1>(data), + &amountMoved); + + VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); + VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); + VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); + VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + } + + + } + + TEST_METHOD(CanMoveEndpointByUnitDocument) + { + const Column firstColumnIndex = 0; + const Column lastColumnIndex = _pScreenInfo->GetBufferSize().Width() - 1; + const ScreenInfoRow topRow = 0; + const ScreenInfoRow bottomRow = _pTextBuffer->TotalRowCount() - 1; + + const std::vector> testData = + { + { + L"can move _end forward to end of document without affecting _start", + { + topRow, firstColumnIndex + 4, + topRow, firstColumnIndex + 4, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex + 4, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"can move _start backward to end of document without affect _end", + { + topRow, firstColumnIndex + 4, + topRow, firstColumnIndex + 4, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + 4, + false + }, + + { + L"can't move _end forward when it's already at the end of the document", + { + topRow + 3, firstColumnIndex + 2, + bottomRow, lastColumnIndex, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 3) + firstColumnIndex + 2, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + false + }, + + { + L"can't move _start backward when it's already at the start of the document", + { + topRow, firstColumnIndex, + topRow + 5, firstColumnIndex + 6, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + 0, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow + 5) + 6, + false + }, + + { + L"moving _end backward creates degenerate range at start of document", + { + topRow + 5, firstColumnIndex + 2, + topRow + 5, firstColumnIndex + 6, + topRow, + lastColumnIndex, + firstColumnIndex, + UiaTextRange::MovementIncrement::Backward, + UiaTextRange::MovementDirection::Backward + }, + -1, + -1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_End, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(topRow) + firstColumnIndex, + true + }, + + { + L"moving _start forward creates degenerate range at end of document", + { + topRow + 5, firstColumnIndex + 2, + topRow + 5, firstColumnIndex + 6, + bottomRow, + firstColumnIndex, + lastColumnIndex, + UiaTextRange::MovementIncrement::Forward, + UiaTextRange::MovementDirection::Forward + }, + 1, + 1, + TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + UiaTextRange::_screenInfoRowToEndpoint(bottomRow) + lastColumnIndex, + true + } + }; + + for (auto data : testData) + { + Log::Comment(std::get<0>(data).c_str()); + std::tuple result; + int amountMoved; + result = UiaTextRange::_moveEndpointByUnitDocument(std::get<2>(data), + std::get<4>(data), + std::get<1>(data), + &amountMoved); + + VERIFY_ARE_EQUAL(std::get<3>(data), amountMoved); + VERIFY_ARE_EQUAL(std::get<5>(data), std::get<0>(result)); + VERIFY_ARE_EQUAL(std::get<6>(data), std::get<1>(result)); + VERIFY_ARE_EQUAL(std::get<7>(data), std::get<2>(result)); + } + } +}; diff --git a/src/interactivity/win32/ut_interactivity_win32/product.pbxproj b/src/interactivity/win32/ut_interactivity_win32/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/interactivity/win32/ut_interactivity_win32/sources b/src/interactivity/win32/ut_interactivity_win32/sources new file mode 100644 index 000000000..f2e32fb63 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/sources @@ -0,0 +1,140 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.Interactivity.Win32.UnitTests +TARGETTYPE = DYNLINK +TARGET_DESTINATION = UnitTests +DLLDEF = + +UNIVERSAL_TEST = 1 +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + UiaTextRangeTests.cpp \ + DefaultResource.rc \ + +INCLUDES = \ + $(INCLUDES); \ + ..\..\..\inc\test; \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_INTERNAL_SDK_LIB_PATH)\onecoreuuid.lib \ + $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + $(SDK_LIB_PATH)\d2d1.lib \ + $(SDK_LIB_PATH)\dwrite.lib \ + $(SDK_LIB_PATH)\dxgi.lib \ + $(SDK_LIB_PATH)\d3d11.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-mm-playsound-l1.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-dwmapi-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-edputil-policy-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-font-l1.lib \ + $(ONECOREWINDOWS_INTERNAL_LIB_PATH_L)\ext-ms-win-gdi-internal-desktop-l1-1-0.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-caret-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-dialogbox-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-gui-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-menu-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-misc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-mouse-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-rectangle-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-server-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-window-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-object-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-rgn-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-cursor-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-dc-access-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-rawinput-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-sysparams-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-window-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-shell-shell32-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uxtheme-themes-l1.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uiacore-l1.lib \ + $(WINCORE_OBJ_PATH)\console\conint\$(O)\conint.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\buffer\out\lib\$(O)\conbufferout.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\host\lib\$(O)\conhostv2.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\tsf\$(O)\contsf.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\propslib\$(O)\conprops.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\adapter\lib\$(O)\ConTermAdapter.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\input\lib\$(O)\ConTermInput.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\parser\lib\$(O)\ConTermParser.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\base\lib\$(O)\ConRenderBase.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\dx\lib\$(O)\ConRenderDx.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\gdi\lib\$(O)\ConRenderGdi.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\vt\lib\$(O)\ConRenderVt.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\wddmcon\lib\$(O)\ConRenderWddmCon.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\server\lib\$(O)\ConServer.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\base\lib\$(O)\ConInteractivityBaseLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\onecore\lib\$(O)\ConInteractivityOneCoreLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\types\lib\$(O)\ConTypes.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ + +DELAYLOAD = \ + PROPSYS.dll; \ + D2D1.dll; \ + DWrite.dll; \ + DXGI.dll; \ + D3D11.dll; \ + OLEAUT32.dll; \ + api-ms-win-mm-playsound-l1.dll; \ + api-ms-win-shcore-scaling-l1.dll; \ + api-ms-win-shell-namespace-l1.dll; \ + ext-ms-win-dwmapi-ext-l1.dll; \ + ext-ms-win-edputil-policy-l1.dll; \ + ext-ms-win-gdi-dc-l1.dll; \ + ext-ms-win-gdi-dc-create-l1.dll; \ + ext-ms-win-gdi-draw-l1.dll; \ + ext-ms-win-gdi-font-l1.dll; \ + ext-ms-win-gdi-internal-desktop-l1.dll; \ + ext-ms-win-ntuser-caret-l1.dll; \ + ext-ms-win-ntuser-dialogbox-l1.dll; \ + ext-ms-win-ntuser-draw-l1.dll; \ + ext-ms-win-ntuser-keyboard-l1.dll; \ + ext-ms-win-ntuser-gui-l1.dll; \ + ext-ms-win-ntuser-menu-l1.dll; \ + ext-ms-win-ntuser-message-l1.dll; \ + ext-ms-win-ntuser-misc-l1.dll; \ + ext-ms-win-ntuser-mouse-l1.dll; \ + ext-ms-win-ntuser-rectangle-ext-l1.dll; \ + ext-ms-win-ntuser-server-l1.dll; \ + ext-ms-win-ntuser-sysparams-ext-l1.dll; \ + ext-ms-win-ntuser-window-l1.dll; \ + ext-ms-win-rtcore-gdi-object-l1.dll; \ + ext-ms-win-rtcore-gdi-rgn-l1.dll; \ + ext-ms-win-rtcore-ntuser-cursor-l1.dll; \ + ext-ms-win-rtcore-ntuser-dc-access-l1.dll; \ + ext-ms-win-rtcore-ntuser-rawinput-l1.dll; \ + ext-ms-win-rtcore-ntuser-sysparams-l1.dll; \ + ext-ms-win-rtcore-ntuser-window-ext-l1.dll; \ + ext-ms-win-shell-shell32-l1.dll; \ + ext-ms-win-uiacore-l1.dll; \ + ext-ms-win-uxtheme-themes-l1.dll; \ + +DLOAD_ERROR_HANDLER = kernelbase + +# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe. +C_DEFINES = $(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)""" +MUI_VERIFY_NO_LOC_RESOURCE = 1 diff --git a/src/interactivity/win32/ut_interactivity_win32/sources.dep b/src/interactivity/win32/ut_interactivity_win32/sources.dep new file mode 100644 index 000000000..c18b8d214 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/sources.dep @@ -0,0 +1,3 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ + diff --git a/src/interactivity/win32/ut_interactivity_win32/testmd.definition b/src/interactivity/win32/ut_interactivity_win32/testmd.definition new file mode 100644 index 000000000..37e9c68d7 --- /dev/null +++ b/src/interactivity/win32/ut_interactivity_win32/testmd.definition @@ -0,0 +1,18 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "Interactivity-Win32-UnitTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ ] + }, + "Logs": [ ], + "Plugins": [ ] +} \ No newline at end of file diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp new file mode 100644 index 000000000..e885e6d38 --- /dev/null +++ b/src/interactivity/win32/window.cpp @@ -0,0 +1,1356 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + + +#include "ConsoleControl.hpp" +#include "icon.hpp" +#include "menu.hpp" +#include "window.hpp" +#include "windowio.hpp" +#include "windowdpiapi.hpp" +#include "windowmetrics.hpp" +#include "windowtheme.hpp" +#include "windowUiaProvider.hpp" + +#include "..\..\host\globals.h" +#include "..\..\host\dbcs.h" +#include "..\..\host\getset.h" +#include "..\..\host\misc.h" +#include "..\..\host\_output.h" +#include "..\..\host\output.h" +#include "..\..\host\renderData.hpp" +#include "..\..\host\scrolling.hpp" +#include "..\..\host\srvinit.h" +#include "..\..\host\stream.h" +#include "..\..\host\telemetry.hpp" +#include "..\..\host\tracing.hpp" + +#include "..\..\renderer\base\renderer.hpp" +#include "..\..\renderer\gdi\gdirenderer.hpp" +#include "..\..\renderer\dx\DxRenderer.hpp" + +#include "..\inc\ServiceLocator.hpp" +#include "..\..\types\inc\Viewport.hpp" + +// The following default masks are used in creating windows +// Make sure that these flags match when switching to fullscreen and back +#define CONSOLE_WINDOW_FLAGS (WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL) +#define CONSOLE_WINDOW_EX_FLAGS (WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES | WS_EX_APPWINDOW | WS_EX_LAYERED) + +// Window class name +#define CONSOLE_WINDOW_CLASS (L"ConsoleWindowClass") + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Types; + +ATOM Window::s_atomWindowClass = 0; +Window* Window::s_Instance = nullptr; + +Window::Window() : + _fIsInFullscreen(false), + _pSettings(nullptr), + _hWnd(0) +{ + ZeroMemory((void*)&_rcClientLast, sizeof(_rcClientLast)); + ZeroMemory((void*)&_rcNonFullscreenWindowSize, sizeof(_rcNonFullscreenWindowSize)); + ZeroMemory((void*)&_rcFullscreenWindowSize, sizeof(_rcFullscreenWindowSize)); +} + +Window::~Window() +{ + if (nullptr != _pUiaProvider) + { + // This is a COM object, so call Release. It will clean up itself when the last ref is released. + _pUiaProvider->Release(); + } + + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + delete ServiceLocator::LocateGlobals().pRender; + } +} + +// Routine Description: +// - This routine allocates and initializes a window for the console +// Arguments: +// - pSettings - All user-configurable settings related to the console host +// - pScreen - The initial screen rendering data to attach to (renders in the client area of this window) +// Return Value: +// - STATUS_SUCCESS or suitable NT error code +[[nodiscard]] +NTSTATUS Window::CreateInstance(_In_ Settings* const pSettings, + _In_ SCREEN_INFORMATION* const pScreen) +{ + NTSTATUS status = s_RegisterWindowClass(); + + if (NT_SUCCESS(status)) + { + Window* pNewWindow = new(std::nothrow) Window(); + + status = NT_TESTNULL(pNewWindow); + + if (NT_SUCCESS(status)) + { + status = pNewWindow->_MakeWindow(pSettings, pScreen); + + if (NT_SUCCESS(status)) + { + Window::s_Instance = pNewWindow; + LOG_IF_FAILED(ServiceLocator::SetConsoleWindowInstance(pNewWindow)); + } + } + } + + return status; +} + +// Routine Description: +// - Registers the window class information with the system +// - Only should happen once for the entire lifetime of this class. +// Arguments: +// - +// Return Value: +// - STATUS_SUCCESS or failure from loading icons/registering class with the system +[[nodiscard]] +NTSTATUS Window::s_RegisterWindowClass() +{ + NTSTATUS status = STATUS_SUCCESS; + + // Today we never call this more than once. + // In the future, if we need multiple windows (for tabs, etc.) we will need to make this thread-safe. + // As such, the window class should always be 0 when we are entering this the first and only time. + FAIL_FAST_IF(!(s_atomWindowClass == 0)); + + // Only register if we haven't already registered + if (s_atomWindowClass == 0) + { + // Prepare window class structure + WNDCLASSEX wc = { 0 }; + wc.cbSize = sizeof(WNDCLASSEX); + wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS; + wc.lpfnWndProc = s_ConsoleWindowProc; + wc.cbClsExtra = 0; + wc.cbWndExtra = GWL_CONSOLE_WNDALLOC; + wc.hInstance = nullptr; + wc.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wc.hbrBackground = nullptr; // We don't want the background painted. It will cause flickering. + wc.lpszMenuName = nullptr; + wc.lpszClassName = CONSOLE_WINDOW_CLASS; + + // Load icons + status = Icon::Instance().GetIcons(&wc.hIcon, &wc.hIconSm); + + if (NT_SUCCESS(status)) + { + s_atomWindowClass = RegisterClassExW(&wc); + + if (s_atomWindowClass == 0) + { + status = NTSTATUS_FROM_WIN32(GetLastError()); + } + } + } + + return status; +} + +// Routine Description: +// - Updates some global system metrics when triggered. +// - Calls subroutines to update metrics for other relevant items +// - Example metrics include window borders, scroll size, timer values, etc. +// Arguments: +// - +// Return Value: +// - +void Window::_UpdateSystemMetrics() const +{ + WindowDpiApi* const dpiApi = ServiceLocator::LocateHighDpiApi(); + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + + Scrolling::s_UpdateSystemMetrics(); + + g.sVerticalScrollSize = (SHORT)dpiApi->GetSystemMetricsForDpi(SM_CXVSCROLL, g.dpi); + g.sHorizontalScrollSize = (SHORT)dpiApi->GetSystemMetricsForDpi(SM_CYHSCROLL, g.dpi); + + gci.GetCursorBlinker().UpdateSystemMetrics(); + + const auto sysConfig = ServiceLocator::LocateSystemConfigurationProvider(); + + g.cursorPixelWidth = sysConfig->GetCursorWidth(); +} + +// Routine Description: +// - This will call the system to create a window for the console, set +// up settings, and prepare for rendering. +// Arguments: +// - pSettings - Load user-configurable settings from this structure +// - pScreen - Attach to this screen for rendering the client area of the window +// Return Value: +// - STATUS_SUCCESS, invalid parameters, or various potential errors from calling CreateWindow +[[nodiscard]] +NTSTATUS Window::_MakeWindow(_In_ Settings* const pSettings, + _In_ SCREEN_INFORMATION* const pScreen) +{ + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + NTSTATUS status = STATUS_SUCCESS; + + if (pSettings == nullptr) + { + status = STATUS_INVALID_PARAMETER_1; + } + else if (pScreen == nullptr) + { + status = STATUS_INVALID_PARAMETER_2; + } + + // Ensure we have appropriate system metrics before we start constructing the window. + _UpdateSystemMetrics(); + + const bool useDx = pSettings->GetUseDx(); + GdiEngine* pGdiEngine = nullptr; + DxEngine* pDxEngine = nullptr; + try + { + if (useDx) + { + pDxEngine = new DxEngine(); + // TODO: MSFT:21255595 make this less gross + // Manually set the Dx Engine to Hwnd mode. When we're trying to + // determine the initial window size, which happens BEFORE the + // window is created, we'll want to make sure the DX engine does + // math in the hwnd mode, not the Composition mode. + THROW_IF_FAILED(pDxEngine->SetHwnd(0)); + g.pRender->AddRenderEngine(pDxEngine); + } + else + { + pGdiEngine = new GdiEngine(); + g.pRender->AddRenderEngine(pGdiEngine); + } + } + catch (...) + { + status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); + } + + if (NT_SUCCESS(status)) + { + if (NT_SUCCESS(status)) + { + SCREEN_INFORMATION& siAttached = GetScreenInfo(); + + siAttached.RefreshFontWithRenderer(); + + // Save reference to settings + _pSettings = pSettings; + + // Figure out coordinates and how big to make the window from the desired client viewport size + // Put left, top, right and bottom into rectProposed for checking against monitor screens below + RECT rectProposed = { pSettings->GetWindowOrigin().X, pSettings->GetWindowOrigin().Y, 0, 0 }; + _CalculateWindowRect(pSettings->GetWindowSize(), &rectProposed); //returns with rectangle filled out + + if (!WI_IsFlagSet(gci.Flags, CONSOLE_AUTO_POSITION)) + { + //if launched from a shortcut, ensure window is visible on screen + if (pSettings->IsStartupTitleIsLinkNameSet()) + { + // if window would be fully OFFscreen, change position so it is ON screen. + // This doesn't change the actual coordinates + // stored in the link, just the starting position + // of the window. + // When the user reconnects the other monitor, the + // window will be where he left it. Great for take + // home laptop scenario. + if (!MonitorFromRect(&rectProposed, MONITOR_DEFAULTTONULL)) + { + //Monitor we'll move to + HMONITOR hMon = MonitorFromRect(&rectProposed, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = { 0 }; + + //get origin of monitor's workarea + mi.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(hMon, &mi); + + //Adjust right and bottom to new positions, relative to monitor workarea's origin + //Need to do this before adjusting left/top so RECT_* calculations are correct + rectProposed.right = mi.rcWork.left + RECT_WIDTH(&rectProposed); + rectProposed.bottom = mi.rcWork.top + RECT_HEIGHT(&rectProposed); + + // Move origin to top left of nearest + // monitor's WORKAREA (accounting for taskbar + // and any app toolbars) + rectProposed.left = mi.rcWork.left; + rectProposed.top = mi.rcWork.top; + } + } + } + + // Attempt to create window + HWND hWnd = CreateWindowExW( + CONSOLE_WINDOW_EX_FLAGS, + CONSOLE_WINDOW_CLASS, + gci.GetTitle().c_str(), + CONSOLE_WINDOW_FLAGS, + WI_IsFlagSet(gci.Flags, + CONSOLE_AUTO_POSITION) ? CW_USEDEFAULT : rectProposed.left, + rectProposed.top, // field is ignored if CW_USEDEFAULT was chosen above + RECT_WIDTH(&rectProposed), + RECT_HEIGHT(&rectProposed), + HWND_DESKTOP, + nullptr, + nullptr, + this // handle to this window class, passed to WM_CREATE to help dispatching to this instance + ); + + if (hWnd == nullptr) + { + DWORD const gle = GetLastError(); + RIPMSG1(RIP_WARNING, "CreateWindow failed with gle = 0x%x", gle); + status = NTSTATUS_FROM_WIN32(gle); + } + + if (NT_SUCCESS(status)) + { + _hWnd = hWnd; + + if (useDx) + { + status = NTSTATUS_FROM_HRESULT(pDxEngine->SetHwnd(hWnd)); + + if (NT_SUCCESS(status)) + { + status = NTSTATUS_FROM_HRESULT(pDxEngine->Enable()); + } + } + else + { + status = NTSTATUS_FROM_HRESULT(pGdiEngine->SetHwnd(hWnd)); + } + + if (NT_SUCCESS(status)) + { + // Set alpha on window if requested + ApplyWindowOpacity(); + + status = Menu::CreateInstance(hWnd); + + if (NT_SUCCESS(status)) + { + gci.ConsoleIme.RefreshAreaAttributes(); + + // Do WM_GETICON workaround. Must call WM_SETICON once or apps calling WM_GETICON will get null. + LOG_IF_FAILED(Icon::Instance().ApplyWindowMessageWorkaround(hWnd)); + + // Set up the hot key for this window. + if (gci.GetHotKey() != 0) + { + SendMessageW(hWnd, WM_SETHOTKEY, gci.GetHotKey(), 0); + } + + ServiceLocator::LocateHighDpiApi()->EnableChildWindowDpiMessage(_hWnd, TRUE /*fEnable*/); + + // Post a window size update so that the new console window will size itself correctly once it's up and + // running. This works around chicken & egg cases involving window size calculations having to do with font + // sizes, DPI, and non-primary monitors (see MSFT #2367234). + siAttached.PostUpdateWindowSize(); + + // Locate window theming modules and try to set the dark mode. + try + { + WindowTheme theme; + LOG_IF_FAILED(theme.TrySetDarkMode(_hWnd)); + } + CATCH_LOG(); + } + } + } + } + } + + return status; +} + +// Routine Description: +// - Called when the window is about to close +// - Right now, it just triggers the process list management to notify that we're closing +// Arguments: +// - +// Return Value: +// - +void Window::_CloseWindow() const +{ + // Pass on the notification to attached processes. + // Since we only have one window for now, this will be the end of the host process as well. + CloseConsoleProcessState(); +} + +// Routine Description: +// - Activates and shows this window based on the flags given. +// Arguments: +// - wShowWindow - See STARTUPINFO wShowWindow member: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686331(v=vs.85).aspx +// Return Value: +// - STATUS_SUCCESS or system errors from activating the window and setting its show states +[[nodiscard]] +NTSTATUS Window::ActivateAndShow(const WORD wShowWindow) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + NTSTATUS status = STATUS_SUCCESS; + HWND const hWnd = GetWindowHandle(); + + // Only activate if the wShowWindow we were passed at process create doesn't explicitly tell us to remain inactive/hidden + if (wShowWindow != SW_SHOWNOACTIVATE && + wShowWindow != SW_SHOWMINNOACTIVE && + wShowWindow != SW_HIDE) + { + // Do not check result. On some SKUs, such as WinPE, it's perfectly OK for NULL to be returned. + SetActiveWindow(hWnd); + } + else if (wShowWindow == SW_SHOWMINNOACTIVE) + { + // If we're minimized and not the active window, set iconic to stop rendering + gci.Flags |= CONSOLE_IS_ICONIC; + } + + if (NT_SUCCESS(status)) + { + ShowWindow(hWnd, wShowWindow); + + SCREEN_INFORMATION& siAttached = GetScreenInfo(); + siAttached.InternalUpdateScrollBars(); + } + + return status; +} + +// Routine Description: +// - This routine sets the window origin. +// Arguments: +// - NewWindow: the inclusive rect to use as the new viewport in the buffer +// Return Value: +// +void Window::ChangeViewport(const SMALL_RECT NewWindow) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + + COORD const FontSize = ScreenInfo.GetScreenFontSize(); + + if (WI_IsFlagClear(gci.Flags, CONSOLE_IS_ICONIC)) + { + Selection* pSelection = &Selection::Instance(); + pSelection->HideSelection(); + + // Fire off an event to let accessibility apps know we've scrolled. + IAccessibilityNotifier *pNotifier = ServiceLocator::LocateAccessibilityNotifier(); + if (pNotifier != nullptr) + { + pNotifier->NotifyConsoleUpdateScrollEvent(ScreenInfo.GetViewport().Left() - NewWindow.Left, + ScreenInfo.GetViewport().Top() - NewWindow.Top); + } + + // The new window is OK. Store it in screeninfo and refresh screen. + ScreenInfo.SetViewport(Viewport::FromInclusive(NewWindow), false); + Tracing::s_TraceWindowViewport(ScreenInfo.GetViewport()); + + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + ServiceLocator::LocateGlobals().pRender->TriggerScroll(); + } + + pSelection->ShowSelection(); + } + else + { + // we're iconic + ScreenInfo.SetViewport(Viewport::FromInclusive(NewWindow), false); + Tracing::s_TraceWindowViewport(ScreenInfo.GetViewport()); + } + + LOG_IF_FAILED(ConsoleImeResizeCompStrView()); + + ScreenInfo.UpdateScrollBars(); +} + +// Routine Description: +// - Sends an update to the window size based on the character size requested. +// Arguments: +// - Size of the window in characters (relative to the current font) +// Return Value: +// - +void Window::UpdateWindowSize(const COORD coordSizeInChars) +{ + GetScreenInfo().SetViewportSize(&coordSizeInChars); + + PostUpdateWindowSize(); +} + +// Routine Description: +// Arguments: +// Return Value: +void Window::UpdateWindowPosition(_In_ POINT const ptNewPos) const +{ + SetWindowPos(GetWindowHandle(), + nullptr, + ptNewPos.x, + ptNewPos.y, + 0, + 0, + SWP_NOSIZE | SWP_NOZORDER); +} + +// This routine adds or removes the name to or from the beginning of the window title. The possible names are "Scroll", "Mark", and "Select" +void Window::UpdateWindowText() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const bool fInScrollMode = Scrolling::s_IsInScrollMode(); + + Selection *pSelection = &Selection::Instance(); + const bool fInKeyboardMarkMode = pSelection->IsInSelectingState() && pSelection->IsKeyboardMarkSelection(); + const bool fInMouseSelectMode = pSelection->IsInSelectingState() && pSelection->IsMouseInitiatedSelection(); + + // should have at most one active mode + FAIL_FAST_IF(!((fInKeyboardMarkMode && !fInMouseSelectMode && !fInScrollMode) || + (!fInKeyboardMarkMode && fInMouseSelectMode && !fInScrollMode) || + (!fInKeyboardMarkMode && !fInMouseSelectMode && fInScrollMode) || + (!fInKeyboardMarkMode && !fInMouseSelectMode && !fInScrollMode))); + + // determine which message, if any, we want to use + DWORD dwMsgId = 0; + if (fInKeyboardMarkMode) + { + dwMsgId = ID_CONSOLE_MSGMARKMODE; + } + else if (fInMouseSelectMode) + { + dwMsgId = ID_CONSOLE_MSGSELECTMODE; + } + else if (fInScrollMode) + { + dwMsgId = ID_CONSOLE_MSGSCROLLMODE; + } + + // if we have a message, use it + if (dwMsgId != 0) + { + // load mode string + WCHAR szMsg[64]; + if (LoadStringW(ServiceLocator::LocateGlobals().hInstance, dwMsgId, szMsg, ARRAYSIZE(szMsg)) > 0) + { + gci.SetTitlePrefix(szMsg); + } + } + else + { + // no mode-based message. set title back to original state. + gci.SetTitlePrefix(L""); + } +} + +void Window::CaptureMouse() +{ + SetCapture(_hWnd); +} + +BOOL Window::ReleaseMouse() +{ + return ReleaseCapture(); +} + +// Routine Description: +// - Adjusts the outer window frame size. Does not move the position. +// Arguments: +// - sizeNew - The X and Y dimensions +// Return Value: +// - +void Window::_UpdateWindowSize(const SIZE sizeNew) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + + if (WI_IsFlagClear(gci.Flags, CONSOLE_IS_ICONIC)) + { + ScreenInfo.InternalUpdateScrollBars(); + + SetWindowPos(GetWindowHandle(), + nullptr, + 0, + 0, + sizeNew.cx, + sizeNew.cy, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_DRAWFRAME); + } +} + +// Routine Description: +// - Triggered when the buffer dimensions/viewport is changed. +// - This function recalculates what size the window should be in order to host the given buffer and viewport +// - Then it will trigger an actual adjustment of the outer window frame +// Arguments: +// - - All state is read from the attached screen buffer +// Return Value: +// - STATUS_SUCCESS or suitable error code +[[nodiscard]] +NTSTATUS Window::_InternalSetWindowSize() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + SCREEN_INFORMATION& siAttached = GetScreenInfo(); + + WI_ClearFlag(gci.Flags, CONSOLE_SETTING_WINDOW_SIZE); + + if (!IsInFullscreen() && !IsInMaximized()) + { + // Figure out how big to make the window, given the desired client area size. + siAttached.ResizingWindow++; + + // First get the buffer viewport size + const auto WindowDimensions = siAttached.GetViewport().Dimensions(); + + // We'll use the font to convert characters to pixels + const auto ScreenFontSize = siAttached.GetScreenFontSize(); + + // Now do the multiplication of characters times pixels per char. This is the client area pixel size. + SIZE WindowSize; + WindowSize.cx = WindowDimensions.X * ScreenFontSize.X; + WindowSize.cy = WindowDimensions.Y * ScreenFontSize.Y; + + // Fill a rectangle to call the system to adjust the client rect into a window rect + RECT rectSizeTemp = { 0 }; + rectSizeTemp.right = WindowSize.cx; + rectSizeTemp.bottom = WindowSize.cy; + FAIL_FAST_IF(!(rectSizeTemp.top == 0 && rectSizeTemp.left == 0)); + ServiceLocator::LocateWindowMetrics()->ConvertClientRectToWindowRect(&rectSizeTemp); + + // Measure the adjusted rectangle dimensions and fill up the size variable + WindowSize.cx = RECT_WIDTH(&rectSizeTemp); + WindowSize.cy = RECT_HEIGHT(&rectSizeTemp); + + if (WindowDimensions.Y != 0) + { + // We want the alt to have scroll bars if the main has scroll bars. + // The bars are disabled, but they're still there. + // This keeps the window, viewport, and SB size from changing when swapping. + if (!siAttached.GetMainBuffer().IsMaximizedX()) + { + WindowSize.cy += ServiceLocator::LocateGlobals().sHorizontalScrollSize; + } + + if (!siAttached.GetMainBuffer().IsMaximizedY()) + { + WindowSize.cx += ServiceLocator::LocateGlobals().sVerticalScrollSize; + } + } + + // Only attempt to actually change the window size if the difference between the size we just calculated + // and the size of the current window is substantial enough to make a rendering difference. + // This is an issue now in the V2 console because we allow sub-character window sizes + // such that there isn't leftover space around the window when snapping. + + // To figure out if it's substantial, calculate what the window size would be if it were one character larger than what we just proposed + SIZE WindowSizeMax; + WindowSizeMax.cx = WindowSize.cx + ScreenFontSize.X; + WindowSizeMax.cy = WindowSize.cy + ScreenFontSize.Y; + + // And figure out the current window size as well. + RECT const rcWindowCurrent = GetWindowRect(); + SIZE WindowSizeCurrent; + WindowSizeCurrent.cx = RECT_WIDTH(&rcWindowCurrent); + WindowSizeCurrent.cy = RECT_HEIGHT(&rcWindowCurrent); + + // If the current window has a few extra sub-character pixels between the proposed size (WindowSize) and the next size up (WindowSizeMax), then don't change anything. + bool const fDeltaXSubstantial = !(WindowSizeCurrent.cx >= WindowSize.cx && WindowSizeCurrent.cx < WindowSizeMax.cx); + bool const fDeltaYSubstantial = !(WindowSizeCurrent.cy >= WindowSize.cy && WindowSizeCurrent.cy < WindowSizeMax.cy); + + // If either change was substantial, update the window accordingly to the newly proposed value. + if (fDeltaXSubstantial || fDeltaYSubstantial) + { + _UpdateWindowSize(WindowSize); + } + else + { + // If the change wasn't substantial, we may still need to update scrollbar positions. Note that PSReadLine + // scrolls the window via Console.SetWindowPosition, which ultimately calls down to SetConsoleWindowInfo, + // which ends up in this function. + siAttached.InternalUpdateScrollBars(); + } + + // MSFT: 12092729 + // To fix an issue with 3rd party applications that wrap our console, notify that the screen buffer size changed + // when the window viewport is updated. + // --- + // - The specific scenario that this impacts is ConEmu (wrapping our console) to use Bash in WSL. + // - The reason this is a problem is because ConEmu has to programatically manipulate our buffer and window size + // one after another to get our dimensions to change. + // - The WSL layer watches our Buffer change message to know when to get the new Window size and send it into the + // WSL environment. This isn't technically correct to use a Buffer message to know when Window changes, but + // it's not totally their fault because we do not provide a Window changed message at all. + // - If our window is adjusted directly, the Buffer and Window dimensions are both updated simultaneously under lock + // and WSL gets one message and updates appropriately. + // - If ConEmu updates it via the API, one is updated, then the other with an unlocked time interval. + // The WSL layer will potentially get the Window size that hasn't been updated yet or is out of sync before the + // other API call is completed which results in the application in the WSL environment thinking the window is + // a different size and outputting VT sequences with an invalid assumption. + // - If we make it so a Window change also emits a Buffer change message, then WSL will be notified appropriately + // and can pass that information into the WSL environment. + // - To Windows apps that weren't expecting this information, it should cause no harm because they should just receive + // an additional Buffer message with the same size again and do nothing special. + ScreenBufferSizeChange(siAttached.GetActiveBuffer().GetBufferSize().Dimensions()); + + siAttached.ResizingWindow--; + } + + LOG_IF_FAILED(ConsoleImeResizeCompStrView()); + + return STATUS_SUCCESS; +} + +// Routine Description: +// - Adjusts the window contents in response to vertical scrolling +// Arguments: +// - ScrollCommand - The relevant command (one line, one page, etc.) +// - AbsoluteChange - The magnitude of the change (for tracking commands) +// Return Value: +// - +void Window::VerticalScroll(const WORD wScrollCommand, const WORD wAbsoluteChange) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + COORD NewOrigin; + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + + // Log a telemetry event saying the user interacted with the Console + Telemetry::Instance().SetUserInteractive(); + + const auto& viewport = ScreenInfo.GetViewport(); + + NewOrigin = viewport.Origin(); + + const SHORT sScreenBufferSizeY = ScreenInfo.GetBufferSize().Height(); + + switch (wScrollCommand) + { + case SB_LINEUP: + { + NewOrigin.Y--; + break; + } + + case SB_LINEDOWN: + { + NewOrigin.Y++; + break; + } + + case SB_PAGEUP: + { + NewOrigin.Y -= viewport.Height() - 1; + break; + } + + case SB_PAGEDOWN: + { + NewOrigin.Y += viewport.Height() - 1; + break; + } + + case SB_THUMBTRACK: + { + gci.Flags |= CONSOLE_SCROLLBAR_TRACKING; + NewOrigin.Y = wAbsoluteChange; + break; + } + + case SB_THUMBPOSITION: + { + UnblockWriteConsole(CONSOLE_SCROLLBAR_TRACKING); + NewOrigin.Y = wAbsoluteChange; + break; + } + + case SB_TOP: + { + NewOrigin.Y = 0; + break; + } + + case SB_BOTTOM: + { + NewOrigin.Y = sScreenBufferSizeY - viewport.Height(); + break; + } + + default: + { + return; + } + } + + NewOrigin.Y = std::clamp(NewOrigin.Y, 0i16, gsl::narrow(sScreenBufferSizeY - viewport.Height())); + LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); +} + +// Routine Description: +// - Adjusts the window contents in response to horizontal scrolling +// Arguments: +// - ScrollCommand - The relevant command (one line, one page, etc.) +// - AbsoluteChange - The magnitude of the change (for tracking commands) +// Return Value: +// - +void Window::HorizontalScroll(const WORD wScrollCommand, const WORD wAbsoluteChange) +{ + // Log a telemetry event saying the user interacted with the Console + Telemetry::Instance().SetUserInteractive(); + + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + const SHORT sScreenBufferSizeX = ScreenInfo.GetBufferSize().Width(); + const auto& viewport = ScreenInfo.GetViewport(); + COORD NewOrigin = viewport.Origin(); + + switch (wScrollCommand) + { + case SB_LINEUP: + { + NewOrigin.X--; + break; + } + + case SB_LINEDOWN: + { + NewOrigin.X++; + break; + } + + case SB_PAGEUP: + { + NewOrigin.X -= viewport.Width() - 1; + break; + } + + case SB_PAGEDOWN: + { + NewOrigin.X += viewport.Width() - 1; + break; + } + + case SB_THUMBTRACK: + case SB_THUMBPOSITION: + { + NewOrigin.X = wAbsoluteChange; + break; + } + + case SB_TOP: + { + NewOrigin.X = 0; + break; + } + + case SB_BOTTOM: + { + NewOrigin.X = (WORD)(sScreenBufferSizeX - viewport.Width()); + break; + } + + default: + { + return; + } + } + NewOrigin.X = std::clamp(NewOrigin.X, 0i16, gsl::narrow(sScreenBufferSizeX - viewport.Width())); + LOG_IF_FAILED(ScreenInfo.SetViewportOrigin(true, NewOrigin, false)); +} + +BOOL Window::EnableBothScrollBars() +{ + return EnableScrollBar(_hWnd, SB_BOTH, ESB_ENABLE_BOTH); +} + +int Window::UpdateScrollBar(bool isVertical, + bool isAltBuffer, + UINT pageSize, + int maxSize, + int viewportPosition) +{ + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = isAltBuffer ? SIF_ALL | SIF_DISABLENOSCROLL : SIF_ALL; + si.nPage = pageSize; + si.nMin = 0; + si.nMax = maxSize; + si.nPos = viewportPosition; + + return SetScrollInfo(_hWnd, isVertical ? SB_VERT : SB_HORZ, &si, TRUE); +} + +// Routine Description: +// - Converts a window position structure (sent to us when the window moves) into a window rectangle (the outside edge dimensions) +// Arguments: +// - lpWindowPos - position structure received via Window message +// - prc - rectangle to fill +// Return Value: +// - +void Window::s_ConvertWindowPosToWindowRect(const LPWINDOWPOS lpWindowPos, _Out_ RECT* const prc) +{ + prc->left = lpWindowPos->x; + prc->top = lpWindowPos->y; + prc->right = lpWindowPos->x + lpWindowPos->cx; + prc->bottom = lpWindowPos->y + lpWindowPos->cy; +} + +// Routine Description: +// - Converts character counts of the viewport (client area, screen buffer) into the outer pixel dimensions of the window using the current window for context +// Arguments: +// - coordWindowInChars - The size of the viewport +// - prectWindow - rectangle to fill with pixel positions of the outer edge rectangle for this window +// Return Value: +// - +void Window::_CalculateWindowRect(const COORD coordWindowInChars, _Inout_ RECT* const prectWindow) const +{ + auto& g = ServiceLocator::LocateGlobals(); + const SCREEN_INFORMATION& siAttached = GetScreenInfo(); + const COORD coordFontSize = siAttached.GetScreenFontSize(); + const HWND hWnd = GetWindowHandle(); + const COORD coordBufferSize = siAttached.GetBufferSize().Dimensions(); + const int iDpi = g.dpi; + + s_CalculateWindowRect(coordWindowInChars, iDpi, coordFontSize, coordBufferSize, hWnd, prectWindow); +} + +// Routine Description: +// - Converts character counts of the viewport (client area, screen buffer) into +// the outer pixel dimensions of the window +// Arguments: +// - coordWindowInChars - The size of the viewport +// - iDpi - The DPI of the monitor on which this window will reside +// (used to get DPI-scaled system metrics) +// - coordFontSize - the size in pixels of the font on the monitor +// (this should be already scaled for DPI) +// - coordBufferSize - the character count of the buffer rectangle in X by Y +// - hWnd - If available, a handle to the window we would change so we can +// retrieve its class information for border/titlebar/etc metrics. +// - prectWindow - rectangle to fill with pixel positions of the outer edge +// rectangle for this window +// Return Value: +// - +void Window::s_CalculateWindowRect(const COORD coordWindowInChars, + const int iDpi, + const COORD coordFontSize, + const COORD coordBufferSize, + _In_opt_ HWND const hWnd, + _Inout_ RECT* const prectWindow) +{ + SIZE sizeWindow; + + // Initially use the given size in characters * font size to get client area pixel size + sizeWindow.cx = coordWindowInChars.X * coordFontSize.X; + sizeWindow.cy = coordWindowInChars.Y * coordFontSize.Y; + + // Create a proposed rectangle + RECT rectProposed = { prectWindow->left, prectWindow->top, prectWindow->left + sizeWindow.cx, prectWindow->top + sizeWindow.cy }; + + // Now adjust the client area into a window size + // 1. Start with default window style + DWORD dwStyle = CONSOLE_WINDOW_FLAGS; + DWORD dwExStyle = CONSOLE_WINDOW_EX_FLAGS; + BOOL fMenu = FALSE; + + // 2. if we already have a window handle, check if the style has been updated + if (hWnd != nullptr) + { + dwStyle = GetWindowStyle(hWnd); + dwExStyle = GetWindowExStyle(hWnd); + } + + // 3. Perform adjustment + // NOTE: This may adjust the position of the window as well as the size. This is why we use rectProposed in the interim. + ServiceLocator::LocateWindowMetrics()->AdjustWindowRectEx(&rectProposed, dwStyle, fMenu, dwExStyle, iDpi); + + // Finally compensate for scroll bars + + // If the window is smaller than the buffer in width, add space at the bottom for a horizontal scroll bar + if (coordWindowInChars.X < coordBufferSize.X) + { + rectProposed.bottom += (SHORT)ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CYHSCROLL, iDpi); + } + + // If the window is smaller than the buffer in height, add space at the right for a vertical scroll bar + if (coordWindowInChars.Y < coordBufferSize.Y) + { + rectProposed.right += (SHORT)ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CXVSCROLL, iDpi); + } + + // Apply the calculated sizes to the existing window pointer + // We do this at the end so we can preserve the positioning of the window and just change the size. + prectWindow->right = prectWindow->left + RECT_WIDTH(&rectProposed); + prectWindow->bottom = prectWindow->top + RECT_HEIGHT(&rectProposed); +} + +RECT Window::GetWindowRect() const +{ + RECT rc = { 0 }; + ::GetWindowRect(GetWindowHandle(), &rc); + return rc; +} + +HWND Window::GetWindowHandle() const +{ + return _hWnd; +} + +SCREEN_INFORMATION& Window::GetScreenInfo() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer(); +} + +const SCREEN_INFORMATION& Window::GetScreenInfo() const +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + return gci.GetActiveOutputBuffer(); +} + +// Routine Description: +// - Gets the window opacity (alpha channel) +// Arguments: +// - +// Return Value: +// - The level of opacity. 0xff should represent 100% opaque and 0x00 should be 100% transparent. (Used for Alpha channel in drawing.) +BYTE Window::GetWindowOpacity() const +{ + return _pSettings->GetWindowAlpha(); +} + +// Routine Description: +// - Sets the window opacity (alpha channel) with the given value +// - Will restrict to within the valid range. Invalid values will use 0% transparency/100% opaque. +// Arguments: +// - bOpacity - 0xff/100% opacity = opaque window. 0xb2/70% opacity = 30% transparent window. +// Return Value: +// - +void Window::SetWindowOpacity(const BYTE bOpacity) +{ + _pSettings->SetWindowAlpha(bOpacity); +} + +// Routine Description: +// - Calls the operating system to apply the current window opacity settings to the active console window. +// Arguments: +// - +// Return Value: +// - +void Window::ApplyWindowOpacity() const +{ + const BYTE bAlpha = GetWindowOpacity(); + HWND const hWnd = GetWindowHandle(); + + // See: http://msdn.microsoft.com/en-us/library/ms997507.aspx + SetLayeredWindowAttributes(hWnd, 0, bAlpha, LWA_ALPHA); +} + +// Routine Description: +// - Changes the window opacity by a specified delta. +// - This will update the internally stored value by the given delta (within boundaries) +// and then will have the new value applied to the actual window. +// - Values that would make the opacity greater than 100% will be fixed to 100%. +// - Values that would bring the opacity below the minimum threshold will be fixed to the minimum threshold. +// Arguments: +// - sOpacityDelta - How much to modify the current window opacity. Positive = more opaque. Negative = more transparent. +// Return Value: +// - +void Window::ChangeWindowOpacity(const short sOpacityDelta) +{ + // Window Opacity is always a BYTE (unsigned char, 1 byte) + // Delta is a short (signed short, 2 bytes) + + int iAlpha = GetWindowOpacity(); // promote unsigned char to fit into a signed int (4 bytes) + + iAlpha += sOpacityDelta; // performing signed math of 2 byte delta into 4 bytes will not under/overflow. + + // comparisons are against 1 byte values and are ok. + if (iAlpha > BYTE_MAX) + { + iAlpha = BYTE_MAX; + } + else if (iAlpha < MIN_WINDOW_OPACITY) + { + iAlpha = MIN_WINDOW_OPACITY; + } + + //Opacity bool is set to true when keyboard or mouse short cut used. + SetWindowOpacity((BYTE)iAlpha); // cast to fit is guaranteed to be within byte bounds by the checks above. + ApplyWindowOpacity(); +} + +// Routine Description: +// - Shorthand for checking if the current window has the maximized property set +// - Uses internally stored window handle +// Return Value: +// - True if maximized. False otherwise. +bool Window::IsInMaximized() const +{ + return IsMaximized(_hWnd); +} + +bool Window::IsInFullscreen() const +{ + return _fIsInFullscreen; +} + +void Window::SetIsFullscreen(const bool fFullscreenEnabled) +{ + // It is possible to enter SetIsFullScreen even if we're already in full screen. + // Use the old is in fullscreen flag to gate checks that rely on the current state. + bool fOldIsInFullscreen = _fIsInFullscreen; + _fIsInFullscreen = fFullscreenEnabled; + + HWND const hWnd = GetWindowHandle(); + + // First, modify regular window styles as appropriate + LONG dwWindowStyle = GetWindowLongW(hWnd, GWL_STYLE); + if (_fIsInFullscreen) + { + // moving to fullscreen. remove WS_OVERLAPPEDWINDOW, which specifies styles for non-fullscreen windows (e.g. + // caption bar). add the WS_POPUP style to allow us to size ourselves to the monitor size. + WI_ClearAllFlags(dwWindowStyle, WS_OVERLAPPEDWINDOW); + WI_SetFlag(dwWindowStyle, WS_POPUP); + } + else + { + // coming back from fullscreen. undo what we did to get in to fullscreen in the first place. + WI_ClearFlag(dwWindowStyle, WS_POPUP); + WI_SetAllFlags(dwWindowStyle, WS_OVERLAPPEDWINDOW); + } + SetWindowLongW(hWnd, GWL_STYLE, dwWindowStyle); + + // Now modify extended window styles as appropriate + LONG dwExWindowStyle = GetWindowLongW(hWnd, GWL_EXSTYLE); + if (_fIsInFullscreen) + { + // moving to fullscreen. remove the window edge style to avoid an ugly border when not focused. + WI_ClearFlag(dwExWindowStyle, WS_EX_WINDOWEDGE); + } + else + { + // coming back from fullscreen. + WI_SetFlag(dwExWindowStyle, WS_EX_WINDOWEDGE); + } + SetWindowLongW(hWnd, GWL_EXSTYLE, dwExWindowStyle); + + _BackupWindowSizes(fOldIsInFullscreen); + _ApplyWindowSize(); +} + +void Window::_BackupWindowSizes(const bool fCurrentIsInFullscreen) +{ + if (_fIsInFullscreen) + { + // Note: the current window size depends on the current state of the window. + // So don't back it up if we're already in full screen. + if (!fCurrentIsInFullscreen) + { + _rcNonFullscreenWindowSize = GetWindowRect(); + } + + // get and back up the current monitor's size + HMONITOR const hCurrentMonitor = MonitorFromWindow(GetWindowHandle(), MONITOR_DEFAULTTONEAREST); + MONITORINFO currMonitorInfo; + currMonitorInfo.cbSize = sizeof(currMonitorInfo); + if (GetMonitorInfo(hCurrentMonitor, &currMonitorInfo)) + { + _rcFullscreenWindowSize = currMonitorInfo.rcMonitor; + } + } +} + +void Window::_ApplyWindowSize() +{ + const RECT rcNewSize = _fIsInFullscreen ? _rcFullscreenWindowSize : _rcNonFullscreenWindowSize; + + SetWindowPos(GetWindowHandle(), + HWND_TOP, + rcNewSize.left, + rcNewSize.top, + RECT_WIDTH(&rcNewSize), + RECT_HEIGHT(&rcNewSize), + SWP_FRAMECHANGED); + + SCREEN_INFORMATION& siAttached = GetScreenInfo(); + siAttached.MakeCurrentCursorVisible(); +} + +void Window::ToggleFullscreen() +{ + SetIsFullscreen(!IsInFullscreen()); +} + +void Window::s_ReinitializeFontsForDPIChange() +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.GetActiveOutputBuffer().RefreshFontWithRenderer(); +} + +LRESULT Window::s_RegPersistWindowPos(_In_ PCWSTR const pwszTitle, + const BOOL fAutoPos, + const Window* const pWindow) +{ + + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + HKEY hCurrentUserKey, hConsoleKey, hTitleKey; + // Open the current user registry key. + NTSTATUS Status = RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(pwszTitle, &hCurrentUserKey, &hConsoleKey, &hTitleKey); + if (NT_SUCCESS(Status)) + { + // Save window size + auto windowRect = pWindow->GetWindowRect(); + const auto windowDimensions = gci.GetActiveOutputBuffer().GetViewport().Dimensions(); + DWORD dwValue = MAKELONG(windowDimensions.X, windowDimensions.Y); + Status = RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWSIZE, + REG_DWORD, + reinterpret_cast(&dwValue), + static_cast(sizeof(dwValue))); + if (NT_SUCCESS(Status)) + { + const COORD coordScreenBufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(); + auto screenBufferWidth = coordScreenBufferSize.X; + auto screenBufferHeight = coordScreenBufferSize.Y; + dwValue = MAKELONG(screenBufferWidth, screenBufferHeight); + Status = RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_BUFFERSIZE, + REG_DWORD, + reinterpret_cast(&dwValue), + static_cast(sizeof(dwValue))); + if (NT_SUCCESS(Status)) + { + + // Save window position + if (fAutoPos) + { + Status = RegistrySerialization::s_DeleteValue(hTitleKey, CONSOLE_REGISTRY_WINDOWPOS); + } + else + { + dwValue = MAKELONG(windowRect.left, windowRect.top); + Status = RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWPOS, + REG_DWORD, + reinterpret_cast(&dwValue), + static_cast(sizeof(dwValue))); + } + } + } + + if (hTitleKey != hConsoleKey) + { + RegCloseKey(hTitleKey); + } + + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + } + + return Status; +} + +LRESULT Window::s_RegPersistWindowOpacity(_In_ PCWSTR const pwszTitle, const Window* const pWindow) +{ + HKEY hCurrentUserKey, hConsoleKey, hTitleKey; + + // Open the current user registry key. + NTSTATUS Status = RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(pwszTitle, &hCurrentUserKey, &hConsoleKey, &hTitleKey); + if (NT_SUCCESS(Status)) + { + // Save window opacity + DWORD dwValue; + dwValue = pWindow->GetWindowOpacity(); + Status = RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWALPHA, + REG_DWORD, + reinterpret_cast(&dwValue), + static_cast(sizeof(dwValue))); + + if (hTitleKey != hConsoleKey) + { + RegCloseKey(hTitleKey); + } + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + } + return Status; +} + +// Routine Description: +// - Creates/retrieves a handle to the UI Automation provider COM interfaces +// Arguments: +// - +// Return Value: +// - Pointer to UI Automation provider class/interfaces. +IRawElementProviderSimple* Window::_GetUiaProvider() +{ + if (nullptr == _pUiaProvider) + { + try + { + _pUiaProvider = WindowUiaProvider::Create(); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + _pUiaProvider = nullptr; + } + } + + return _pUiaProvider; +} + +[[nodiscard]] +HRESULT Window::SignalUia(_In_ EVENTID id) +{ + if (_pUiaProvider != nullptr) + { + return _pUiaProvider->Signal(id); + } + return S_FALSE; +} + +[[nodiscard]] +HRESULT Window::UiaSetTextAreaFocus() +{ + if (_pUiaProvider != nullptr) + { + LOG_IF_FAILED(_pUiaProvider->SetTextAreaFocus()); + return S_OK; + } + return S_FALSE; +} + +void Window::SetOwner() +{ + SetConsoleWindowOwner(_hWnd, nullptr); +} + +BOOL Window::GetCursorPosition(_Out_ LPPOINT lpPoint) +{ + return GetCursorPos(lpPoint); +} + +BOOL Window::GetClientRectangle(_Out_ LPRECT lpRect) +{ + return GetClientRect(_hWnd, lpRect); +} + +int Window::MapPoints(_Inout_updates_(cPoints) LPPOINT lpPoints, _In_ UINT cPoints) +{ + return MapWindowPoints(_hWnd, nullptr, lpPoints, cPoints); +} + +BOOL Window::ConvertScreenToClient(_Inout_ LPPOINT lpPoint) +{ + return ScreenToClient(_hWnd, lpPoint); +} diff --git a/src/interactivity/win32/window.hpp b/src/interactivity/win32/window.hpp new file mode 100644 index 000000000..b8f51aafa --- /dev/null +++ b/src/interactivity/win32/window.hpp @@ -0,0 +1,183 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- window.hpp + +Abstract: +- This module is used for managing the main console window + +Author(s): +- Michael Niksa (MiNiksa) 14-Oct-2014 +- Paul Campbell (PaulCam) 14-Oct-2014 +--*/ +#pragma once + +#include "..\inc\IConsoleWindow.hpp" + + +namespace Microsoft::Console::Interactivity::Win32 +{ + class WindowUiaProvider; + + class Window final : public IConsoleWindow + { + public: + [[nodiscard]] + static NTSTATUS CreateInstance(_In_ Settings* const pSettings, + _In_ SCREEN_INFORMATION* const pScreen); + + [[nodiscard]] + NTSTATUS ActivateAndShow(const WORD wShowWindow); + + ~Window(); + + RECT GetWindowRect() const; + HWND GetWindowHandle() const; + SCREEN_INFORMATION& GetScreenInfo(); + const SCREEN_INFORMATION& GetScreenInfo() const; + + BYTE GetWindowOpacity() const; + void SetWindowOpacity(const BYTE bOpacity); + void ApplyWindowOpacity() const; + void ChangeWindowOpacity(const short sOpacityDelta); + + bool IsInMaximized() const; + + bool IsInFullscreen() const; + void SetIsFullscreen(const bool fFullscreenEnabled); + void ToggleFullscreen(); + + void ChangeViewport(const SMALL_RECT NewWindow); + + void VerticalScroll(const WORD wScrollCommand, + const WORD wAbsoluteChange); + void HorizontalScroll(const WORD wScrollCommand, + const WORD wAbsoluteChange); + + BOOL EnableBothScrollBars(); + int UpdateScrollBar(bool isVertical, + bool isAltBuffer, + UINT pageSize, + int maxSize, + int viewportPosition); + + void UpdateWindowSize(const COORD coordSizeInChars); + void UpdateWindowPosition(_In_ POINT const ptNewPos) const; + void UpdateWindowText(); + + void CaptureMouse(); + BOOL ReleaseMouse(); + + // Dispatchers (requests from other parts of the + // console get dispatched onto the window message + // queue/thread) + BOOL SendNotifyBeep() const; + + BOOL PostUpdateScrollBars() const; + BOOL PostUpdateWindowSize() const; + BOOL PostUpdateExtendedEditKeys() const; + + [[nodiscard]] + HRESULT SignalUia(_In_ EVENTID id); + + void SetOwner(); + BOOL GetCursorPosition(_Out_ LPPOINT lpPoint); + BOOL GetClientRectangle(_Out_ LPRECT lpRect); + int MapPoints(_Inout_updates_(cPoints) LPPOINT lpPoints, + _In_ UINT cPoints); + BOOL ConvertScreenToClient(_Inout_ LPPOINT lpPoint); + + [[nodiscard]] + HRESULT UiaSetTextAreaFocus(); + + protected: + // prevent accidental generation of copies + Window(Window const&) = delete; + void operator=(Window const&) = delete; + + private: + Window(); + + // Registration/init + [[nodiscard]] + static NTSTATUS s_RegisterWindowClass(); + [[nodiscard]] + NTSTATUS _MakeWindow(_In_ Settings* const pSettings, + _In_ SCREEN_INFORMATION* const pScreen); + void _CloseWindow() const; + + static ATOM s_atomWindowClass; + Settings* _pSettings; + + HWND _hWnd; + static Window* s_Instance; + + [[nodiscard]] + NTSTATUS _InternalSetWindowSize(); + void _UpdateWindowSize(const SIZE sizeNew); + + void _UpdateSystemMetrics() const; + + // Wndproc + static LRESULT CALLBACK s_ConsoleWindowProc(_In_ HWND hwnd, + _In_ UINT uMsg, + _In_ WPARAM wParam, + _In_ LPARAM lParam); + LRESULT CALLBACK ConsoleWindowProc(_In_ HWND, + _In_ UINT uMsg, + _In_ WPARAM wParam, + _In_ LPARAM lParam); + + // Wndproc helpers + void _HandleDrop(const WPARAM wParam) const; + [[nodiscard]] + HRESULT _HandlePaint() const; + void _HandleWindowPosChanged(const LPARAM lParam); + + // Accessibility/UI Automation + LRESULT _HandleGetObject(const HWND hwnd, + const WPARAM wParam, + const LPARAM lParam); + IRawElementProviderSimple* _GetUiaProvider(); + WindowUiaProvider* _pUiaProvider = nullptr; + + // Dynamic Settings helpers + static LRESULT s_RegPersistWindowPos(_In_ PCWSTR const pwszTitle, + const BOOL fAutoPos, + const Window* const pWindow); + static LRESULT s_RegPersistWindowOpacity(_In_ PCWSTR const pwszTitle, + const Window* const pWindow); + + // The size/position of the window on the most recent update. + // This is remembered so we can figure out which + // size the client was resized from. + RECT _rcClientLast; + + // Full screen + void _BackupWindowSizes(const bool fCurrentIsInFullscreen); + void _ApplyWindowSize(); + + bool _fIsInFullscreen; + RECT _rcFullscreenWindowSize; + RECT _rcNonFullscreenWindowSize; + + // math helpers + void _CalculateWindowRect(const COORD coordWindowInChars, + _Inout_ RECT* const prectWindow) const; + static void s_CalculateWindowRect(const COORD coordWindowInChars, + const int iDpi, + const COORD coordFontSize, + const COORD coordBufferSize, + _In_opt_ HWND const hWnd, + _Inout_ RECT* const prectWindow); + + static void s_ReinitializeFontsForDPIChange(); + + bool _fInDPIChange = false; + + static void s_ConvertWindowPosToWindowRect(const LPWINDOWPOS lpWindowPos, + _Out_ RECT* const prc); + }; +} diff --git a/src/interactivity/win32/windowUiaProvider.cpp b/src/interactivity/win32/windowUiaProvider.cpp new file mode 100644 index 000000000..5dfe3234b --- /dev/null +++ b/src/interactivity/win32/windowUiaProvider.cpp @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include "precomp.h" + +#include "windowUiaProvider.hpp" +#include "window.hpp" + +#include "screenInfoUiaProvider.hpp" +#include "UiaTextRange.hpp" + +#include "../inc/ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Interactivity::Win32::WindowUiaProviderTracing; + +WindowUiaProvider::WindowUiaProvider() : + _signalEventFiring{}, + _pScreenInfoProvider{ nullptr }, + _cRefs(1) +{ + +} + +WindowUiaProvider::~WindowUiaProvider() +{ + if (_pScreenInfoProvider) + { + _pScreenInfoProvider->Release(); + } +} + +WindowUiaProvider* WindowUiaProvider::Create() +{ + WindowUiaProvider* pWindowProvider = nullptr; + ScreenInfoUiaProvider* pScreenInfoProvider = nullptr; + try + { + pWindowProvider = new WindowUiaProvider(); + pScreenInfoProvider = new ScreenInfoUiaProvider(pWindowProvider); + pWindowProvider->_pScreenInfoProvider = pScreenInfoProvider; + + Tracing::s_TraceUia(pWindowProvider, ApiCall::Create, nullptr); + + return pWindowProvider; + } + catch (...) + { + if (nullptr != pWindowProvider) + { + pWindowProvider->Release(); + } + + if (nullptr != pScreenInfoProvider) + { + pScreenInfoProvider->Release(); + } + + LOG_CAUGHT_EXCEPTION(); + + return nullptr; + } +} + +[[nodiscard]] +HRESULT WindowUiaProvider::Signal(_In_ EVENTID id) +{ + HRESULT hr = S_OK; + + // ScreenInfoUiaProvider is responsible for signaling selection + // changed events and text changed events + if (id == UIA_Text_TextSelectionChangedEventId || + id == UIA_Text_TextChangedEventId) + { + if (_pScreenInfoProvider) + { + hr = _pScreenInfoProvider->Signal(id); + } + else + { + hr = E_POINTER; + } + return hr; + } + + if (_signalEventFiring.find(id) != _signalEventFiring.end() && + _signalEventFiring[id] == true) + { + return hr; + } + + try + { + _signalEventFiring[id] = true; + } + CATCH_RETURN(); + + IRawElementProviderSimple* pProvider = static_cast(this); + hr = UiaRaiseAutomationEvent(pProvider, id); + _signalEventFiring[id] = false; + + // tracing + ApiMessageSignal apiMsg; + apiMsg.Signal = id; + Tracing::s_TraceUia(this, ApiCall::Signal, &apiMsg); + + return hr; +} + +[[nodiscard]] +HRESULT WindowUiaProvider::SetTextAreaFocus() +{ + try + { + return _pScreenInfoProvider->Signal(UIA_AutomationFocusChangedEventId); + } + CATCH_RETURN(); +} + +#pragma region IUnknown + +IFACEMETHODIMP_(ULONG) WindowUiaProvider::AddRef() +{ + Tracing::s_TraceUia(this, ApiCall::AddRef, nullptr); + return InterlockedIncrement(&_cRefs); +} + +IFACEMETHODIMP_(ULONG) WindowUiaProvider::Release() +{ + Tracing::s_TraceUia(this, ApiCall::Release, nullptr); + long val = InterlockedDecrement(&_cRefs); + if (val == 0) + { + delete this; + } + return val; +} + +IFACEMETHODIMP WindowUiaProvider::QueryInterface(_In_ REFIID riid, _COM_Outptr_result_maybenull_ void** ppInterface) +{ + Tracing::s_TraceUia(this, ApiCall::QueryInterface, nullptr); + if (riid == __uuidof(IUnknown)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(IRawElementProviderSimple)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(IRawElementProviderFragment)) + { + *ppInterface = static_cast(this); + } + else if (riid == __uuidof(IRawElementProviderFragmentRoot)) + { + *ppInterface = static_cast(this); + } + else + { + *ppInterface = nullptr; + return E_NOINTERFACE; + } + + (static_cast(*ppInterface))->AddRef(); + + return S_OK; +} + +#pragma endregion + +#pragma region IRawElementProviderSimple + +// Implementation of IRawElementProviderSimple::get_ProviderOptions. +// Gets UI Automation provider options. +IFACEMETHODIMP WindowUiaProvider::get_ProviderOptions(_Out_ ProviderOptions* pOptions) +{ + Tracing::s_TraceUia(this, ApiCall::GetProviderOptions, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + *pOptions = ProviderOptions_ServerSideProvider; + return S_OK; +} + +// Implementation of IRawElementProviderSimple::get_PatternProvider. +// Gets the object that supports ISelectionPattern. +IFACEMETHODIMP WindowUiaProvider::GetPatternProvider(_In_ PATTERNID /*patternId*/, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface) +{ + Tracing::s_TraceUia(this, ApiCall::GetPatternProvider, nullptr); + *ppInterface = nullptr; + RETURN_IF_FAILED(_EnsureValidHwnd()); + + return S_OK; +} + +// Implementation of IRawElementProviderSimple::get_PropertyValue. +// Gets custom properties. +IFACEMETHODIMP WindowUiaProvider::GetPropertyValue(_In_ PROPERTYID propertyId, _Out_ VARIANT* pVariant) +{ + Tracing::s_TraceUia(this, ApiCall::GetPropertyValue, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + pVariant->vt = VT_EMPTY; + + // Returning the default will leave the property as the default + // so we only really need to touch it for the properties we want to implement + if (propertyId == UIA_ControlTypePropertyId) + { + pVariant->vt = VT_I4; + pVariant->lVal = UIA_WindowControlTypeId; + } + else if (propertyId == UIA_AutomationIdPropertyId) + { + pVariant->bstrVal = SysAllocString(L"Console Window"); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + } + else if (propertyId == UIA_IsControlElementPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_IsContentElementPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_IsKeyboardFocusablePropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_HasKeyboardFocusPropertyId) + { + pVariant->vt = VT_BOOL; + pVariant->boolVal = VARIANT_TRUE; + } + else if (propertyId == UIA_ProviderDescriptionPropertyId) + { + pVariant->bstrVal = SysAllocString(L"Microsoft Console Host Window"); + if (pVariant->bstrVal != nullptr) + { + pVariant->vt = VT_BSTR; + } + } + + return S_OK; +} + +// Implementation of IRawElementProviderSimple::get_HostRawElementProvider. +// Gets the default UI Automation provider for the host window. This provider +// supplies many properties. +IFACEMETHODIMP WindowUiaProvider::get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::GetHostRawElementProvider, nullptr); + try + { + const HWND hwnd = _GetWindowHandle(); + return UiaHostProviderFromHwnd(hwnd, ppProvider); + } + catch(...) + { + return static_cast(UIA_E_ELEMENTNOTAVAILABLE); + } +} +#pragma endregion + +#pragma region IRawElementProviderFragment + +IFACEMETHODIMP WindowUiaProvider::Navigate(_In_ NavigateDirection direction, _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) +{ + ApiMsgNavigate apiMsg; + apiMsg.Direction = direction; + Tracing::s_TraceUia(this, ApiCall::Navigate, &apiMsg); + + RETURN_IF_FAILED(_EnsureValidHwnd()); + *ppProvider = nullptr; + HRESULT hr = S_OK; + + if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild) + { + *ppProvider = _pScreenInfoProvider; + (*ppProvider)->AddRef(); + + // signal that the focus changed + LOG_IF_FAILED(_pScreenInfoProvider->Signal(UIA_AutomationFocusChangedEventId)); + } + + // For the other directions (parent, next, previous) the default of nullptr is correct + return hr; +} + +IFACEMETHODIMP WindowUiaProvider::GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId) +{ + Tracing::s_TraceUia(this, ApiCall::GetRuntimeId, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + // Root defers this to host, others must implement it... + *ppRuntimeId = nullptr; + + return S_OK; +} + +IFACEMETHODIMP WindowUiaProvider::get_BoundingRectangle(_Out_ UiaRect* pRect) +{ + Tracing::s_TraceUia(this, ApiCall::GetBoundingRectangle, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + const IConsoleWindow* const pIConsoleWindow = _getIConsoleWindow(); + RETURN_HR_IF_NULL((HRESULT)UIA_E_ELEMENTNOTAVAILABLE, pIConsoleWindow); + + RECT const rc = pIConsoleWindow->GetWindowRect(); + + pRect->left = rc.left; + pRect->top = rc.top; + pRect->width = rc.right - rc.left; + pRect->height = rc.bottom - rc.top; + + return S_OK; +} + +IFACEMETHODIMP WindowUiaProvider::GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots) +{ + Tracing::s_TraceUia(this, ApiCall::GetEmbeddedFragmentRoots, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + *ppRoots = nullptr; + return S_OK; +} + +IFACEMETHODIMP WindowUiaProvider::SetFocus() +{ + Tracing::s_TraceUia(this, ApiCall::SetFocus, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + return Signal(UIA_AutomationFocusChangedEventId); +} + +IFACEMETHODIMP WindowUiaProvider::get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::GetFragmentRoot, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + *ppProvider = this; + AddRef(); + return S_OK; +} + +#pragma endregion + +#pragma region IRawElementProviderFragmentRoot + +IFACEMETHODIMP WindowUiaProvider::ElementProviderFromPoint(_In_ double /*x*/, + _In_ double /*y*/, + _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::ElementProviderFromPoint, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + + *ppProvider = _pScreenInfoProvider; + (*ppProvider)->AddRef(); + + return S_OK; +} + +IFACEMETHODIMP WindowUiaProvider::GetFocus(_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider) +{ + Tracing::s_TraceUia(this, ApiCall::GetFocus, nullptr); + RETURN_IF_FAILED(_EnsureValidHwnd()); + return _pScreenInfoProvider->QueryInterface(IID_PPV_ARGS(ppProvider)); +} + +#pragma endregion + +HWND WindowUiaProvider::_GetWindowHandle() const +{ + IConsoleWindow* const pIConsoleWindow = _getIConsoleWindow(); + THROW_HR_IF_NULL(E_POINTER, pIConsoleWindow); + + return pIConsoleWindow->GetWindowHandle(); +} + +[[nodiscard]] +HRESULT WindowUiaProvider::_EnsureValidHwnd() const +{ + try + { + HWND const hwnd = _GetWindowHandle(); + RETURN_HR_IF((HRESULT)UIA_E_ELEMENTNOTAVAILABLE, !(IsWindow(hwnd))); + } + CATCH_RETURN(); + return S_OK; +} + +IConsoleWindow* const WindowUiaProvider::_getIConsoleWindow() +{ + return ServiceLocator::LocateConsoleWindow(); +} diff --git a/src/interactivity/win32/windowUiaProvider.hpp b/src/interactivity/win32/windowUiaProvider.hpp new file mode 100644 index 000000000..c4e4a436c --- /dev/null +++ b/src/interactivity/win32/windowUiaProvider.hpp @@ -0,0 +1,137 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- windowUiaProvider.hpp + +Abstract: +- This module provides UI Automation access to the console window to + support both automation tests and accessibility (screen reading) + applications. +- Based on examples, sample code, and guidance from + https://msdn.microsoft.com/en-us/library/windows/desktop/ee671596(v=vs.85).aspx + +Author(s): +- Michael Niksa (MiNiksa) 2017 +- Austin Diviness (AustDi) 2017 +--*/ + +#pragma once + +#include "precomp.h" + +namespace Microsoft::Console::Interactivity::Win32 +{ + // Forward declare, prevent circular ref. + class Window; + class ScreenInfoUiaProvider; + + class WindowUiaProvider final : public IRawElementProviderSimple, + public IRawElementProviderFragment, + public IRawElementProviderFragmentRoot + { + public: + static WindowUiaProvider* Create(); + virtual ~WindowUiaProvider(); + + [[nodiscard]] + HRESULT Signal(_In_ EVENTID id); + [[nodiscard]] + HRESULT SetTextAreaFocus(); + + // IUnknown methods + IFACEMETHODIMP_(ULONG) AddRef(); + IFACEMETHODIMP_(ULONG) Release(); + IFACEMETHODIMP QueryInterface(_In_ REFIID riid, + _COM_Outptr_result_maybenull_ void** ppInterface); + + // IRawElementProviderSimple methods + IFACEMETHODIMP get_ProviderOptions(_Out_ ProviderOptions* pOptions); + IFACEMETHODIMP GetPatternProvider(_In_ PATTERNID iid, + _COM_Outptr_result_maybenull_ IUnknown** ppInterface); + IFACEMETHODIMP GetPropertyValue(_In_ PROPERTYID idProp, + _Out_ VARIANT* pVariant); + IFACEMETHODIMP get_HostRawElementProvider(_COM_Outptr_result_maybenull_ IRawElementProviderSimple** ppProvider); + + // IRawElementProviderFragment methods + IFACEMETHODIMP Navigate(_In_ NavigateDirection direction, + _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider); + IFACEMETHODIMP GetRuntimeId(_Outptr_result_maybenull_ SAFEARRAY** ppRuntimeId); + IFACEMETHODIMP get_BoundingRectangle(_Out_ UiaRect* pRect); + IFACEMETHODIMP GetEmbeddedFragmentRoots(_Outptr_result_maybenull_ SAFEARRAY** ppRoots); + IFACEMETHODIMP SetFocus(); + IFACEMETHODIMP get_FragmentRoot(_COM_Outptr_result_maybenull_ IRawElementProviderFragmentRoot** ppProvider); + + // IRawElementProviderFragmentRoot methods + IFACEMETHODIMP ElementProviderFromPoint(_In_ double x, + _In_ double y, + _COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider); + IFACEMETHODIMP GetFocus(_COM_Outptr_result_maybenull_ IRawElementProviderFragment** ppProvider); + + private: + WindowUiaProvider(); + + HWND _GetWindowHandle() const; + [[nodiscard]] + HRESULT _EnsureValidHwnd() const; + static IConsoleWindow* const _getIConsoleWindow(); + + + // this is used to prevent the object from + // signaling an event while it is already in the + // process of signalling another event. + // This fixes a problem with JAWS where it would + // call a public method that calls + // UiaRaiseAutomationEvent to signal something + // happened, which JAWS then detects the signal + // and calls the same method in response, + // eventually overflowing the stack. + // We aren't using this as a cheap locking + // mechanism for multi-threaded code. + std::map _signalEventFiring; + + ScreenInfoUiaProvider* _pScreenInfoProvider; + + // Ref counter for COM object + ULONG _cRefs; + }; + + namespace WindowUiaProviderTracing + { + enum class ApiCall + { + Create, + Signal, + AddRef, + Release, + QueryInterface, + GetProviderOptions, + GetPatternProvider, + GetPropertyValue, + GetHostRawElementProvider, + Navigate, + GetRuntimeId, + GetBoundingRectangle, + GetEmbeddedFragmentRoots, + SetFocus, + GetFragmentRoot, + ElementProviderFromPoint, + GetFocus + }; + + struct IApiMsg + { + }; + + struct ApiMessageSignal : public IApiMsg + { + EVENTID Signal; + }; + + struct ApiMsgNavigate : public IApiMsg + { + NavigateDirection Direction; + }; + } +} diff --git a/src/interactivity/win32/windowdpiapi.cpp b/src/interactivity/win32/windowdpiapi.cpp new file mode 100644 index 000000000..d771957f1 --- /dev/null +++ b/src/interactivity/win32/windowdpiapi.cpp @@ -0,0 +1,260 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "windowdpiapi.hpp" + +using namespace Microsoft::Console::Interactivity::Win32; + +#pragma region Public Methods + +#pragma region IHighDpiApi Members + +[[nodiscard]] +HRESULT WindowDpiApi::SetProcessPerMonitorDpiAwareness() +{ + return SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); +} + +BOOL WindowDpiApi::SetProcessDpiAwarenessContext() +{ + return SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); +} + +BOOL WindowDpiApi::EnablePerMonitorDialogScaling() +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + static bool fTried = false; + static FARPROC pfnFunc = nullptr; + + if (!fTried) + { + pfnFunc = GetProcAddress(_hUser32, "EnablePerMonitorDialogScaling"); + + if (pfnFunc == nullptr) + { + // If retrieve by name fails, we have to get it by the ordinal because it was made a secret. + pfnFunc = GetProcAddress(_hUser32, MAKEINTRESOURCEA(2577)); + } + + fTried = true; + } + + if (pfnFunc != nullptr) + { + return (BOOL)pfnFunc(); + } + } + + return FALSE; +#else + return EnablePerMonitorDialogScaling(); +#endif +} + +#pragma endregion + +BOOL WindowDpiApi::SetProcessDpiAwarenessContext(_In_ DPI_AWARENESS_CONTEXT dpiContext) +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef int(WINAPI *PfnSetProcessDpiAwarenessContexts)(DPI_AWARENESS_CONTEXT dpiContext); + + static bool fTried = false; + static PfnSetProcessDpiAwarenessContexts pfn = nullptr; + + if (!fTried) + { + pfn = (PfnSetProcessDpiAwarenessContexts)GetProcAddress(_hUser32, "SetProcessDpiAwarenessContext"); + } + + fTried = true; + + if (pfn != nullptr) + { + return pfn(dpiContext); + } + + } + + return FALSE; +#else + return EnableChildWindowDpiMessage(dpiContext); +#endif +} + +BOOL WindowDpiApi::EnableChildWindowDpiMessage(const HWND hwnd, const BOOL fEnable) +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef BOOL(WINAPI *PfnEnableChildWindowDpiMessage)(HWND hwnd, BOOL fEnable); + + static bool fTried = false; + static PfnEnableChildWindowDpiMessage pfn = nullptr; + + if (!fTried) + { + pfn = (PfnEnableChildWindowDpiMessage)GetProcAddress(_hUser32, "EnableChildWindowDpiMessage"); + + if (pfn == nullptr) + { + // If that fails, we have to get it by the ordinal because it was made a secret in RS1. + pfn = (PfnEnableChildWindowDpiMessage)GetProcAddress(_hUser32, MAKEINTRESOURCEA(2704)); + } + + fTried = true; + } + + if (pfn != nullptr) + { + return pfn(hwnd, fEnable); + } + } + + return FALSE; +#else + return EnableChildWindowDpiMessage(hwnd, fEnable); +#endif +} + +BOOL WindowDpiApi::AdjustWindowRectExForDpi(_Inout_ LPRECT const lpRect, + const DWORD dwStyle, + const BOOL bMenu, + const DWORD dwExStyle, + const UINT dpi) +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef BOOL(WINAPI *PfnAdjustWindowRectExForDpi)(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, int dpi); + + static bool fTried = false; + static PfnAdjustWindowRectExForDpi pfn = nullptr; + + if (!fTried) + { + // Try to retrieve it the RS1 way first + pfn = (PfnAdjustWindowRectExForDpi)GetProcAddress(_hUser32, "AdjustWindowRectExForDpi"); + + if (pfn == nullptr) + { + // If that fails, we have to get it by the ordinal because it was made a secret in TH/TH2. + pfn = (PfnAdjustWindowRectExForDpi)GetProcAddress(_hUser32, MAKEINTRESOURCEA(2580)); + } + + fTried = true; + } + + if (pfn != nullptr) + { + return pfn(lpRect, dwStyle, bMenu, dwExStyle, dpi); + } + } + + return AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); +#else + return AdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); +#endif +} + +int WindowDpiApi::GetWindowDPI(const HWND hwnd) +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef int(WINAPI *PfnGetWindowDPI)(HWND hwnd); + + static bool fTried = false; + static PfnGetWindowDPI pfn = nullptr; + + if (!fTried) + { + pfn = (PfnGetWindowDPI)GetProcAddress(_hUser32, "GetWindowDPI"); + + if (pfn == nullptr) + { + // If that fails, we have to get it by the ordinal because it was made a secret in RS1. + pfn = (PfnGetWindowDPI)GetProcAddress(_hUser32, MAKEINTRESOURCEA(2707)); + } + + fTried = true; + } + + if (pfn != nullptr) + { + return pfn(hwnd); + } + } + + return USER_DEFAULT_SCREEN_DPI; +#else + // GetDpiForWindow is the public API version (as of RS1) of GetWindowDPI + return GetDpiForWindow(hwnd); +#endif +} + +int WindowDpiApi::GetSystemMetricsForDpi(const int nIndex, const UINT dpi) +{ +#ifdef CON_DPIAPI_INDIRECT + if (_hUser32 != nullptr) + { + typedef int(WINAPI *PfnGetDpiMetrics)(int nIndex, int dpi); + + static bool fTried = false; + static PfnGetDpiMetrics pfn = nullptr; + + if (!fTried) + { + // First try the TH1/TH2 name of the function. + pfn = (PfnGetDpiMetrics)GetProcAddress(_hUser32, "GetDpiMetrics"); + + if (pfn == nullptr) + { + // If not, then try the RS1 name of the function. + pfn = (PfnGetDpiMetrics)GetProcAddress(_hUser32, "GetSystemMetricsForDpi"); + } + + fTried = true; + } + + if (pfn != nullptr) + { + return pfn(nIndex, dpi); + } + + } + + return GetSystemMetrics(nIndex); +#else + return GetSystemMetricsForDpi(nIndex, dpi); +#endif +} + +#pragma endregion + +#pragma region Private Methods + +#ifdef CON_DPIAPI_INDIRECT +WindowDpiApi::WindowDpiApi() +{ + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _hUser32 = LoadLibraryExW(L"user32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); +} +#endif + +WindowDpiApi::~WindowDpiApi() +{ + if (_hUser32 != nullptr) + { + FreeLibrary(_hUser32); + _hUser32 = nullptr; + } +} + +#pragma endregion diff --git a/src/interactivity/win32/windowdpiapi.hpp b/src/interactivity/win32/windowdpiapi.hpp new file mode 100644 index 000000000..83321f139 --- /dev/null +++ b/src/interactivity/win32/windowdpiapi.hpp @@ -0,0 +1,78 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- windowdpiapi.hpp + +Abstract: +- This module is used for abstracting calls to High DPI APIs. + +Author(s): +- Michael Niksa (MiNiksa) Apr-2016 +--*/ +#pragma once + +#include "..\inc\IHighDpiApi.hpp" + +// Uncomment to build ConFans or other down-level build scenarios. +// #define CON_DPIAPI_INDIRECT + +// To avoid a break when the RS1 SDK gets dropped in, don't redef. +#ifndef _DPI_AWARENESS_CONTEXTS_ + +DECLARE_HANDLE(DPI_AWARENESS_CONTEXT); + +typedef enum DPI_AWARENESS { + DPI_AWARENESS_INVALID = -1, + DPI_AWARENESS_UNAWARE = 0, + DPI_AWARENESS_SYSTEM_AWARE = 1, + DPI_AWARENESS_PER_MONITOR_AWARE = 2 +} DPI_AWARENESS; + +#define DPI_AWARENESS_CONTEXT_UNAWARE ((DPI_AWARENESS_CONTEXT)-1) +#define DPI_AWARENESS_CONTEXT_SYSTEM_AWARE ((DPI_AWARENESS_CONTEXT)-2) +#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE ((DPI_AWARENESS_CONTEXT)-3) + +#endif + +// This type is being defined in RS2 but is percolating through the +// tree. Def it here if it hasn't collided with our branch yet. +#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 +#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 ((DPI_AWARENESS_CONTEXT)-4) +#endif + +namespace Microsoft::Console::Interactivity::Win32 +{ + class WindowDpiApi final : public IHighDpiApi + { + public: + // IHighDpi Interface + BOOL SetProcessDpiAwarenessContext(); + [[nodiscard]] + HRESULT SetProcessPerMonitorDpiAwareness(); + BOOL EnablePerMonitorDialogScaling(); + + // Module-internal Functions + BOOL SetProcessDpiAwarenessContext(_In_ DPI_AWARENESS_CONTEXT dpiContext); + BOOL EnableChildWindowDpiMessage(const HWND hwnd, + const BOOL fEnable); + BOOL AdjustWindowRectExForDpi(_Inout_ LPRECT const lpRect, + const DWORD dwStyle, + const BOOL bMenu, + const DWORD dwExStyle, + const UINT dpi); + + int GetWindowDPI(const HWND hwnd); + int GetSystemMetricsForDpi(const int nIndex, + const UINT dpi); + +#ifdef CON_DPIAPI_INDIRECT + WindowDpiApi(); +#endif + ~WindowDpiApi(); + + private: + HMODULE _hUser32; + }; +} diff --git a/src/interactivity/win32/windowime.hpp b/src/interactivity/win32/windowime.hpp new file mode 100644 index 000000000..dc44c7f2d --- /dev/null +++ b/src/interactivity/win32/windowime.hpp @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#pragma hdrstop + +RECT GetImeSuggestionWindowPos(); diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp new file mode 100644 index 000000000..435cc7193 --- /dev/null +++ b/src/interactivity/win32/windowio.cpp @@ -0,0 +1,1086 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "windowio.hpp" + +#include "ConsoleControl.hpp" +#include "find.h" +#include "clipboard.hpp" +#include "consoleKeyInfo.hpp" +#include "window.hpp" + +#include "..\..\host\ApiRoutines.h" +#include "..\..\host\init.hpp" +#include "..\..\host\input.h" +#include "..\..\host\handle.h" +#include "..\..\host\scrolling.hpp" +#include "..\..\host\output.h" + +#include "..\inc\ServiceLocator.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Interactivity::Win32; + +// For usage with WM_SYSKEYDOWN message processing. +// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms646286(v=vs.85).aspx +// Bit 29 is whether ALT was held when the message was posted. +#define WM_SYSKEYDOWN_ALT_PRESSED (0x20000000) + +// ---------------------------- +// Helpers +// ---------------------------- + +ULONG ConvertMouseButtonState(_In_ ULONG Flag, _In_ ULONG State) +{ + if (State & MK_LBUTTON) + { + Flag |= FROM_LEFT_1ST_BUTTON_PRESSED; + } + if (State & MK_MBUTTON) + { + Flag |= FROM_LEFT_2ND_BUTTON_PRESSED; + } + if (State & MK_RBUTTON) + { + Flag |= RIGHTMOST_BUTTON_PRESSED; + } + + return Flag; +} + +/* +* This routine tells win32k what process we want to use to masquerade as the +* owner of conhost's window. If ProcessData is nullptr that means the root process +* has exited so we need to find any old process to be the owner. If this console +* has no processes attached to it -- it's only being kept alive by references +* via IO handles -- then we'll just set the owner to conhost.exe itself. +*/ +VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pProcessData) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + FAIL_FAST_IF(!(gci.IsConsoleLocked())); + + DWORD dwProcessId; + DWORD dwThreadId; + if (nullptr != pProcessData) + { + dwProcessId = pProcessData->dwProcessId; + dwThreadId = pProcessData->dwThreadId; + } + else + { + // Find a process to own the console window. If there are none then let's use conhost's. + pProcessData = gci.ProcessHandleList.GetFirstProcess(); + if (pProcessData != nullptr) + { + dwProcessId = pProcessData->dwProcessId; + dwThreadId = pProcessData->dwThreadId; + pProcessData->fRootProcess = true; + } + else + { + dwProcessId = GetCurrentProcessId(); + dwThreadId = GetCurrentThreadId(); + } + } + + CONSOLEWINDOWOWNER ConsoleOwner; + ConsoleOwner.hwnd = hwnd; + ConsoleOwner.ProcessId = dwProcessId; + ConsoleOwner.ThreadId = dwThreadId; + + // Comment out this line to enable UIA tree to be visible until UIAutomationCore.dll can support our scenario. + LOG_IF_FAILED(ServiceLocator::LocateConsoleControl() + ->Control(ConsoleControl::ControlType::ConsoleSetWindowOwner, + &ConsoleOwner, + sizeof(ConsoleOwner))); +} + +// ---------------------------- +// Window Message Handlers +// (called by windowproc) +// ---------------------------- + +// Routine Description: +// - Handler for detecting whether a key-press event can be appropriately converted into a terminal sequence. +// Will only trigger when virtual terminal input mode is set via STDIN handle +// Arguments: +// - pInputRecord - Input record event from the general input event handler +// Return Value: +// - True if the modes were appropriate for converting to a terminal sequence AND there was a matching terminal sequence for this key. False otherwise. +bool HandleTerminalMouseEvent(const COORD cMousePosition, + const unsigned int uiButton, + const short sModifierKeystate, + const short sWheelDelta) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + // If the modes don't align, this is unhandled by default. + bool fWasHandled = false; + + // Virtual terminal input mode + if (IsInVirtualTerminalInputMode()) + { + fWasHandled = gci.terminalMouseInput.HandleMouse(cMousePosition, uiButton, sModifierKeystate, sWheelDelta); + } + + return fWasHandled; +} + +void HandleKeyEvent(const HWND hWnd, + const UINT Message, + const WPARAM wParam, + const LPARAM lParam, + _Inout_opt_ PBOOL pfUnlockConsole) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + + // BOGUS for WM_CHAR/WM_DEADCHAR, in which LOWORD(lParam) is a character + WORD VirtualKeyCode = LOWORD(wParam); + WORD VirtualScanCode = LOBYTE(HIWORD(lParam)); + const WORD RepeatCount = LOWORD(lParam); + const ULONG ControlKeyState = GetControlKeyState(lParam); + const BOOL bKeyDown = WI_IsFlagClear(lParam, KEY_TRANSITION_UP); + + if (bKeyDown) + { + // Log a telemetry flag saying the user interacted with the Console + // Only log when the key is a down press. Otherwise we're getting many calls with + // Message = WM_CHAR, VirtualKeyCode = VK_TAB, with bKeyDown = false + // when nothing is happening, or the user has merely clicked on the title bar, and + // this can incorrectly mark the session as being interactive. + Telemetry::Instance().SetUserInteractive(); + } + + // Make sure we retrieve the key info first, or we could chew up + // unneeded space in the key info table if we bail out early. + if (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR) + { + // --- START LOAD BEARING CODE --- + // NOTE: We MUST match up the original data from the WM_KEYDOWN stroke (handled at some inexact moment in the + // past by TranslateMessageEx) with the WM_CHAR we are processing now to ensure we have the correct + // wVirtualScanCode to associate with the message and pass down into the console input queue for further + // processing. + // This is required because we cannot accurately re-synthesize (using MapVirtualKey/Ex) + // the original scan code just based on the information we have now and the scan code might be + // required by the underlying client application, processed input handler (inside the console), + // or other input channels to help portray certain key sequences. + // Most notably this affects Ctrl-C, Ctrl-Break, and Pause/Break among others. + // + RetrieveKeyInfo(hWnd, + &VirtualKeyCode, + &VirtualScanCode, + !gci.pInputBuffer->fInComposition); + + // --- END LOAD BEARING CODE --- + } + + KeyEvent keyEvent{ !!bKeyDown, RepeatCount, VirtualKeyCode, VirtualScanCode, UNICODE_NULL, 0 }; + + if (Message == WM_CHAR || Message == WM_SYSCHAR || Message == WM_DEADCHAR || Message == WM_SYSDEADCHAR) + { + // If this is a fake character, zero the scancode. + if (lParam & 0x02000000) + { + keyEvent.SetVirtualScanCode(0); + } + keyEvent.SetActiveModifierKeys(GetControlKeyState(lParam)); + if (Message == WM_CHAR || Message == WM_SYSCHAR) + { + keyEvent.SetCharData(static_cast(wParam)); + } + else + { + keyEvent.SetCharData(0); + } + } + else + { + // if alt-gr, ignore + if (lParam & 0x02000000) + { + return; + } + keyEvent.SetActiveModifierKeys(ControlKeyState); + keyEvent.SetCharData(0); + } + + const INPUT_KEY_INFO inputKeyInfo(VirtualKeyCode, ControlKeyState); + + // Capture telemetry on Ctrl+Shift+ C or V commands + if (IsInProcessedInputMode()) + { + // Capture telemetry data when a user presses ctrl+shift+c or v in processed mode + if (inputKeyInfo.IsShiftAndCtrlOnly()) + { + if (VirtualKeyCode == 'V') + { + Telemetry::Instance().LogCtrlShiftVProcUsed(); + } + else if (VirtualKeyCode == 'C') + { + Telemetry::Instance().LogCtrlShiftCProcUsed(); + } + } + } + else + { + // Capture telemetry data when a user presses ctrl+shift+c or v in raw mode + if (inputKeyInfo.IsShiftAndCtrlOnly()) + { + if (VirtualKeyCode == 'V') + { + Telemetry::Instance().LogCtrlShiftVRawUsed(); + } + else if (VirtualKeyCode == 'C') + { + Telemetry::Instance().LogCtrlShiftCRawUsed(); + } + } + } + + // If this is a key up message, should we ignore it? We do this so that if a process reads a line from the input + // buffer, the key up event won't get put in the buffer after the read completes. + if (gci.Flags & CONSOLE_IGNORE_NEXT_KEYUP) + { + gci.Flags &= ~CONSOLE_IGNORE_NEXT_KEYUP; + if (!bKeyDown) + { + return; + } + } + + Selection* pSelection = &Selection::Instance(); + + if (bKeyDown && gci.GetInterceptCopyPaste() && inputKeyInfo.IsShiftAndCtrlOnly()) + { + // Intercept C-S-v to paste + switch (VirtualKeyCode) + { + case 'V': + // the user is attempting to paste from the clipboard + Telemetry::Instance().SetKeyboardTextEditingUsed(); + Clipboard::Instance().Paste(); + return; + } + } + else if (!IsInVirtualTerminalInputMode()) + { + // First attempt to process simple key chords (Ctrl+Key) + if (inputKeyInfo.IsCtrlOnly() && ShouldTakeOverKeyboardShortcuts() && bKeyDown) + { + switch (VirtualKeyCode) + { + case 'A': + // Set Text Selection using keyboard to true for telemetry + Telemetry::Instance().SetKeyboardTextSelectionUsed(); + // the user is asking to select all + pSelection->SelectAll(); + return; + case 'F': + // the user is asking to go to the find window + DoFind(); + *pfUnlockConsole = FALSE; + return; + case 'M': + // the user is asking for mark mode + Selection::Instance().InitializeMarkSelection(); + return; + case 'V': + // the user is attempting to paste from the clipboard + Telemetry::Instance().SetKeyboardTextEditingUsed(); + Clipboard::Instance().Paste(); + return; + case VK_HOME: + case VK_END: + case VK_UP: + case VK_DOWN: + // if the user is asking for keyboard scroll, give it to them + if (Scrolling::s_HandleKeyScrollingEvent(&inputKeyInfo)) + { + return; + } + break; + case VK_PRIOR: + case VK_NEXT: + Telemetry::Instance().SetCtrlPgUpPgDnUsed(); + break; + } + } + + // Handle F11 fullscreen toggle + if (VirtualKeyCode == VK_F11 && + bKeyDown && + inputKeyInfo.HasNoModifiers() && + ShouldTakeOverKeyboardShortcuts()) + { + ServiceLocator::LocateConsoleWindow()->ToggleFullscreen(); + return; + } + + // handle shift-ins paste + if (inputKeyInfo.IsShiftOnly() && ShouldTakeOverKeyboardShortcuts()) + { + if (!bKeyDown) + { + return; + } + else if (VirtualKeyCode == VK_INSERT && !(pSelection->IsInSelectingState() && pSelection->IsKeyboardMarkSelection())) + { + Clipboard::Instance().Paste(); + return; + } + } + + // handle ctrl+shift+plus/minus for transparency adjustment + if (inputKeyInfo.IsShiftAndCtrlOnly() && ShouldTakeOverKeyboardShortcuts()) + { + if (!bKeyDown) + { + return; + } + else + { + //This is the only place where the window opacity is changed NOT due to the props sheet. + short opacityDelta = 0; + if (VirtualKeyCode == VK_OEM_PLUS || VirtualKeyCode == VK_ADD) + { + opacityDelta = OPACITY_DELTA_INTERVAL; + } + else if (VirtualKeyCode == VK_OEM_MINUS || VirtualKeyCode == VK_SUBTRACT) + { + opacityDelta = -OPACITY_DELTA_INTERVAL; + } + if (opacityDelta != 0) + { + ServiceLocator::LocateConsoleWindow()->ChangeWindowOpacity(opacityDelta); + return; + } + + } + } + } + + // Then attempt to process more complicated selection/scrolling commands that require state. + // These selection and scrolling functions must go after the simple key-chord combinations + // as they have the potential to modify state in a way those functions do not expect. + if (gci.Flags & CONSOLE_SELECTING) + { + if (!bKeyDown) + { + return; + } + + Selection::KeySelectionEventResult handlingResult = pSelection->HandleKeySelectionEvent(&inputKeyInfo); + if (handlingResult == Selection::KeySelectionEventResult::CopyToClipboard) + { + // If the ALT key is held, also select HTML as well as plain text. + bool const fAlsoSelectHtml = WI_IsFlagSet(GetKeyState(VK_MENU), KEY_PRESSED); + Clipboard::Instance().Copy(fAlsoSelectHtml); + return; + } + else if (handlingResult == Selection::KeySelectionEventResult::EventHandled) + { + return; + } + } + if (Scrolling::s_IsInScrollMode()) + { + if (!bKeyDown || Scrolling::s_HandleKeyScrollingEvent(&inputKeyInfo)) + { + return; + } + } + // we need to check if there is an active popup because otherwise they won't be able to receive shift+key events + if (pSelection->s_IsValidKeyboardLineSelection(&inputKeyInfo) && IsInProcessedInputMode() && gci.PopupCount.load() == 0) + { + if (!bKeyDown || pSelection->HandleKeyboardLineSelectionEvent(&inputKeyInfo)) + { + return; + } + } + + // if the user is inputting chars at an inappropriate time, beep. + if ((gci.Flags & (CONSOLE_SELECTING | CONSOLE_SCROLLING | CONSOLE_SCROLLBAR_TRACKING)) && + bKeyDown && + !IsSystemKey(VirtualKeyCode)) + { + ServiceLocator::LocateConsoleWindow()->SendNotifyBeep(); + return; + } + + if (gci.pInputBuffer->fInComposition) + { + return; + } + + bool generateBreak = false; + // ignore key strokes that will generate CHAR messages. this is only necessary while a dialog box is up. + if (ServiceLocator::LocateGlobals().uiDialogBoxCount != 0) + { + if (Message != WM_CHAR && Message != WM_SYSCHAR && Message != WM_DEADCHAR && Message != WM_SYSDEADCHAR) + { + WCHAR awch[MAX_CHARS_FROM_1_KEYSTROKE]; + BYTE KeyState[256]; + if (GetKeyboardState(KeyState)) + { + int cwch = ToUnicodeEx((UINT)wParam, HIWORD(lParam), KeyState, awch, ARRAYSIZE(awch), TM_POSTCHARBREAKS, nullptr); + if (cwch != 0) + { + return; + } + } + else + { + return; + } + } + else + { + // remember to generate break + if (Message == WM_CHAR) + { + generateBreak = true; + } + } + } + + HandleGenericKeyEvent(keyEvent, generateBreak); +} + +// Routine Description: +// - Returns TRUE if DefWindowProc should be called. +BOOL HandleSysKeyEvent(const HWND hWnd, const UINT Message, const WPARAM wParam, const LPARAM lParam, _Inout_opt_ PBOOL pfUnlockConsole) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + WORD VirtualKeyCode; + + if (Message == WM_SYSCHAR || Message == WM_SYSDEADCHAR) + { + VirtualKeyCode = (WORD)MapVirtualKeyW(LOBYTE(HIWORD(lParam)), MAPVK_VSC_TO_VK_EX); + } + else + { + VirtualKeyCode = LOWORD(wParam); + } + + // Log a telemetry flag saying the user interacted with the Console + Telemetry::Instance().SetUserInteractive(); + + // check for ctrl-esc + BOOL const bCtrlDown = GetKeyState(VK_CONTROL) & KEY_PRESSED; + + if (VirtualKeyCode == VK_ESCAPE && + bCtrlDown && !(GetKeyState(VK_MENU) & KEY_PRESSED) && !(GetKeyState(VK_SHIFT) & KEY_PRESSED)) + { + return TRUE; // call DefWindowProc + } + + // check for alt-f4 + if (VirtualKeyCode == VK_F4 && (GetKeyState(VK_MENU) & KEY_PRESSED) && IsInProcessedInputMode() && gci.IsAltF4CloseAllowed()) + { + return TRUE; // let DefWindowProc generate WM_CLOSE + } + + if (WI_IsFlagClear(lParam, WM_SYSKEYDOWN_ALT_PRESSED)) + { // we're iconic + // Check for ENTER while iconic (restore accelerator). + if (VirtualKeyCode == VK_RETURN) + { + + return TRUE; // call DefWindowProc + } + else + { + HandleKeyEvent(hWnd, Message, wParam, lParam, pfUnlockConsole); + return FALSE; + } + } + + if (VirtualKeyCode == VK_RETURN && !bCtrlDown) + { + // only toggle on keydown + if (!(lParam & KEY_TRANSITION_UP)) + { + ServiceLocator::LocateConsoleWindow()->ToggleFullscreen(); + } + + return FALSE; + } + + // make sure alt-space gets translated so that the system menu is displayed. + if (!(GetKeyState(VK_CONTROL) & KEY_PRESSED)) + { + if (VirtualKeyCode == VK_SPACE) + { + if (IsInVirtualTerminalInputMode()) + { + HandleKeyEvent(hWnd, Message, wParam, lParam, pfUnlockConsole); + return FALSE; + } + + return TRUE; // call DefWindowProc + } + + if (VirtualKeyCode == VK_ESCAPE) + { + return TRUE; // call DefWindowProc + } + if (VirtualKeyCode == VK_TAB) + { + return TRUE; // call DefWindowProc + } + } + + HandleKeyEvent(hWnd, Message, wParam, lParam, pfUnlockConsole); + + return FALSE; +} + +[[nodiscard]] +static HRESULT _AdjustFontSize(const SHORT delta) noexcept +{ + auto& globals = ServiceLocator::LocateGlobals(); + auto& screenInfo = globals.getConsoleInformation().GetActiveOutputBuffer(); + + // Increase or decrease font by delta through the API to ensure our behavior matches public behavior. + + CONSOLE_FONT_INFOEX font = { 0 }; + font.cbSize = sizeof(font); + + RETURN_IF_FAILED(globals.api.GetCurrentConsoleFontExImpl(screenInfo, false, font)); + + font.dwFontSize.Y += delta; + + RETURN_IF_FAILED(globals.api.SetCurrentConsoleFontExImpl(screenInfo, false, font)); + + return S_OK; +} + +// Routine Description: +// - Returns TRUE if DefWindowProc should be called. +BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, + const UINT Message, + const WPARAM wParam, + const LPARAM lParam) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (Message != WM_MOUSEMOVE) + { + // Log a telemetry flag saying the user interacted with the Console + Telemetry::Instance().SetUserInteractive(); + } + + Selection* const pSelection = &Selection::Instance(); + + if (!(gci.Flags & CONSOLE_HAS_FOCUS) && !pSelection->IsMouseButtonDown()) + { + return TRUE; + } + + if (gci.Flags & CONSOLE_IGNORE_NEXT_MOUSE_INPUT) + { + // only reset on up transition + if (Message != WM_LBUTTONDOWN && Message != WM_MBUTTONDOWN && Message != WM_RBUTTONDOWN) + { + gci.Flags &= ~CONSOLE_IGNORE_NEXT_MOUSE_INPUT; + return FALSE; + } + return TRUE; + } + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx + // Important Do not use the LOWORD or HIWORD macros to extract the x- and y- + // coordinates of the cursor position because these macros return incorrect + // results on systems with multiple monitors. Systems with multiple monitors + // can have negative x- and y- coordinates, and LOWORD and HIWORD treat the + // coordinates as unsigned quantities. + short x = GET_X_LPARAM(lParam); + short y = GET_Y_LPARAM(lParam); + + COORD MousePosition; + // If it's a *WHEEL event, it's in screen coordinates, not window + if (Message == WM_MOUSEWHEEL || Message == WM_MOUSEHWHEEL) + { + POINT coords = { x, y }; + ScreenToClient(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), &coords); + MousePosition = { (SHORT)coords.x, (SHORT)coords.y }; + } + else + { + MousePosition = { x, y }; + } + + // translate mouse position into characters, if necessary. + COORD ScreenFontSize = ScreenInfo.GetScreenFontSize(); + MousePosition.X /= ScreenFontSize.X; + MousePosition.Y /= ScreenFontSize.Y; + + const bool fShiftPressed = WI_IsFlagSet(GetKeyState(VK_SHIFT), KEY_PRESSED); + + // We need to try and have the virtual terminal handle the mouse's position in viewport coordinates, + // not in screen buffer coordinates. It expects the top left to always be 0,0 + // (the TerminalMouseInput object will add (1,1) to convert to VT coords on it's own.) + // Mouse events with shift pressed will ignore this and fall through to the default handler. + // This is in line with PuTTY's behavior and vim's own documentation: + // "The xterm handling of the mouse buttons can still be used by keeping the shift key pressed." - `:help 'mouse'`, vim. + // Mouse events while we're selecting or have a selection will also skip this and fall though + // (so that the VT handler doesn't eat any selection region updates) + if (!fShiftPressed && !pSelection->IsInSelectingState()) + { + short sDelta = 0; + if (Message == WM_MOUSEWHEEL) + { + short sWheelDelta = GET_WHEEL_DELTA_WPARAM(wParam); + // For most devices, we'll get mouse events as multiples of + // WHEEL_DELTA, where WHEEL_DELTA represents a single scroll unit + // But sometimes, things like trackpads will scroll in finer + // measurements. In this case, the VT mouse scrolling wouldn't work. + // So if that happens, ensure we scroll at least one time. + if (abs(sWheelDelta) < WHEEL_DELTA) + { + sDelta = sWheelDelta < 0 ? -1 : 1; + } + else + { + sDelta = sWheelDelta / WHEEL_DELTA; + } + } + + if (HandleTerminalMouseEvent(MousePosition, Message, GET_KEYSTATE_WPARAM(wParam), sDelta)) + { + return FALSE; + } + } + + MousePosition.X += ScreenInfo.GetViewport().Left(); + MousePosition.Y += ScreenInfo.GetViewport().Top(); + + const COORD coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); + + // make sure mouse position is clipped to screen buffer + if (MousePosition.X < 0) + { + MousePosition.X = 0; + } + else if (MousePosition.X >= coordScreenBufferSize.X) + { + MousePosition.X = coordScreenBufferSize.X - 1; + } + if (MousePosition.Y < 0) + { + MousePosition.Y = 0; + } + else if (MousePosition.Y >= coordScreenBufferSize.Y) + { + MousePosition.Y = coordScreenBufferSize.Y - 1; + } + + // Process the transparency mousewheel message before the others so that we can + // process all the mouse events within the Selection and QuickEdit check + if (Message == WM_MOUSEWHEEL) + { + const short sKeyState = GET_KEYSTATE_WPARAM(wParam); + + if (WI_IsFlagSet(sKeyState, MK_CONTROL)) + { + const short sDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA; + + // ctrl+shift+scroll adjusts opacity of the window + if (WI_IsFlagSet(sKeyState, MK_SHIFT)) + { + ServiceLocator::LocateConsoleWindow()->ChangeWindowOpacity(OPACITY_DELTA_INTERVAL * sDelta); + } + // ctrl+scroll adjusts the font size + else + { + LOG_IF_FAILED(_AdjustFontSize(sDelta)); + } + } + } + + if (pSelection->IsInSelectingState() || pSelection->IsInQuickEditMode()) + { + if (Message == WM_LBUTTONDOWN) + { + // make sure message matches button state + if (!(GetKeyState(VK_LBUTTON) & KEY_PRESSED)) + { + return FALSE; + } + + if (pSelection->IsInQuickEditMode() && !pSelection->IsInSelectingState()) + { + // start a mouse selection + pSelection->InitializeMouseSelection(MousePosition); + + pSelection->MouseDown(); + + // Check for ALT-Mouse Down "use alternate selection" + // If in box mode, use line mode. If in line mode, use box mode. + // TODO: move into initialize? + pSelection->CheckAndSetAlternateSelection(); + + pSelection->ShowSelection(); + } + else + { + bool fExtendSelection = false; + + // We now capture the mouse to our Window. We do this so that the + // user can "scroll" the selection endpoint to an off screen + // position by moving the mouse off the client area. + if (pSelection->IsMouseInitiatedSelection()) + { + // Check for SHIFT-Mouse Down "continue previous selection" command. + if (fShiftPressed) + { + fExtendSelection = true; + } + } + + // if we chose to extend the selection, do that. + if (fExtendSelection) + { + pSelection->MouseDown(); + pSelection->ExtendSelection(MousePosition); + } + else + { + // otherwise, set up a new selection from here. note that it's important to ClearSelection(true) here + // because ClearSelection() unblocks console output, causing us to have + // a line of output occur every time the user changes the selection. + pSelection->ClearSelection(true); + pSelection->InitializeMouseSelection(MousePosition); + pSelection->MouseDown(); + pSelection->ShowSelection(); + } + } + } + else if (Message == WM_LBUTTONUP) + { + if (pSelection->IsInSelectingState() && pSelection->IsMouseInitiatedSelection()) + { + pSelection->MouseUp(); + } + } + else if (Message == WM_LBUTTONDBLCLK) + { + // on double-click, attempt to select a "word" beneath the cursor + const COORD selectionAnchor = pSelection->GetSelectionAnchor(); + + if (MousePosition == selectionAnchor) + { + try + { + const std::pair wordBounds = ScreenInfo.GetWordBoundary(MousePosition); + MousePosition = wordBounds.second; + // update both ends of the selection since we may have adjusted the anchor in some circumstances. + pSelection->AdjustSelection(wordBounds.first, wordBounds.second); + + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } + pSelection->MouseDown(); + } + else if ((Message == WM_RBUTTONDOWN) || (Message == WM_RBUTTONDBLCLK)) + { + if (!pSelection->IsMouseButtonDown()) + { + if (pSelection->IsInSelectingState()) + { + // Capture data on when quick edit copy is used in proc or raw mode + if (IsInProcessedInputMode()) + { + Telemetry::Instance().LogQuickEditCopyProcUsed(); + } + else + { + Telemetry::Instance().LogQuickEditCopyRawUsed(); + } + // If the ALT key is held, also select HTML as well as plain text. + bool const fAlsoSelectHtml = WI_IsFlagSet(GetKeyState(VK_MENU), KEY_PRESSED); + Clipboard::Instance().Copy(fAlsoSelectHtml); + } + else if (gci.Flags & CONSOLE_QUICK_EDIT_MODE) + { + // Capture data on when quick edit paste is used in proc or raw mode + if (IsInProcessedInputMode()) + { + Telemetry::Instance().LogQuickEditPasteProcUsed(); + } + else + { + Telemetry::Instance().LogQuickEditPasteRawUsed(); + } + + Clipboard::Instance().Paste(); + } + gci.Flags |= CONSOLE_IGNORE_NEXT_MOUSE_INPUT; + } + } + else if (Message == WM_MBUTTONDOWN) + { + ServiceLocator::LocateConsoleControl() + ->EnterReaderModeHelper(ServiceLocator::LocateConsoleWindow()->GetWindowHandle()); + } + else if (Message == WM_MOUSEMOVE) + { + if (pSelection->IsMouseButtonDown() && pSelection->ShouldAllowMouseDragSelection(MousePosition)) + { + pSelection->ExtendSelection(MousePosition); + } + } + else if (Message == WM_MOUSEWHEEL || Message == WM_MOUSEHWHEEL) + { + return TRUE; + } + + // We're done processing the messages for selection. We need to return + return FALSE; + } + + if (WI_IsFlagClear(gci.pInputBuffer->InputMode, ENABLE_MOUSE_INPUT)) + { + ReleaseCapture(); + return TRUE; + } + + ULONG ButtonFlags; + ULONG EventFlags; + switch (Message) + { + case WM_LBUTTONDOWN: + SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle()); + ButtonFlags = FROM_LEFT_1ST_BUTTON_PRESSED; + EventFlags = 0; + break; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + ReleaseCapture(); + ButtonFlags = EventFlags = 0; + break; + case WM_RBUTTONDOWN: + SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle()); + ButtonFlags = RIGHTMOST_BUTTON_PRESSED; + EventFlags = 0; + break; + case WM_MBUTTONDOWN: + SetCapture(ServiceLocator::LocateConsoleWindow()->GetWindowHandle()); + ButtonFlags = FROM_LEFT_2ND_BUTTON_PRESSED; + EventFlags = 0; + break; + case WM_MOUSEMOVE: + ButtonFlags = 0; + EventFlags = MOUSE_MOVED; + break; + case WM_LBUTTONDBLCLK: + ButtonFlags = FROM_LEFT_1ST_BUTTON_PRESSED; + EventFlags = DOUBLE_CLICK; + break; + case WM_RBUTTONDBLCLK: + ButtonFlags = RIGHTMOST_BUTTON_PRESSED; + EventFlags = DOUBLE_CLICK; + break; + case WM_MBUTTONDBLCLK: + ButtonFlags = FROM_LEFT_2ND_BUTTON_PRESSED; + EventFlags = DOUBLE_CLICK; + break; + case WM_MOUSEWHEEL: + ButtonFlags = ((UINT)wParam & 0xFFFF0000); + EventFlags = MOUSE_WHEELED; + break; + case WM_MOUSEHWHEEL: + ButtonFlags = ((UINT)wParam & 0xFFFF0000); + EventFlags = MOUSE_HWHEELED; + break; + default: + RIPMSG1(RIP_ERROR, "Invalid message 0x%x", Message); + ButtonFlags = 0; + EventFlags = 0; + break; + } + + ULONG EventsWritten = 0; + try + { + std::unique_ptr mouseEvent = std::make_unique( + MousePosition, + ConvertMouseButtonState(ButtonFlags, static_cast(wParam)), + GetControlKeyState(0), + EventFlags); + EventsWritten = static_cast(gci.pInputBuffer->Write(std::move(mouseEvent))); + } + catch(...) + { + LOG_HR(wil::ResultFromCaughtException()); + EventsWritten = 0; + } + + if (EventsWritten != 1) + { + RIPMSG1(RIP_WARNING, "PutInputInBuffer: EventsWritten != 1 (0x%x), 1 expected", EventsWritten); + } + + return FALSE; +} + +// ---------------------------- +// Window Initialization +// ---------------------------- + +// Routine Description: +// - This routine gets called to filter input to console dialogs so that we can do the special processing that StoreKeyInfo does. +LRESULT DialogHookProc(int nCode, WPARAM /*wParam*/, LPARAM lParam) +{ + MSG msg = *((PMSG)lParam); + + if (nCode == MSGF_DIALOGBOX) + { + if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST) + { + if (msg.message != WM_CHAR && msg.message != WM_DEADCHAR && msg.message != WM_SYSCHAR && msg.message != WM_SYSDEADCHAR) + { + + // don't store key info if dialog box input + if (GetWindowLongPtrW(msg.hwnd, GWLP_HWNDPARENT) == 0) + { + StoreKeyInfo(&msg); + } + } + } + } + + return 0; +} + +// Routine Description: +// - This routine gets called by the console input thread to set up the console window. +NTSTATUS InitWindowsSubsystem(_Out_ HHOOK * phhook) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + ConsoleProcessHandle* ProcessData = gci.ProcessHandleList.FindProcessInList(ConsoleProcessList::ROOT_PROCESS_ID); + FAIL_FAST_IF(!(ProcessData != nullptr && ProcessData->fRootProcess)); + + // Create and activate the main window + NTSTATUS Status = Window::CreateInstance(&gci, gci.ScreenBuffers); + + if (!NT_SUCCESS(Status)) + { + RIPMSG2(RIP_WARNING, "CreateWindowsWindow failed with status 0x%x, gle = 0x%x", Status, GetLastError()); + return Status; + } + + // We intentionally ignore the return value of SetWindowsHookEx. There are mixed LUID cases where this call will fail but in the past this call + // was special cased (for CSRSS) to always succeed. Thus, we ignore failure for app compat (as not having the hook isn't fatal). + *phhook = SetWindowsHookExW(WH_MSGFILTER, (HOOKPROC)DialogHookProc, nullptr, GetCurrentThreadId()); + + SetConsoleWindowOwner(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), ProcessData); + + LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->ActivateAndShow(gci.GetShowWindow())); + + NotifyWinEvent(EVENT_CONSOLE_START_APPLICATION, ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), ProcessData->dwProcessId, 0); + + return STATUS_SUCCESS; +} + +// ---------------------------- +// Console Input Thread +// (for a window) +// ---------------------------- + +DWORD ConsoleInputThreadProcWin32(LPVOID /*lpParameter*/) +{ + InitEnvironmentVariables(); + + LockConsole(); + HHOOK hhook = nullptr; + NTSTATUS Status = STATUS_SUCCESS; + + if (!ServiceLocator::LocateGlobals().launchArgs.IsHeadless()) + { + // If we're not headless, set up the main conhost window. + Status = InitWindowsSubsystem(&hhook); + } + else + { + // If we are headless (because we're a pseudo console), we + // will still need a window handle in the win32 environment + // in case anyone sends messages at that HWND (vim.exe is an example.) + // We have to CreateWindow on the same thread that will pump the messages + // which is this thread. + ServiceLocator::LocatePseudoWindow(); + } + + UnlockConsole(); + if (!NT_SUCCESS(Status)) + { + ServiceLocator::LocateGlobals().ntstatusConsoleInputInitStatus = Status; + ServiceLocator::LocateGlobals().hConsoleInputInitEvent.SetEvent(); + return Status; + } + + ServiceLocator::LocateGlobals().hConsoleInputInitEvent.SetEvent(); + + for (;;) + { + MSG msg; + if (GetMessageW(&msg, nullptr, 0, 0) == 0) + { + break; + } + + // --- START LOAD BEARING CODE --- + // TranslateMessageEx appears to be necessary for a few things (that we could in the future take care of ourselves...) + // 1. The normal TranslateMessage will return TRUE for all WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP + // no matter what. + // - This means that if there *is* a translation for the keydown, it will post a WM_CHAR to our queue and say TRUE. + // ***HOWEVER*** it also means that if there is *NOT* a translation for the keydown, it will post nothing and still say TRUE. + // - TRUE from TranslateMessage typically means "don't dispatch, it's already handled." + // - *But* the console needs to dispatch a WM_KEYDOWN that wasn't translated into a WM_CHAR so the underlying console client can + // receive it and decide what to do with it. + // - Thus TranslateMessageEx was kludged in December 1990 to return FALSE for the case where it doesn't post a WM_CHAR so the + // console can know this and handle it. + // - Instead of using this kludge from many years ago... we could instead use the ToUnicode/ToUnicodeEx exports to translate + // the WM_KEYDOWN to WM_CHAR ourselves and synchronously dispatch it with all context if necessary (or continue to dispatch the + // WM_KEYDOWN if ToUnicode offers no translation. We would no longer need the private TranslateMessageEx (or even TranslateMessage at all). + // 2. TranslateMessage also performs translation of ALT+NUMPAD sequences on our behalf into their corresponding character input + // - If we take out TranslateMessage entirely as stated in part 1, we would have to reimplement our own version of translating ALT+NUMPAD + // sequences at this point inside the console. + // - The Clipboard class (clipboard.cpp) already does the inverse of this to mock up keypad sequences for text strings pasted into the console + // so they can be faithfully represented as a user "typing" into the client application. The vision would be we leverage the knowledge from + // clipboard to build a transcoder capable of doing the reverse at this point so TranslateMessage would be completely unnecessary for us. + // Until that future point in time.... this is LOAD BEARING CODE and should not be hastily modified or removed! + if (!ServiceLocator::LocateConsoleControl()->TranslateMessageEx(&msg, TM_POSTCHARBREAKS)) + { + DispatchMessageW(&msg); + } + // do this so that alt-tab works while journalling + else if (msg.message == WM_SYSKEYDOWN && msg.wParam == VK_TAB && WI_IsFlagSet(msg.lParam, WM_SYSKEYDOWN_ALT_PRESSED)) + { + // alt is really down + DispatchMessageW(&msg); + } + else + { + StoreKeyInfo(&msg); + } + // -- END LOAD BEARING CODE + } + + // Free all resources used by this thread + DeactivateTextServices(); + + if (nullptr != hhook) + { + UnhookWindowsHookEx(hhook); + } + + return 0; +} diff --git a/src/interactivity/win32/windowio.hpp b/src/interactivity/win32/windowio.hpp new file mode 100644 index 000000000..b9f0170b5 --- /dev/null +++ b/src/interactivity/win32/windowio.hpp @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "precomp.h" + +#include "..\..\server\ProcessHandle.h" + +#pragma hdrstop + +void HandleKeyEvent(const HWND hWnd, + const UINT Message, + const WPARAM wParam, + const LPARAM lParam, + _Inout_opt_ PBOOL pfUnlockConsole); +BOOL HandleSysKeyEvent(const HWND hWnd, + const UINT Message, + const WPARAM wParam, + const LPARAM lParam, + _Inout_opt_ PBOOL pfUnlockConsole); +BOOL HandleMouseEvent(const SCREEN_INFORMATION& ScreenInfo, + const UINT Message, + const WPARAM wParam, + const LPARAM lParam); + +VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pProcessData); +DWORD ConsoleInputThreadProcWin32(LPVOID lpParameter); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp new file mode 100644 index 000000000..edc55c783 --- /dev/null +++ b/src/interactivity/win32/windowproc.cpp @@ -0,0 +1,947 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "Clipboard.hpp" +#include "ConsoleControl.hpp" +#include "find.h" +#include "menu.hpp" +#include "window.hpp" +#include "windowdpiapi.hpp" +#include "windowime.hpp" +#include "windowio.hpp" +#include "windowmetrics.hpp" + +#include "..\..\host\_output.h" +#include "..\..\host\output.h" +#include "..\..\host\dbcs.h" +#include "..\..\host\handle.h" +#include "..\..\host\input.h" +#include "..\..\host\misc.h" +#include "..\..\host\registry.hpp" +#include "..\..\host\scrolling.hpp" +#include "..\..\host\srvinit.h" + +#include "..\inc\ServiceLocator.hpp" + +#include "../interactivity/win32/windowtheme.hpp" +#include "../interactivity/win32/windowUiaProvider.hpp" +#include "../interactivity/win32/CustomWindowMessages.h" + +#include +#include + + +using namespace Microsoft::Console::Interactivity::Win32; +using namespace Microsoft::Console::Types; + +// The static and specific window procedures for this class are contained here +#pragma region Window Procedure + +LRESULT CALLBACK Window::s_ConsoleWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam) +{ + // Save the pointer here to the specific window instance when one is created + if (Message == WM_CREATE) + { + const CREATESTRUCT* const pCreateStruct = reinterpret_cast(lParam); + + Window* const pWindow = reinterpret_cast(pCreateStruct->lpCreateParams); + SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast(pWindow)); + } + + // Dispatch the message to the specific class instance + Window* const pWindow = reinterpret_cast(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); + if (pWindow != nullptr) + { + return pWindow->ConsoleWindowProc(hWnd, Message, wParam, lParam); + } + + // If we get this far, call the default window proc + return DefWindowProcW(hWnd, Message, wParam, lParam); +} + +LRESULT CALLBACK Window::ConsoleWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam) +{ + Globals& g = ServiceLocator::LocateGlobals(); + CONSOLE_INFORMATION& gci = g.getConsoleInformation(); + LRESULT Status = 0; + BOOL Unlock = TRUE; + + LockConsole(); + + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + if (hWnd == nullptr) // TODO: this might not be possible anymore + { + if (Message == WM_CLOSE) + { + _CloseWindow(); + Status = 0; + } + else + { + Status = DefWindowProcW(hWnd, Message, wParam, lParam); + } + + UnlockConsole(); + return Status; + } + + switch (Message) + { + case WM_CREATE: + { + // Load all metrics we'll need. + _UpdateSystemMetrics(); + + // The system is not great and the window rect is wrong the first time for High DPI (WM_DPICHANGED scales strangely.) + // So here we have to grab the DPI of the current window (now that we have a window). + // Then we have to re-propose a window size for our window that is scaled to DPI and SetWindowPos. + + // First get the new DPI and update all the scaling factors in the console that are affected. + + // NOTE: GetWindowDpi and/or GetDpiForWindow can be *WRONG* at this point in time depending on monitor configuration. + // They won't be correct until the window is actually shown. So instead of using those APIs, figure out the DPI + // based on the rectangle that is about to be shown using the nearest monitor. + + // Get proposed window rect from create structure + CREATESTRUCTW* pcs = (CREATESTRUCTW*)lParam; + RECT rc; + rc.left = pcs->x; + rc.top = pcs->y; + rc.right = rc.left + pcs->cx; + rc.bottom = rc.top + pcs->cy; + + // Find nearest montitor. + HMONITOR hmon = MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST); + + // This API guarantees that dpix and dpiy will be equal, but neither is an optional parameter so give two UINTs. + UINT dpix = USER_DEFAULT_SCREEN_DPI; + UINT dpiy = USER_DEFAULT_SCREEN_DPI; + GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); // If this fails, we'll use the default of 96. + + // Pick one and set it to the global DPI. + ServiceLocator::LocateGlobals().dpi = (int)dpix; + + _UpdateSystemMetrics(); // scroll bars and cursors and such. + s_ReinitializeFontsForDPIChange(); // font sizes. + + // Now re-propose the window size with the same origin. + RECT rectProposed = { rc.left, rc.top, 0, 0 }; + _CalculateWindowRect(_pSettings->GetWindowSize(), &rectProposed); + + SetWindowPos(hWnd, NULL, rectProposed.left, rectProposed.top, RECT_WIDTH(&rectProposed), RECT_HEIGHT(&rectProposed), SWP_NOACTIVATE | SWP_NOZORDER); + + // Save the proposed window rect dimensions here so we can adjust if the system comes back and changes them on what we asked for. + ServiceLocator::LocateWindowMetrics()->ConvertWindowRectToClientRect(&rectProposed); + + break; + } + + case WM_DROPFILES: + { + _HandleDrop(wParam); + break; + } + + case WM_GETOBJECT: + { + Status = _HandleGetObject(hWnd, wParam, lParam); + break; + } + + case WM_DESTROY: + { + // signal to uia that they can disconnect our uia provider + if (_pUiaProvider) + { + UiaReturnRawElementProvider(hWnd, 0, 0, NULL); + } + break; + } + + case WM_SIZING: + { + // Signal that the user changed the window size, so we can return the value later for telemetry. By only + // sending the data back if the size has changed, helps reduce the amount of telemetry being sent back. + // WM_SIZING doesn't fire if they resize the window using Win-UpArrow, so we'll miss that scenario. We could + // listen to the WM_SIZE message instead, but they can fire when the window is being restored from being + // minimized, and not only when they resize the window. + Telemetry::Instance().SetWindowSizeChanged(); + goto CallDefWin; + break; + } + + case WM_GETDPISCALEDSIZE: + { + // This message will send us the DPI we're about to be changed to. + // Our goal is to use it to try to figure out the Window Rect that we'll need at that DPI to maintain + // the same client rendering that we have now. + + // First retrieve the new DPI and the current DPI. + DWORD const dpiProposed = (WORD)wParam; + DWORD const dpiCurrent = g.dpi; + + // Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that. + const FontInfo& fiCurrent = ScreenInfo.GetCurrentFont(); + FontInfoDesired fiDesired(fiCurrent); + FontInfo fiProposed(nullptr, 0, 0, { 0, 0 }, 0); + + const HRESULT hr = g.pRender->GetProposedFont(dpiProposed, fiDesired, fiProposed); + // fiProposal will be updated by the renderer for this new font. + // GetProposedFont can fail if there's no render engine yet. + // This can happen if we're headless. + // Just assume that the font is 1x1 in that case. + const COORD coordFontProposed = SUCCEEDED(hr) ? fiProposed.GetSize() : COORD({1, 1}); + + // Then from that font size, we need to calculate the client area. + // Then from the client area we need to calculate the window area (using the proposed DPI scalar here as well.) + + // Retrieve the additional parameters we need for the math call based on the current window & buffer properties. + const Viewport viewport = ScreenInfo.GetViewport(); + COORD coordWindowInChars = viewport.Dimensions(); + + const COORD coordBufferSize = ScreenInfo.GetTextBuffer().GetSize().Dimensions(); + + // Now call the math calculation for our proposed size. + RECT rectProposed = { 0 }; + s_CalculateWindowRect(coordWindowInChars, dpiProposed, coordFontProposed, coordBufferSize, hWnd, &rectProposed); + + // Prepare where we're going to keep our final suggestion. + SIZE* const pSuggestionSize = (SIZE*)lParam; + + pSuggestionSize->cx = RECT_WIDTH(&rectProposed); + pSuggestionSize->cy = RECT_HEIGHT(&rectProposed); + + // Format our final suggestion for consumption. + UnlockConsole(); + return TRUE; + } + + case WM_DPICHANGED: + { + _fInDPIChange = true; + ServiceLocator::LocateGlobals().dpi = HIWORD(wParam); + _UpdateSystemMetrics(); + s_ReinitializeFontsForDPIChange(); + + if (IsInFullscreen()) + { + // If we're a full screen window, completely ignore what the DPICHANGED says as it will be bigger than the monitor and + // instead just ensure that the window is still taking up the full screen. + SetIsFullscreen(true); + } + else + { + // this is the RECT that the system suggests. + RECT* const prcNewScale = (RECT*)lParam; + SetWindowPos(hWnd, HWND_TOP, prcNewScale->left, prcNewScale->top, RECT_WIDTH(prcNewScale), RECT_HEIGHT(prcNewScale), SWP_NOZORDER | SWP_NOACTIVATE); + } + + _fInDPIChange = false; + + break; + } + + case WM_ACTIVATE: + { + // if we're activated by a mouse click, remember it so + // we don't pass the click on to the app. + if (LOWORD(wParam) == WA_CLICKACTIVE) + { + gci.Flags |= CONSOLE_IGNORE_NEXT_MOUSE_INPUT; + } + goto CallDefWin; + break; + } + + case WM_SETFOCUS: + { + gci.ProcessHandleList.ModifyConsoleProcessFocus(TRUE); + + gci.Flags |= CONSOLE_HAS_FOCUS; + + gci.GetCursorBlinker().FocusStart(); + + HandleFocusEvent(TRUE); + + // ActivateTextServices does nothing if already active so this is OK to be called every focus. + ActivateTextServices(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), GetImeSuggestionWindowPos); + + // set the text area to have focus for accessibility consumers + if (_pUiaProvider) + { + LOG_IF_FAILED(_pUiaProvider->SetTextAreaFocus()); + } + + break; + } + + case WM_KILLFOCUS: + { + gci.ProcessHandleList.ModifyConsoleProcessFocus(FALSE); + + gci.Flags &= ~CONSOLE_HAS_FOCUS; + + // turn it off when we lose focus. + gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsOn(false); + gci.GetCursorBlinker().FocusEnd(); + + HandleFocusEvent(FALSE); + + break; + } + + case WM_PAINT: + { + // Since we handle our own minimized window state, we need to + // check if we're minimized (iconic) and set our internal state flags accordingly. + // http://msdn.microsoft.com/en-us/library/windows/desktop/dd162483(v=vs.85).aspx + // NOTE: We will not get called to paint ourselves when minimized because we set an icon when registering the window class. + // That means this CONSOLE_IS_ICONIC is unnnecessary when/if we can decouple the drawing with D2D. + if (IsIconic(hWnd)) + { + WI_SetFlag(gci.Flags, CONSOLE_IS_ICONIC); + } + else + { + WI_ClearFlag(gci.Flags, CONSOLE_IS_ICONIC); + } + + LOG_IF_FAILED(_HandlePaint()); + + // NOTE: We cannot let the OS handle this message (meaning do NOT pass to DefWindowProc) + // or it will cause missing painted regions in scenarios without a DWM (like Core Server SKU). + // Ensure it is re-validated in this handler so we don't receive infinite WM_PAINTs after + // we have stored the invalid region data for the next trip around the renderer thread. + + break; + } + + case WM_ERASEBKGND: + { + break; + } + + case WM_CLOSE: + { + // Write the final trace log during the WM_CLOSE message while the console process is still fully alive. + // This gives us time to query the process for information. We shouldn't really miss any useful + // telemetry between now and when the process terminates. + Telemetry::Instance().WriteFinalTraceLog(); + + _CloseWindow(); + break; + } + + case WM_SETTINGCHANGE: + { + try + { + WindowTheme theme; + LOG_IF_FAILED(theme.TrySetDarkMode(hWnd)); + } + CATCH_LOG(); + + gci.GetCursorBlinker().SettingsChanged(); + } + __fallthrough; + + case WM_DISPLAYCHANGE: + { + _UpdateSystemMetrics(); + break; + } + + case WM_WINDOWPOSCHANGING: + { + // Enforce maximum size here instead of WM_GETMINMAXINFO. + // If we return it in WM_GETMINMAXINFO, then it will be enforced when snapping across DPI boundaries (bad.) + + // Retrieve the suggested dimensions and make a rect and size. + LPWINDOWPOS lpwpos = (LPWINDOWPOS)lParam; + + // We only need to apply restrictions if the size is changing. + if (!WI_IsFlagSet(lpwpos->flags, SWP_NOSIZE)) + { + // Figure out the suggested dimensions + RECT rcSuggested; + rcSuggested.left = lpwpos->x; + rcSuggested.top = lpwpos->y; + rcSuggested.right = rcSuggested.left + lpwpos->cx; + rcSuggested.bottom = rcSuggested.top + lpwpos->cy; + SIZE szSuggested; + szSuggested.cx = RECT_WIDTH(&rcSuggested); + szSuggested.cy = RECT_HEIGHT(&rcSuggested); + + // Figure out the current dimensions for comparison. + RECT rcCurrent = GetWindowRect(); + + // Determine whether we're being resized by someone dragging the edge or completely moved around. + bool fIsEdgeResize = false; + { + // We can only be edge resizing if our existing rectangle wasn't empty. If it was empty, we're doing the initial create. + if (!IsRectEmpty(&rcCurrent)) + { + // If one or two sides are changing, we're being edge resized. + unsigned int cSidesChanging = 0; + if (rcCurrent.left != rcSuggested.left) + { + cSidesChanging++; + } + if (rcCurrent.right != rcSuggested.right) + { + cSidesChanging++; + } + if (rcCurrent.top != rcSuggested.top) + { + cSidesChanging++; + } + if (rcCurrent.bottom != rcSuggested.bottom) + { + cSidesChanging++; + } + + if (cSidesChanging == 1 || cSidesChanging == 2) + { + fIsEdgeResize = true; + } + } + } + + // If the window is maximized, let it do whatever it wants to do. + // If not, then restrict it to our maximum possible window. + if (!WI_IsFlagSet(GetWindowStyle(hWnd), WS_MAXIMIZE)) + { + // Find the related monitor, the maximum pixel size, + // and the dpi for the suggested rect. + UINT dpiOfMaximum; + RECT rcMaximum; + + if (fIsEdgeResize) + { + // If someone's dragging from the edge to resize in one direction, we want to make sure we never grow past the current monitor. + rcMaximum = ServiceLocator::LocateWindowMetrics()->GetMaxWindowRectInPixels(&rcCurrent, &dpiOfMaximum); + } + else + { + // In other circumstances, assume we're snapping around or some other jump (TS). + // Just do whatever we're told using the new suggestion as the restriction monitor. + rcMaximum = ServiceLocator::LocateWindowMetrics()->GetMaxWindowRectInPixels(&rcSuggested, &dpiOfMaximum); + } + + // Only apply the maximum size restriction if the current DPI matches the DPI of the + // maximum rect. This keeps us from applying the wrong restriction if the monitor + // we're moving to has a different DPI but we've yet to get notified of that DPI + // change. If we do apply it, then we'll restrict the console window BEFORE its + // been resized for the DPI change, so we're likely to shrink the window too much + // or worse yet, keep it from moving entirely. We'll get a WM_DPICHANGED, + // resize the window, and then process the restriction in a few window messages. + if (((int)dpiOfMaximum == g.dpi) && + ((szSuggested.cx > RECT_WIDTH(&rcMaximum)) || (szSuggested.cy > RECT_HEIGHT(&rcMaximum)))) + { + lpwpos->cx = std::min(RECT_WIDTH(&rcMaximum), szSuggested.cx); + lpwpos->cy = std::min(RECT_HEIGHT(&rcMaximum), szSuggested.cy); + + // We usually add SWP_NOMOVE so that if the user is dragging the left or top edge + // and hits the restriction, then the window just stops growing, it doesn't + // move with the mouse. However during DPI changes, we need to allow a move + // because the RECT from WM_DPICHANGED has been specially crafted by win32k + // to keep the mouse cursor from jumping away from the caption bar. + if (!_fInDPIChange) + { + lpwpos->flags |= SWP_NOMOVE; + } + } + } + + break; + } + else + { + goto CallDefWin; + } + } + + case WM_WINDOWPOSCHANGED: + { + // Only handle this if the DPI is the same as last time. + // If the DPI is different, assume we're about to get a DPICHANGED notification + // which will have a better suggested rectangle than this one. + // NOTE: This stopped being possible in RS4 as the DPI now changes when and only when + // we receive WM_DPICHANGED. We keep this check around so that we perform better downlevel. + int const dpi = ServiceLocator::LocateHighDpiApi()->GetWindowDPI(hWnd); + if (dpi == ServiceLocator::LocateGlobals().dpi) + { + _HandleWindowPosChanged(lParam); + } + break; + } + + case WM_CONTEXTMENU: + { + Telemetry::Instance().SetContextMenuUsed(); + if (DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam) == HTCLIENT) + { + HMENU hHeirMenu = Menu::s_GetHeirMenuHandle(); + + Unlock = FALSE; + UnlockConsole(); + + TrackPopupMenuEx(hHeirMenu, + TPM_RIGHTBUTTON | (GetSystemMetrics(SM_MENUDROPALIGNMENT) == 0 ? TPM_LEFTALIGN : TPM_RIGHTALIGN), + GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), hWnd, nullptr); + } + else + { + goto CallDefWin; + } + break; + } + + case WM_NCLBUTTONDOWN: + { + // allow user to move window even when bigger than the screen + switch (wParam & 0x00FF) + { + case HTCAPTION: + UnlockConsole(); + Unlock = FALSE; + SetActiveWindow(hWnd); + SendMessageTimeoutW(hWnd, WM_SYSCOMMAND, SC_MOVE | wParam, lParam, SMTO_NORMAL, INFINITE, nullptr); + break; + default: + goto CallDefWin; + } + break; + } + + case WM_KEYDOWN: + case WM_KEYUP: + case WM_CHAR: + case WM_DEADCHAR: + { + HandleKeyEvent(hWnd, Message, wParam, lParam, &Unlock); + break; + } + + case WM_SYSKEYDOWN: + case WM_SYSKEYUP: + case WM_SYSCHAR: + case WM_SYSDEADCHAR: + { + if (HandleSysKeyEvent(hWnd, Message, wParam, lParam, &Unlock)) + { + goto CallDefWin; + } + break; + } + + case WM_COMMAND: + // If this is an edit command from the context menu, treat it like a sys command. + if ((wParam < ID_CONSOLE_COPY) || (wParam > ID_CONSOLE_SELECTALL)) + { + break; + } + + __fallthrough; + + case WM_SYSCOMMAND: + if (wParam == ID_CONSOLE_MARK) + { + Selection::Instance().InitializeMarkSelection(); + } + else if (wParam == ID_CONSOLE_COPY) + { + Clipboard::Instance().Copy(); + } + else if (wParam == ID_CONSOLE_PASTE) + { + Clipboard::Instance().Paste(); + } + else if (wParam == ID_CONSOLE_SCROLL) + { + Scrolling::s_DoScroll(); + } + else if (wParam == ID_CONSOLE_FIND) + { + DoFind(); + Unlock = FALSE; + } + else if (wParam == ID_CONSOLE_SELECTALL) + { + Selection::Instance().SelectAll(); + } + else if (wParam == ID_CONSOLE_CONTROL) + { + Menu::s_ShowPropertiesDialog(hWnd, FALSE); + } + else if (wParam == ID_CONSOLE_DEFAULTS) + { + Menu::s_ShowPropertiesDialog(hWnd, TRUE); + } + else + { + goto CallDefWin; + } + break; + + case WM_HSCROLL: + { + HorizontalScroll(LOWORD(wParam), HIWORD(wParam)); + break; + } + + case WM_VSCROLL: + { + VerticalScroll(LOWORD(wParam), HIWORD(wParam)); + break; + } + + case WM_INITMENU: + { + HandleMenuEvent(WM_INITMENU); + Menu::Instance()->Initialize(); + break; + } + + case WM_MENUSELECT: + { + if (HIWORD(wParam) == 0xffff) + { + HandleMenuEvent(WM_MENUSELECT); + } + break; + } + + case WM_MOUSEMOVE: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_LBUTTONDBLCLK: + case WM_RBUTTONDOWN: + case WM_RBUTTONUP: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONUP: + case WM_MBUTTONDBLCLK: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + { + if (HandleMouseEvent(ScreenInfo, Message, wParam, lParam)) + { + if (Message != WM_MOUSEWHEEL && Message != WM_MOUSEHWHEEL) + { + goto CallDefWin; + } + } + else + { + break; + } + + // Don't handle zoom. + if (wParam & MK_CONTROL) + { + goto CallDefWin; + } + + Status = 1; + + bool isMouseWheel = Message == WM_MOUSEWHEEL; + bool isMouseHWheel = Message == WM_MOUSEHWHEEL; + + if (isMouseWheel || isMouseHWheel) + { + short wheelDelta = (short)HIWORD(wParam); + bool hasShift = (wParam & MK_SHIFT) ? true : false; + + Scrolling::s_HandleMouseWheel(isMouseWheel, + isMouseHWheel, + wheelDelta, + hasShift, + ScreenInfo); + } + break; + } + + case CM_SET_WINDOW_SIZE: + { + Status = _InternalSetWindowSize(); + break; + } + + case CM_BEEP: + { + UnlockConsole(); + Unlock = FALSE; + + // Don't fall back to Beep() on win32 systems -- if the user configures their system for no sound, we should + // respect that. + PlaySoundW((LPCWSTR)SND_ALIAS_SYSTEMHAND, nullptr, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); + break; + } + + case CM_UPDATE_SCROLL_BARS: + { + ScreenInfo.InternalUpdateScrollBars(); + break; + } + + case CM_UPDATE_TITLE: + { + SetWindowTextW(hWnd, gci.GetTitleAndPrefix().c_str()); + break; + } + + case CM_UPDATE_EDITKEYS: + { + // Re-read the edit key settings from registry. + Registry reg(&gci); + reg.GetEditKeys(NULL); + break; + } + +#ifdef DBG + case CM_SET_KEY_STATE: + { + const int keyboardInputTableStateSize = 256; + if (wParam < keyboardInputTableStateSize) + { + BYTE keyState[keyboardInputTableStateSize]; + GetKeyboardState(keyState); + keyState[wParam] = static_cast(lParam); + SetKeyboardState(keyState); + } + else + { + LOG_HR_MSG(E_INVALIDARG, "CM_SET_KEY_STATE invalid wParam"); + } + break; + } + + case CM_SET_KEYBOARD_LAYOUT: + { + try + { + std::wstringstream wss; + wss << std::setfill(L'0') << std::setw(8) << wParam; + std::wstring wstr(wss.str()); + LoadKeyboardLayout(wstr.c_str(), KLF_ACTIVATE); + } + catch (...) + { + LOG_HR_MSG(wil::ResultFromCaughtException(), "CM_SET_KEYBOARD_LAYOUT exception"); + } + break; + } +#endif DBG + + case EVENT_CONSOLE_CARET: + case EVENT_CONSOLE_UPDATE_REGION: + case EVENT_CONSOLE_UPDATE_SIMPLE: + case EVENT_CONSOLE_UPDATE_SCROLL: + case EVENT_CONSOLE_LAYOUT: + case EVENT_CONSOLE_START_APPLICATION: + case EVENT_CONSOLE_END_APPLICATION: + { + NotifyWinEvent(Message, hWnd, (LONG)wParam, (LONG)lParam); + break; + } + + default: + CallDefWin: + { + if (Unlock) + { + UnlockConsole(); + Unlock = FALSE; + } + Status = DefWindowProcW(hWnd, Message, wParam, lParam); + break; + } + } + + if (Unlock) + { + UnlockConsole(); + } + + return Status; +} + +#pragma endregion + +// Helper handler methods for specific cases within the large window procedure are in this section +#pragma region Message Handlers + +void Window::_HandleWindowPosChanged(const LPARAM lParam) +{ + HWND hWnd = GetWindowHandle(); + SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + + LPWINDOWPOS const lpWindowPos = (LPWINDOWPOS)lParam; + + // If the frame changed, update the system metrics. + if (WI_IsFlagSet(lpWindowPos->flags, SWP_FRAMECHANGED)) + { + _UpdateSystemMetrics(); + } + + // This message is sent as the result of someone calling SetWindowPos(). We use it here to set/clear the + // CONSOLE_IS_ICONIC bit appropriately. doing so in the WM_SIZE handler is incorrect because the WM_SIZE + // comes after the WM_ERASEBKGND during SetWindowPos() processing, and the WM_ERASEBKGND needs to know if + // the console window is iconic or not. + if (!ScreenInfo.ResizingWindow && (lpWindowPos->cx || lpWindowPos->cy) && !IsIconic(hWnd)) + { + // calculate the dimensions for the newly proposed window rectangle + RECT rcNew; + s_ConvertWindowPosToWindowRect(lpWindowPos, &rcNew); + ServiceLocator::LocateWindowMetrics()->ConvertWindowRectToClientRect(&rcNew); + + // If the window is not being resized, including a DPI change, then + // don't do anything except update our windowrect + if (!WI_IsFlagSet(lpWindowPos->flags, SWP_NOSIZE) || _fInDPIChange) + { + ScreenInfo.ProcessResizeWindow(&rcNew, &_rcClientLast); + } + + // now that operations are complete, save the new rectangle size as the last seen value + _rcClientLast = rcNew; + } +} + +// Routine Description: +// - This helper method for the window procedure will handle the WM_PAINT message +// - It will retrieve the invalid rectangle and dispatch that information to the attached renderer +// (if available). It will then attempt to validate/finalize the paint to appease the system +// and prevent more WM_PAINTs from coming back (until of course something else causes an invalidation). +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded. ERROR_INVALID_HANDLE if there is no HWND. E_FAIL if GDI failed for some reason. +[[nodiscard]] +HRESULT Window::_HandlePaint() const +{ + HWND const hwnd = GetWindowHandle(); + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE), hwnd); + + // We have to call BeginPaint to retrieve the invalid rectangle state + // BeginPaint/EndPaint does a bunch of other magic in the system level + // that we can't sufficiently replicate with GetInvalidRect/ValidateRect. + // --- + // We've tried in the past to not call BeginPaint/EndPaint + // and under certain circumstances (windows with SW_HIDE, SKUs without DWM, etc.) + // the system either sends WM_PAINT messages ad nauseum or fails to redraw everything correctly. + PAINTSTRUCT ps; + HDC const hdc = BeginPaint(hwnd, &ps); + RETURN_HR_IF_NULL(E_FAIL, hdc); + + if (ServiceLocator::LocateGlobals().pRender != nullptr) + { + // In lieu of actually painting right now, we're just going to aggregate this information in the renderer + // and let it paint whenever it feels appropriate. + RECT const rcUpdate = ps.rcPaint; + ServiceLocator::LocateGlobals().pRender->TriggerSystemRedraw(&rcUpdate); + } + + LOG_IF_WIN32_BOOL_FALSE(EndPaint(hwnd, &ps)); + + return S_OK; +} + +// Routine Description: +// - This routine is called when ConsoleWindowProc receives a WM_DROPFILES message. +// - It initially calls DragQueryFile() to calculate the number of files dropped and then DragQueryFile() is called to retrieve the filename. +// - DoStringPaste() pastes the filename to the console window +// Arguments: +// - wParam - Identifies the structure containing the filenames of the dropped files. +// - Console - Pointer to CONSOLE_INFORMATION structure +// Return Value: +// - +void Window::_HandleDrop(const WPARAM wParam) const +{ + WCHAR szPath[MAX_PATH]; + BOOL fAddQuotes; + + if (DragQueryFile((HDROP)wParam, 0, szPath, ARRAYSIZE(szPath)) != 0) + { + // Log a telemetry flag saying the user interacted with the Console + // Only log when DragQueryFile succeeds, because if we don't when the console starts up, we're seeing + // _HandleDrop get called multiple times (and DragQueryFile fail), + // which can incorrectly mark this console session as interactive. + Telemetry::Instance().SetUserInteractive(); + + fAddQuotes = (wcschr(szPath, L' ') != nullptr); + if (fAddQuotes) + { + Clipboard::Instance().StringPaste(L"\"", 1); + } + + Clipboard::Instance().StringPaste(szPath, wcslen(szPath)); + + if (fAddQuotes) + { + Clipboard::Instance().StringPaste(L"\"", 1); + } + } +} + +LRESULT Window::_HandleGetObject(const HWND hwnd, const WPARAM wParam, const LPARAM lParam) +{ + LRESULT retVal = 0; + + // If we are receiving a request from Microsoft UI Automation framework, then return the basic UIA COM interface. + if (static_cast(lParam) == static_cast(UiaRootObjectId)) + { + // NOTE: Deliverable MSFT: 10881045 is required before this will work properly. + // The UIAutomationCore.dll cannot currently handle the fact that our HWND is assigned to the child PID. + // It will attempt to set up events/pipes on the wrong PID/HWND combination when called here. + // A temporary workaround until that is delivered is to disable window handle reparenting using + // ConsoleControl's ConsoleSetWindowOwner call. + retVal = UiaReturnRawElementProvider(hwnd, wParam, lParam, _GetUiaProvider()); + } + // Otherwise, return 0. We don't implement MS Active Accessibility (the other framework that calls WM_GETOBJECT). + + return retVal; +} + +#pragma endregion + +// Dispatchers are used to post or send a window message into the queue from other portions of the codebase without accessing internal properties directly +#pragma region Dispatchers + +BOOL Window::PostUpdateWindowSize() const +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const SCREEN_INFORMATION& ScreenInfo = GetScreenInfo(); + + if (ScreenInfo.ConvScreenInfo != nullptr) + { + return FALSE; + } + + if (gci.Flags & CONSOLE_SETTING_WINDOW_SIZE) + { + return FALSE; + } + + gci.Flags |= CONSOLE_SETTING_WINDOW_SIZE; + return PostMessageW(GetWindowHandle(), CM_SET_WINDOW_SIZE, (WPARAM)&ScreenInfo, 0); +} + +BOOL Window::SendNotifyBeep() const +{ + return SendNotifyMessageW(GetWindowHandle(), CM_BEEP, 0, 0); +} + +BOOL Window::PostUpdateScrollBars() const +{ + return PostMessageW(GetWindowHandle(), CM_UPDATE_SCROLL_BARS, (WPARAM)&GetScreenInfo(), 0); +} + +BOOL Window::PostUpdateExtendedEditKeys() const +{ + return PostMessageW(GetWindowHandle(), CM_UPDATE_EDITKEYS, 0, 0); +} + +#pragma endregion diff --git a/src/interactivity/win32/windowtheme.cpp b/src/interactivity/win32/windowtheme.cpp new file mode 100644 index 000000000..5ca98a213 --- /dev/null +++ b/src/interactivity/win32/windowtheme.cpp @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "windowtheme.hpp" + +#include + +using namespace Microsoft::Console::Interactivity::Win32; + +#define DWMWA_USE_IMMERSIVE_DARK_MODE 19 +#define DARK_MODE_STRING_NAME L"DarkMode_Explorer" +#define UXTHEME_DLL_NAME L"uxtheme.dll" +#define UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL 132 + +// Routine Description: +// - Constructs window theme class (holding module references for function lookups) +WindowTheme::WindowTheme() +{ + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _module.reset(LoadLibraryExW(UXTHEME_DLL_NAME, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); +} + +// Routine Description: +// - Attempts to set the dark mode on the given HWND. +// - Will check the system for user preferences and high contrast to see if it's a good idea +// before setting it. +// Arguments: +// - hwnd - Window to apply dark mode to +// Return Value: +// - S_OK or suitable HRESULT from theming or DWM engines. +[[nodiscard]] +HRESULT WindowTheme::TrySetDarkMode(HWND hwnd) const noexcept +{ + // I have to be a big B BOOL or DwnSetWindowAttribute will be upset (E_INVALIDARG) when I am passed in. + const BOOL isDarkMode = !!_IsDarkMode(); + + if (isDarkMode) + { + RETURN_IF_FAILED(SetWindowTheme(hwnd, DARK_MODE_STRING_NAME, nullptr)); + RETURN_IF_FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, sizeof(isDarkMode))); + } + else + { + RETURN_IF_FAILED(SetWindowTheme(hwnd, L"", nullptr)); + RETURN_IF_FAILED(DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, sizeof(isDarkMode))); + } + + return S_OK; +} + +// Routine Description: +// - Logical determination of if we should use the dark mode or not. +// - Combines user preferences and high contrast accessibility settings. +// Arguments: +// - +// Return Value: +// - TRUE if dark mode is allowed. FALSE if it is not. +bool WindowTheme::_IsDarkMode() const noexcept +{ + if (_ShouldAppsUseDarkMode() && !_IsHighContrast()) + { + return true; + } + else + { + return false; + } +} + +// Routine Description: +// - Looks up the high contrast state of the system. +// Arguments: +// - +// Return Value: +// - True if the system is in high contrast (shouldn't change theme further.) False otherwise. +bool WindowTheme::_IsHighContrast() const noexcept +{ + BOOL fHighContrast = FALSE; + HIGHCONTRAST hc = { sizeof(hc) }; + if (SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0)) + { + fHighContrast = (HCF_HIGHCONTRASTON & hc.dwFlags); + } + return fHighContrast; +} + +// Routine Description: +// - Looks up the user preference for dark mode. +// Arguments: +// - +// Return Value: +// - True if the user chose dark mode in settings. False otherwise. +bool WindowTheme::_ShouldAppsUseDarkMode() const noexcept +{ + if (_module.get() != nullptr) + { + typedef bool(WINAPI *PfnShouldAppsUseDarkMode)(); + + static bool tried = false; + static PfnShouldAppsUseDarkMode pfn = nullptr; + + if (!tried) + { + pfn = (PfnShouldAppsUseDarkMode)GetProcAddress(_module.get(), MAKEINTRESOURCEA(UXTHEME_SHOULDAPPSUSEDARKMODE_ORDINAL)); + } + + tried = true; + + if (pfn != nullptr) + { + return pfn(); + } + } + + return false; +} diff --git a/src/interactivity/win32/windowtheme.hpp b/src/interactivity/win32/windowtheme.hpp new file mode 100644 index 000000000..d4a25b97a --- /dev/null +++ b/src/interactivity/win32/windowtheme.hpp @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- windowtheme.hpp + +Abstract: +- This module is used for abstracting calls to set window themes. + +Author(s): +- Michael Niksa (MiNiksa) Oct-2018 +--*/ +#pragma once + +namespace Microsoft::Console::Interactivity::Win32 +{ + class WindowTheme final + { + public: + WindowTheme(); + + [[nodiscard]] + HRESULT TrySetDarkMode(HWND hwnd) const noexcept; + + private: + bool _IsDarkMode() const noexcept; + + bool _IsHighContrast() const noexcept; + bool _ShouldAppsUseDarkMode() const noexcept; + + wil::unique_hmodule _module; + }; +} diff --git a/src/internal/internal.vcxproj b/src/internal/internal.vcxproj new file mode 100644 index 000000000..793865d7c --- /dev/null +++ b/src/internal/internal.vcxproj @@ -0,0 +1,24 @@ + + + + + + + Create + + + + + + + + {EF3E32A7-5FF6-42B4-B6E2-96CD7D033F00} + Win32Proj + internal + Internal + ConInt + + + + + diff --git a/src/internal/precomp.cpp b/src/internal/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/internal/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/internal/precomp.h b/src/internal/precomp.h new file mode 100644 index 000000000..7a6c2dda0 --- /dev/null +++ b/src/internal/precomp.h @@ -0,0 +1,23 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#endif + +// Windows Header Files: +#include diff --git a/src/internal/stubs.cpp b/src/internal/stubs.cpp new file mode 100644 index 000000000..1636497fb --- /dev/null +++ b/src/internal/stubs.cpp @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "..\inc\conint.h" + +using namespace Microsoft::Console::Internal; + +[[nodiscard]] +HRESULT ProcessPolicy::CheckAppModelPolicy(const HANDLE /*hToken*/, + bool& fIsWrongWayBlocked) noexcept +{ + fIsWrongWayBlocked = false; + return S_OK; +} + +[[nodiscard]] +HRESULT ProcessPolicy::CheckIntegrityLevelPolicy(const HANDLE /*hOtherToken*/, + bool& fIsWrongWayBlocked) noexcept +{ + fIsWrongWayBlocked = false; + return S_OK; +} + +void EdpPolicy::AuditClipboard(const std::wstring_view /*destinationName*/) noexcept +{ +} + diff --git a/src/project.inc b/src/project.inc new file mode 100644 index 000000000..092249508 --- /dev/null +++ b/src/project.inc @@ -0,0 +1,50 @@ +# ------------------------------------- +# Windows Console +# - Common Project Configuration +# ------------------------------------- + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +UNICODE = 1 +C_DEFINES = $(C_DEFINES) -DUNICODE -D_UNICODE + +# ------------------------------------- +# CRT Configuration +# ------------------------------------- + +USE_UNICRT = 1 +USE_MSVCRT = 1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +MSC_WARNING_LEVEL = /W4 /WX +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +# ------------------------------------- +# Common Console Includes and Libraries +# ------------------------------------- +CONSOLE_SRC_PATH = $(PROJECT_ROOT)\core\console\open\src +CONSOLE_OBJ_PATH = $(WINCORE_OBJ_PATH)\console\open\src + +INCLUDES= \ + $(INCLUDES); \ + $(CONSOLE_SRC_PATH)\inc; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + $(MINCORE_INTERNAL_PRIV_SDK_INC_PATH_L); \ + $(ONECORE_INTERNAL_SDK_INC_PATH); \ + +# ------------------------------------- +# Linker Settings +# ------------------------------------- + +# Add the ConsoleTypes.natvis to our binaries, so the PDB's will have the info +# embedded by default +LINKER_FLAGS = $(LINKER_FLAGS) /natvis:$(CONSOLE_SRC_PATH)\..\tools\ConsoleTypes.natvis diff --git a/src/project.unittest.inc b/src/project.unittest.inc new file mode 100644 index 000000000..bffbe994f --- /dev/null +++ b/src/project.unittest.inc @@ -0,0 +1,36 @@ +# ------------------------------------- +# Windows Console +# - Common Test Project Configuration +# ------------------------------------- + +!include project.inc + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGET_DESTINATION = UnitTests +UNIVERSAL_TEST = 1 +TEST_CODE = 1 + +# ------------------------------------- +# Common Console Includes and Libraries +# ------------------------------------- + +INCLUDES = \ + $(INCLUDES); \ + $(CONSOLE_SRC_PATH)\inc\test; \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ diff --git a/src/propsheet/ColorControl.cpp b/src/propsheet/ColorControl.cpp new file mode 100644 index 000000000..789a1981f --- /dev/null +++ b/src/propsheet/ColorControl.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "ColorControl.h" + +COLORREF GetColorForItem(const int itemId) +{ + if (itemId >= IDD_COLOR_1 && itemId <= IDD_COLOR_16) + { + return AttrToRGB(itemId - IDD_COLOR_1); + } + else if (itemId == IDD_TERMINAL_FGCOLOR) + { + return g_fakeForegroundColor; + } + else if (itemId == IDD_TERMINAL_BGCOLOR) + { + return g_fakeBackgroundColor; + } + else if (itemId == IDD_TERMINAL_CURSOR_COLOR) + { + return g_fakeCursorColor; + } + return RGB(0xff, 0x0, 0xff); +} + +void SimpleColorDoPaint(const HWND hColor, PAINTSTRUCT& ps, const int ColorId) +{ + RECT rColor; + COLORREF rgbBrush; + HBRUSH hbr; + + GetClientRect(hColor, &rColor); + rgbBrush = GetNearestColor(ps.hdc, GetColorForItem(ColorId)); + if ((hbr = CreateSolidBrush(rgbBrush)) != NULL) { + InflateRect(&rColor, -1, -1); + FillRect(ps.hdc, &rColor, hbr); + DeleteObject(hbr); + } +} + +// Routine Description: +// - Window proc for the color buttons +LRESULT SimpleColorControlProc(const HWND hColor, const UINT wMsg, const WPARAM wParam, const LPARAM lParam) +{ + PAINTSTRUCT ps; + int ColorId; + HWND hDlg; + + ColorId = GetWindowLong(hColor, GWL_ID); + hDlg = GetParent(hColor); + + switch (wMsg) { + case WM_GETDLGCODE: + return DLGC_WANTARROWS | DLGC_WANTTAB; + break; + case WM_PAINT: + BeginPaint(hColor, &ps); + SimpleColorDoPaint(hColor, ps, ColorId); + EndPaint(hColor, &ps); + break; + default: + return DefWindowProc(hColor, wMsg, wParam, lParam); + break; + } + return TRUE; +} diff --git a/src/propsheet/ColorControl.h b/src/propsheet/ColorControl.h new file mode 100644 index 000000000..c8f082253 --- /dev/null +++ b/src/propsheet/ColorControl.h @@ -0,0 +1,18 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- ColorControl.h + +Abstract: +- This module contains the definitions for a color control on the prop sheet. + +Author(s): + Mike Griese (migrie) July-2018 +--*/ + +#pragma once + +LRESULT SimpleColorControlProc(HWND hColor, UINT wMsg, WPARAM wParam, LPARAM lParam); +void SimpleColorDoPaint(HWND hColor, PAINTSTRUCT& ps, int ColorId); diff --git a/src/propsheet/ColorsPage.cpp b/src/propsheet/ColorsPage.cpp new file mode 100644 index 000000000..594134a69 --- /dev/null +++ b/src/propsheet/ColorsPage.cpp @@ -0,0 +1,420 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "ColorsPage.h" +#include "ColorControl.h" + +static BYTE ColorArray[4]; +static int iColor; + +// Routine Description: +// - Window proc for the color buttons +LRESULT ColorTableControlProc(HWND hColor, UINT wMsg, WPARAM wParam, LPARAM lParam) +{ + PAINTSTRUCT ps; + int ColorId; + RECT rColor; + RECT rTemp; + HDC hdc; + HWND hWnd; + HWND hDlg; + + ColorId = GetWindowLong(hColor, GWL_ID); + hDlg = GetParent(hColor); + + switch (wMsg) { + case WM_SETFOCUS: + if (ColorArray[iColor] != (BYTE)(ColorId - IDD_COLOR_1)) { + hWnd = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + SetFocus(hWnd); + } + __fallthrough; + case WM_KILLFOCUS: + hdc = GetDC(hDlg); + hWnd = GetDlgItem(hDlg, IDD_COLOR_1); + GetWindowRect(hWnd, &rColor); + hWnd = GetDlgItem(hDlg, IDD_COLOR_16); + GetWindowRect(hWnd, &rTemp); + rColor.right = rTemp.right; + ScreenToClient(hDlg, (LPPOINT)&rColor.left); + ScreenToClient(hDlg, (LPPOINT)&rColor.right); + InflateRect(&rColor, 2, 2); + DrawFocusRect(hdc, &rColor); + ReleaseDC(hDlg, hdc); + break; + case WM_KEYDOWN: + switch (wParam) { + case VK_UP: + case VK_LEFT: + if (ColorId > IDD_COLOR_1) { + SendMessage(hDlg, CM_SETCOLOR, + ColorId - 1 - IDD_COLOR_1, (LPARAM)hColor); + } + break; + case VK_DOWN: + case VK_RIGHT: + if (ColorId < IDD_COLOR_16) { + SendMessage(hDlg, CM_SETCOLOR, + ColorId + 1 - IDD_COLOR_1, (LPARAM)hColor); + } + break; + case VK_TAB: + hWnd = GetDlgItem(hDlg, IDD_COLOR_1); + hWnd = GetNextDlgTabItem(hDlg, hWnd, GetKeyState(VK_SHIFT) < 0); + SetFocus(hWnd); + break; + default: + return DefWindowProc(hColor, wMsg, wParam, lParam); + } + break; + case WM_RBUTTONDOWN: + case WM_LBUTTONDOWN: + SendMessage(hDlg, CM_SETCOLOR, + ColorId - IDD_COLOR_1, (LPARAM)hColor); + break; + case WM_PAINT: + + BeginPaint(hColor, &ps); + GetClientRect(hColor, &rColor); + + // are we the selected color for the current object? + if (ColorArray[iColor] == (BYTE)(ColorId - IDD_COLOR_1)) { + // highlight the selected color + FrameRect(ps.hdc, &rColor, (HBRUSH)GetStockObject(BLACK_BRUSH)); + InflateRect(&rColor, -1, -1); + FrameRect(ps.hdc, &rColor, (HBRUSH)GetStockObject(BLACK_BRUSH)); + } + + SimpleColorDoPaint(hColor, ps, ColorId); + EndPaint(hColor, &ps); + break; + default: + return SimpleColorControlProc(hColor, wMsg, wParam, lParam); + break; + } + return TRUE; +} + +bool InitColorsDialog(HWND hDlg) +{ + ColorArray[IDD_COLOR_SCREEN_TEXT - IDD_COLOR_SCREEN_TEXT] = + LOBYTE(gpStateInfo->ScreenAttributes) & 0x0F; + ColorArray[IDD_COLOR_SCREEN_BKGND - IDD_COLOR_SCREEN_TEXT] = + LOBYTE(gpStateInfo->ScreenAttributes >> 4); + ColorArray[IDD_COLOR_POPUP_TEXT - IDD_COLOR_SCREEN_TEXT] = + LOBYTE(gpStateInfo->PopupAttributes) & 0x0F; + ColorArray[IDD_COLOR_POPUP_BKGND - IDD_COLOR_SCREEN_TEXT] = + LOBYTE(gpStateInfo->PopupAttributes >> 4); + CheckRadioButton(hDlg,IDD_COLOR_SCREEN_TEXT,IDD_COLOR_POPUP_BKGND,IDD_COLOR_SCREEN_BKGND); + iColor = IDD_COLOR_SCREEN_BKGND - IDD_COLOR_SCREEN_TEXT; + + // initialize size of edit controls + + SendDlgItemMessage(hDlg, IDD_COLOR_RED, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_COLOR_GREEN, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_COLOR_BLUE, EM_LIMITTEXT, 3, 0); + + // initialize arrow controls + + SendDlgItemMessage(hDlg, IDD_COLOR_REDSCROLL, UDM_SETRANGE, 0, + MAKELONG(255, 0)); + SendDlgItemMessage(hDlg, IDD_COLOR_REDSCROLL, UDM_SETPOS, 0, + MAKELONG(GetRValue(AttrToRGB(ColorArray[iColor])), 0)); + SendDlgItemMessage(hDlg, IDD_COLOR_GREENSCROLL, UDM_SETRANGE, 0, + MAKELONG(255, 0)); + SendDlgItemMessage(hDlg, IDD_COLOR_GREENSCROLL, UDM_SETPOS, 0, + MAKELONG(GetGValue(AttrToRGB(ColorArray[iColor])), 0)); + SendDlgItemMessage(hDlg, IDD_COLOR_BLUESCROLL, UDM_SETRANGE, 0, + MAKELONG(255, 0)); + SendDlgItemMessage(hDlg, IDD_COLOR_BLUESCROLL, UDM_SETPOS, 0, + MAKELONG(GetBValue(AttrToRGB(ColorArray[iColor])), 0)); + + CreateAndAssociateToolTipToControl(IDD_TRANSPARENCY, hDlg, IDS_TOOLTIP_OPACITY); + + SendMessage(GetDlgItem(hDlg, IDD_TRANSPARENCY), TBM_SETRANGE, FALSE, (LPARAM)MAKELONG(TRANSPARENCY_RANGE_MIN, BYTE_MAX)); + ToggleV2ColorControls(hDlg); + + return TRUE; +} + +// Routine Description: +// - Dialog proc for the color selection dialog box. +// +INT_PTR WINAPI ColorDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) +{ + UINT Value; + UINT Red; + UINT Green; + UINT Blue; + UINT Item; + HWND hWnd; + HWND hWndOld; + BOOL bOK; + static bool fHaveInitialized = false; + + switch (wMsg) { + case WM_INITDIALOG: + { + fHaveInitialized = true; + return InitColorsDialog(hDlg); + } + + case WM_COMMAND: + { + if (!fHaveInitialized) { + return FALSE; + } + + Item = LOWORD(wParam); + switch (Item) { + case IDD_COLOR_SCREEN_TEXT: + case IDD_COLOR_SCREEN_BKGND: + case IDD_COLOR_POPUP_TEXT: + case IDD_COLOR_POPUP_BKGND: + hWndOld = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + + iColor = Item - IDD_COLOR_SCREEN_TEXT; + + // repaint new color + hWnd = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + InvalidateRect(hWnd, NULL, TRUE); + + // repaint old color + if (hWndOld != hWnd) { + InvalidateRect(hWndOld, NULL, TRUE); + } + + return TRUE; + + case IDD_COLOR_RED: + case IDD_COLOR_GREEN: + case IDD_COLOR_BLUE: + { + switch (HIWORD(wParam)) { + case EN_UPDATE: + if (!CheckNum (hDlg, Item)) { + Undo((HWND)lParam); + } + break; + + case EN_CHANGE: + /* + * Update the state info structure + */ + Value = GetDlgItemInt(hDlg, Item, &bOK, TRUE); + if (bOK) { + if (Value > 255) { + UpdateItem(hDlg, Item, 255); + Value = 255; + } + if (Item == IDD_COLOR_RED) { + Red = Value; + } else { + Red = GetRValue(AttrToRGB(ColorArray[iColor])); + } + if (Item == IDD_COLOR_GREEN) { + Green = Value; + } else { + Green = GetGValue(AttrToRGB(ColorArray[iColor])); + } + if (Item == IDD_COLOR_BLUE) { + Blue = Value; + } else { + Blue = GetBValue(AttrToRGB(ColorArray[iColor])); + } + UpdateStateInfo(hDlg, ColorArray[iColor] + IDD_COLOR_1, + RGB(Red, Green, Blue)); + UpdateApplyButton(hDlg); + } + __fallthrough; + + case EN_KILLFOCUS: + /* + * Update the preview windows with the new value + */ + hWnd = GetDlgItem(hDlg, IDD_COLOR_SCREEN_COLORS); + InvalidateRect(hWnd, NULL, FALSE); + hWnd = GetDlgItem(hDlg, IDD_COLOR_POPUP_COLORS); + InvalidateRect(hWnd, NULL, FALSE); + hWnd = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + InvalidateRect(hWnd, NULL, FALSE); + break; + } + return TRUE; + } + } + break; + } + + case WM_NOTIFY: + { + const PSHNOTIFY * const pshn = (LPPSHNOTIFY)lParam; + switch (pshn->hdr.code) { + case PSN_APPLY: + /* + * Write out the state values and exit. + */ + + // Ignore opacity in v1 console + if (g_fForceV2) + { + gpStateInfo->bWindowTransparency = (BYTE)SendDlgItemMessage(hDlg, IDD_TRANSPARENCY, TBM_GETPOS, 0, 0); + } + + EndDlgPage(hDlg, !pshn->lParam); + return TRUE; + + case PSN_RESET: + // Ignore opacity in v1 console + if (g_fForceV2 && g_bPreviewOpacity != gpStateInfo->bWindowTransparency) + { + SetLayeredWindowAttributes(gpStateInfo->hWnd, 0, gpStateInfo->bWindowTransparency, LWA_ALPHA); + } + + return 0; + + case PSN_SETACTIVE: + ToggleV2ColorControls(hDlg); + return 0; + + case PSN_KILLACTIVE: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID(GetFocus()); + if (Item) + { + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + } + return TRUE; + } + break; + } + + case WM_VSCROLL: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID((HWND)lParam) - 1; + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + return TRUE; + + case WM_HSCROLL: + if (IDD_TRANSPARENCY == GetDlgCtrlID((HWND) lParam)) + { + switch (LOWORD(wParam)) + { + //When moving slider with the mouse + case TB_THUMBPOSITION: + case TB_THUMBTRACK: + g_bPreviewOpacity = (BYTE) HIWORD(wParam); + break; + + //moving via keyboard + default: + g_bPreviewOpacity = (BYTE) SendMessage((HWND) lParam, TBM_GETPOS, 0, 0); + } + + PreviewOpacity(hDlg, g_bPreviewOpacity); + UpdateApplyButton(hDlg); + + return TRUE; + } + break; + + case CM_SETCOLOR: + UpdateStateInfo(hDlg, iColor + IDD_COLOR_SCREEN_TEXT, (UINT)wParam); + UpdateApplyButton(hDlg); + + hWndOld = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + + ColorArray[iColor] = (BYTE)wParam; + + /* Force the preview window to repaint */ + + if (iColor < (IDD_COLOR_POPUP_TEXT - IDD_COLOR_SCREEN_TEXT)) { + hWnd = GetDlgItem(hDlg, IDD_COLOR_SCREEN_COLORS); + } else { + hWnd = GetDlgItem(hDlg, IDD_COLOR_POPUP_COLORS); + } + InvalidateRect(hWnd, NULL, TRUE); + + // repaint new color + hWnd = GetDlgItem(hDlg, ColorArray[iColor]+IDD_COLOR_1); + InvalidateRect(hWnd, NULL, TRUE); + SetFocus(hWnd); + + UpdateItem(hDlg, IDD_COLOR_RED, + GetRValue(AttrToRGB(ColorArray[iColor]))); + UpdateItem(hDlg, IDD_COLOR_GREEN, + GetGValue(AttrToRGB(ColorArray[iColor]))); + UpdateItem(hDlg, IDD_COLOR_BLUE, + GetBValue(AttrToRGB(ColorArray[iColor]))); + + // repaint old color + if (hWndOld != hWnd) { + InvalidateRect(hWndOld, NULL, TRUE); + } + return TRUE; + + default: + break; + } + + return FALSE; +} + + +// enables or disables color page dialog controls depending on whether V2 is enabled or not +void ToggleV2ColorControls(__in const HWND hDlg) +{ + EnableWindow(GetDlgItem(hDlg, IDD_TRANSPARENCY), g_fForceV2); + SetOpacitySlider(hDlg); + + EnableWindow(GetDlgItem(hDlg, IDD_OPACITY_GROUPBOX), g_fForceV2); + EnableWindow(GetDlgItem(hDlg, IDD_OPACITY_LOW_LABEL), g_fForceV2); + EnableWindow(GetDlgItem(hDlg, IDD_OPACITY_HIGH_LABEL), g_fForceV2); + EnableWindow(GetDlgItem(hDlg, IDD_OPACITY_VALUE), g_fForceV2); +} + + +void PreviewOpacity(HWND hDlg, BYTE bOpacity) +{ + // Ignore opacity in v1 console + if (g_fForceV2) + { + WCHAR wszOpacityValue[4]; + HWND hWndConsole = gpStateInfo->hWnd; + + StringCchPrintf(wszOpacityValue, ARRAYSIZE(wszOpacityValue), L"%d", (int) ((float) bOpacity / BYTE_MAX * 100)); + SetDlgItemText(hDlg, IDD_OPACITY_VALUE, wszOpacityValue); + + if (hWndConsole) + { + // TODO: CONSIDER:What happens when this code is eventually ported directly into the shell. Which "hWnd" does this apply to then? Desktop? + // Hopefully it is simply null. -RSE http://osgvsowi/2136073 + SetLayeredWindowAttributes(hWndConsole, 0, bOpacity, LWA_ALPHA); + } + } +} + +void SetOpacitySlider(__in HWND hDlg) +{ + if (g_fForceV2) + { + if (0 == g_bPreviewOpacity) //if no preview opacity has been set yet... + { + g_bPreviewOpacity = (gpStateInfo->bWindowTransparency >= TRANSPARENCY_RANGE_MIN) ? gpStateInfo->bWindowTransparency : BYTE_MAX; + } + } + else + { + g_bPreviewOpacity = BYTE_MAX; //always fully opaque in V1 + } + + SendMessage(GetDlgItem(hDlg, IDD_TRANSPARENCY), TBM_SETPOS, TRUE, (LPARAM)(g_bPreviewOpacity)); + PreviewOpacity(hDlg, g_bPreviewOpacity); +} diff --git a/src/propsheet/ColorsPage.h b/src/propsheet/ColorsPage.h new file mode 100644 index 000000000..c6312b51f --- /dev/null +++ b/src/propsheet/ColorsPage.h @@ -0,0 +1,21 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- ColorsPage.h + +Abstract: +- This module contains the definitions for console colors dialog. + +Author(s): + Mike Griese (migrie) Oct-2016 +--*/ + +#pragma once + +void ToggleV2ColorControls(__in const HWND hDlg); +INT_PTR WINAPI ColorDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); +void SetOpacitySlider(__in HWND hDlg); +void PreviewOpacity(HWND hDlg, BYTE bOpacity); +LRESULT ColorTableControlProc(HWND hColor, UINT wMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/propsheet/LayoutPage.cpp b/src/propsheet/LayoutPage.cpp new file mode 100644 index 000000000..367691a1c --- /dev/null +++ b/src/propsheet/LayoutPage.cpp @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +BOOL g_fScreenSizeDlgInitialized = FALSE; +BOOL g_fInScreenSizeSETACTIVE = FALSE; + + +BOOL GetStateInfo(HWND /*hDlg*/, UINT Item, __out LPINT lpValue) +{ + BOOL bRet = TRUE; + int Value; + + switch (Item) { + case IDD_SCRBUF_WIDTH: + Value = gpStateInfo->ScreenBufferSize.X; + break; + case IDD_SCRBUF_HEIGHT: + Value = gpStateInfo->ScreenBufferSize.Y; + break; + case IDD_WINDOW_WIDTH: + Value = gpStateInfo->WindowSize.X; + break; + case IDD_WINDOW_HEIGHT: + Value = gpStateInfo->WindowSize.Y; + break; + case IDD_WINDOW_POSX: + Value = gpStateInfo->WindowPosX; + break; + case IDD_WINDOW_POSY: + Value = gpStateInfo->WindowPosY; + break; + default: + Value = 0; + bRet = FALSE; + break; + } + + *lpValue = Value; + return bRet; +} + +/*++ + Dialog proc for the screen size dialog box. +--*/ +INT_PTR WINAPI ScreenSizeDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) +{ + UINT Value; + UINT Item; + HWND hWnd; + BOOL bOK; + LONG xScreen; + LONG yScreen; + LONG cxScreen; + LONG cyScreen; + LONG cxFrame; + LONG cyFrame; + + switch (wMsg) { + case WM_INITDIALOG: + // initialize size of edit controls + + SendDlgItemMessage(hDlg, IDD_SCRBUF_WIDTH, EM_LIMITTEXT, 4, 0); + SendDlgItemMessage(hDlg, IDD_SCRBUF_HEIGHT, EM_LIMITTEXT, 4, 0); + SendDlgItemMessage(hDlg, IDD_WINDOW_WIDTH, EM_LIMITTEXT, 4, 0); + SendDlgItemMessage(hDlg, IDD_WINDOW_HEIGHT, EM_LIMITTEXT, 4, 0); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSX, EM_LIMITTEXT, 5, 0); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSY, EM_LIMITTEXT, 5, 0); + + // Get some system parameters + + xScreen = GetSystemMetrics(SM_XVIRTUALSCREEN); + yScreen = GetSystemMetrics(SM_YVIRTUALSCREEN); + cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN); + cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN); + cxFrame = GetSystemMetrics(SM_CXFRAME); + cyFrame = GetSystemMetrics(SM_CYFRAME); + + // initialize arrow controls + + SendDlgItemMessage(hDlg, IDD_SCRBUF_WIDTHSCROLL, UDM_SETRANGE, 0, + MAKELONG(9999, 1)); + SendDlgItemMessage(hDlg, IDD_SCRBUF_WIDTHSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->ScreenBufferSize.X, 0)); + SendDlgItemMessage(hDlg, IDD_SCRBUF_HEIGHTSCROLL, UDM_SETRANGE, 0, + MAKELONG(9999, 1)); + SendDlgItemMessage(hDlg, IDD_SCRBUF_HEIGHTSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->ScreenBufferSize.Y, 0)); + SendDlgItemMessage(hDlg, IDD_WINDOW_WIDTHSCROLL, UDM_SETRANGE, 0, + MAKELONG(9999, 1)); + SendDlgItemMessage(hDlg, IDD_WINDOW_WIDTHSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->WindowSize.X, 0)); + SendDlgItemMessage(hDlg, IDD_WINDOW_HEIGHTSCROLL, UDM_SETRANGE, 0, + MAKELONG(9999, 1)); + SendDlgItemMessage(hDlg, IDD_WINDOW_HEIGHTSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->WindowSize.Y, 0)); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSXSCROLL, UDM_SETRANGE, 0, + MAKELONG(xScreen + cxScreen - cxFrame, xScreen - cxFrame)); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSXSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->WindowPosX, 0)); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSYSCROLL, UDM_SETRANGE, 0, + MAKELONG(yScreen + cyScreen - cyFrame, yScreen - cyFrame)); + SendDlgItemMessage(hDlg, IDD_WINDOW_POSYSCROLL, UDM_SETPOS, 0, + MAKELONG(gpStateInfo->WindowPosY, 0)); + + // + // put current values in dialog box + // + + CheckDlgButton(hDlg, IDD_AUTO_POSITION, gpStateInfo->AutoPosition); + SendMessage(hDlg, WM_COMMAND, IDD_AUTO_POSITION, 0); + + CheckDlgButton(hDlg, IDD_LINE_WRAP, gpStateInfo->fWrapText); + CreateAndAssociateToolTipToControl(IDD_LINE_WRAP, hDlg, IDS_TOOLTIP_LINE_WRAP); + ToggleV2ColorControls(hDlg); + g_fScreenSizeDlgInitialized = TRUE; + + return TRUE; + + case WM_VSCROLL: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID((HWND)lParam) - 1; + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + return TRUE; + + case WM_COMMAND: + Item = LOWORD(wParam); + switch (Item) { + case IDD_SCRBUF_WIDTH: + case IDD_SCRBUF_HEIGHT: + case IDD_WINDOW_WIDTH: + case IDD_WINDOW_HEIGHT: + case IDD_WINDOW_POSX: + case IDD_WINDOW_POSY: + switch (HIWORD(wParam)) { + case EN_UPDATE: + if (!CheckNum (hDlg, Item)) { + Undo((HWND)lParam); + } else if (!g_fInScreenSizeSETACTIVE && g_fScreenSizeDlgInitialized) { + UpdateApplyButton(hDlg); + } + + break; + case EN_KILLFOCUS: + /* + * Update the state info structure + */ + Value = (UINT)SendDlgItemMessage(hDlg, Item + 1, UDM_GETPOS, 0, 0); + if (HIWORD(Value) == 0) { + UpdateStateInfo(hDlg, Item, (SHORT)LOWORD(Value)); + } else { + Value = GetStateInfo(hDlg, Item, &bOK); + if (bOK) { + UpdateItem(hDlg, Item, Value); + } + } + + /* + * Update the preview window with the new value + */ + hWnd = GetDlgItem(hDlg, IDD_PREVIEWWINDOW); + SendMessage(hWnd, CM_PREVIEW_UPDATE, 0, 0); + break; + } + return TRUE; + + case IDD_LINE_WRAP: + { + const BOOL fCurrentLineWrap = (IsDlgButtonChecked(hDlg, IDD_LINE_WRAP) == BST_CHECKED); + gpStateInfo->fWrapText = fCurrentLineWrap; + EnableWindow(GetDlgItem(hDlg, IDD_SCRBUF_WIDTH), g_fForceV2 ? !fCurrentLineWrap : TRUE); + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_AUTO_POSITION: + Value = IsDlgButtonChecked(hDlg, IDD_AUTO_POSITION); + UpdateStateInfo(hDlg, IDD_AUTO_POSITION, Value); + if (g_fScreenSizeDlgInitialized) { + UpdateApplyButton(hDlg); + } + + for (Item = IDD_WINDOW_POSX; Item < IDD_AUTO_POSITION; Item++) { + hWnd = GetDlgItem(hDlg, Item); + EnableWindow(hWnd, (Value == FALSE)); + } + break; + + default: + break; + } + break; + + case WM_NOTIFY: + { + const PSHNOTIFY * const pshn = (LPPSHNOTIFY)lParam; + switch (pshn->hdr.code) { + case PSN_APPLY: + /* + * Write out the state values and exit. + */ + EndDlgPage(hDlg, !pshn->lParam); + return TRUE; + + case PSN_KILLACTIVE: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID(GetFocus()); + if (Item) + { + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + } + return TRUE; + + case PSN_SETACTIVE: + // When page becomes active, ensure that window and screen size box availablility + // is updated based on the Word Wrap status. + g_fInScreenSizeSETACTIVE = TRUE; + if (g_fForceV2 && gpStateInfo->fWrapText) + { + EnableWindow(GetDlgItem(hDlg, IDD_SCRBUF_WIDTH), FALSE); + + gpStateInfo->ScreenBufferSize.X = gpStateInfo->WindowSize.X; + UpdateItem(hDlg, IDD_SCRBUF_WIDTH, gpStateInfo->ScreenBufferSize.X); + + // Force the preview window to update as well + hWnd = GetDlgItem(hDlg, IDD_PREVIEWWINDOW); + SendMessage(hWnd, CM_PREVIEW_UPDATE, 0, 0); + } + else + { + EnableWindow(GetDlgItem(hDlg, IDD_SCRBUF_WIDTH), TRUE); + } + + ToggleV2LayoutControls(hDlg); + g_fInScreenSizeSETACTIVE = FALSE; + return 0; + } + break; + } + + default: + break; + } + + return FALSE; +} + + +// enables or disables layout page dialog controls depending on whether V2 is enabled or not +void ToggleV2LayoutControls(__in const HWND hDlg) +{ + EnableWindow(GetDlgItem(hDlg, IDD_LINE_WRAP), g_fForceV2); + CheckDlgButton(hDlg, IDD_LINE_WRAP, g_fForceV2 ? gpStateInfo->fWrapText : FALSE); + EnableWindow(GetDlgItem(hDlg, IDD_SCRBUF_WIDTH), g_fForceV2 ? !gpStateInfo->fWrapText : TRUE); +} diff --git a/src/propsheet/LayoutPage.h b/src/propsheet/LayoutPage.h new file mode 100644 index 000000000..9272536da --- /dev/null +++ b/src/propsheet/LayoutPage.h @@ -0,0 +1,18 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- LayoutPage.h + +Abstract: +- This module contains the definitions for console layout dialog. + +Author(s): + Mike Griese (migrie) Oct-2016 +--*/ + +#pragma once + +void ToggleV2LayoutControls(__in const HWND hDlg); +INT_PTR WINAPI ScreenSizeDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/propsheet/OptionsPage.cpp b/src/propsheet/OptionsPage.cpp new file mode 100644 index 000000000..40b1c007f --- /dev/null +++ b/src/propsheet/OptionsPage.cpp @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +bool OptionsCommandCallback(HWND hDlg, const unsigned int Item, const unsigned int Notification, HWND hControlWindow) +{ + UINT Value; + BOOL bOK; + + switch (Item) { + case IDD_LANGUAGELIST: + switch (Notification) { + case CBN_SELCHANGE: { + HWND hWndLanguageCombo; + LONG lListIndex; + + hWndLanguageCombo = GetDlgItem(hDlg, IDD_LANGUAGELIST); + lListIndex = (LONG)SendMessage(hWndLanguageCombo, CB_GETCURSEL, 0, 0); + Value = (UINT)SendMessage(hWndLanguageCombo, CB_GETITEMDATA, lListIndex, 0); + if (Value != -1) { + fChangeCodePage = (Value != gpStateInfo->CodePage); + UpdateStateInfo(hDlg, Item, Value); + UpdateApplyButton(hDlg); + } + break; + } + + default: + DBGFONTS(("unhandled CBN_%x from POINTSLIST\n",Notification)); + break; + } + return TRUE; + case IDD_CURSOR_SMALL: + case IDD_CURSOR_MEDIUM: + case IDD_CURSOR_LARGE: + UpdateStateInfo(hDlg, Item, 0); + if (Notification != EN_KILLFOCUS) { + // we don't want to light up the apply button just because a cursor selection lost focus -- this can + // happen when switching between tabs even if there's no actual change in selection. + UpdateApplyButton(hDlg); + } + return TRUE; + + case IDD_HISTORY_NODUP: + case IDD_QUICKEDIT: + case IDD_INSERT: + Value = IsDlgButtonChecked(hDlg, Item); + UpdateStateInfo(hDlg, Item, Value); + UpdateApplyButton(hDlg); + return TRUE; + + case IDD_HISTORY_SIZE: + case IDD_HISTORY_NUM: + switch (Notification) { + case EN_UPDATE: + if (!CheckNum(hDlg, Item)) { + Undo(hControlWindow); + } else if (g_fSettingsDlgInitialized) { + UpdateApplyButton(hDlg); + } + break; + + case EN_KILLFOCUS: + /* + * Update the state info structure + */ + Value = GetDlgItemInt(hDlg, Item, &bOK, TRUE); + if (bOK) { + UpdateStateInfo(hDlg, Item, Value); + UpdateApplyButton(hDlg); + } + break; + } + return TRUE; + + case IDD_FORCEV2: + { + g_fForceV2 = (IsDlgButtonChecked(hDlg, IDD_FORCEV2) != BST_CHECKED); + ToggleV2OptionsControls(hDlg); + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_LINE_SELECTION: + { + const BOOL fCurrentLineSelection = (IsDlgButtonChecked(hDlg, IDD_LINE_SELECTION) == BST_CHECKED); + gpStateInfo->fLineSelection = fCurrentLineSelection; + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_FILTER_ON_PASTE: + { + const BOOL fCurrentFilterOnPaste = (IsDlgButtonChecked(hDlg, IDD_FILTER_ON_PASTE) == BST_CHECKED); + gpStateInfo->fFilterOnPaste = fCurrentFilterOnPaste; + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_INTERCEPT_COPY_PASTE: + { + const BOOL fCurrentInterceptCopyPaste = (IsDlgButtonChecked(hDlg, IDD_INTERCEPT_COPY_PASTE) == BST_CHECKED); + gpStateInfo->InterceptCopyPaste = fCurrentInterceptCopyPaste; + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_CTRL_KEYS_ENABLED: + { + // NOTE: the checkbox being checked means that Ctrl keys should be enabled, hence the negation here + const BOOL fCurrentCtrlKeysDisabled = !(IsDlgButtonChecked(hDlg, IDD_CTRL_KEYS_ENABLED) == BST_CHECKED); + gpStateInfo->fCtrlKeyShortcutsDisabled = fCurrentCtrlKeysDisabled; + UpdateApplyButton(hDlg); + return TRUE; + } + + case IDD_EDIT_KEYS: + { + const BOOL fCurrentEditKeys = (IsDlgButtonChecked(hDlg, IDD_EDIT_KEYS) == BST_CHECKED); + g_fEditKeys = fCurrentEditKeys; + UpdateApplyButton(hDlg); + return TRUE; + } + + } + return FALSE; +} + +// Routine Description: +// - Dialog proc for the settings dialog box. +// +INT_PTR WINAPI SettingsDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) +{ + UINT Item; + UINT Notification; + + switch (wMsg) { + case WM_INITDIALOG: + CheckDlgButton(hDlg, IDD_HISTORY_NODUP, gpStateInfo->HistoryNoDup); + CheckDlgButton(hDlg, IDD_QUICKEDIT, gpStateInfo->QuickEdit); + CheckDlgButton(hDlg, IDD_INSERT, gpStateInfo->InsertMode); + + // v2 options + CheckDlgButton(hDlg, IDD_FORCEV2, !g_fForceV2); + CheckDlgButton(hDlg, IDD_LINE_SELECTION, gpStateInfo->fLineSelection); + CheckDlgButton(hDlg, IDD_FILTER_ON_PASTE, gpStateInfo->fFilterOnPaste); + CheckDlgButton(hDlg, IDD_CTRL_KEYS_ENABLED, !gpStateInfo->fCtrlKeyShortcutsDisabled); + CheckDlgButton(hDlg, IDD_EDIT_KEYS, g_fEditKeys); + CheckDlgButton(hDlg, IDD_INTERCEPT_COPY_PASTE, gpStateInfo->InterceptCopyPaste); + + // tooltips + CreateAndAssociateToolTipToControl(IDD_LINE_SELECTION, hDlg, IDS_TOOLTIP_LINE_SELECTION); + CreateAndAssociateToolTipToControl(IDD_FILTER_ON_PASTE, hDlg, IDS_TOOLTIP_FILTER_ON_PASTE); + CreateAndAssociateToolTipToControl(IDD_CTRL_KEYS_ENABLED, hDlg, IDS_TOOLTIP_CTRL_KEYS); + CreateAndAssociateToolTipToControl(IDD_EDIT_KEYS, hDlg, IDS_TOOLTIP_EDIT_KEYS); + CreateAndAssociateToolTipToControl(IDD_INTERCEPT_COPY_PASTE, hDlg, IDS_TOOLTIP_INTERCEPT_COPY_PASTE); + + // initialize cursor radio buttons + if (gpStateInfo->CursorSize <= 25) { + Item = IDD_CURSOR_SMALL; + } else if (gpStateInfo->CursorSize <= 50) { + Item = IDD_CURSOR_MEDIUM; + } else { + Item = IDD_CURSOR_LARGE; + } + CheckRadioButton(hDlg, IDD_CURSOR_SMALL, IDD_CURSOR_LARGE, Item); + + SetDlgItemInt(hDlg, IDD_HISTORY_SIZE, gpStateInfo->HistoryBufferSize, + FALSE); + SendDlgItemMessage(hDlg, IDD_HISTORY_SIZE, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_HISTORY_SIZESCROLL, UDM_SETRANGE, 0, + MAKELONG(999, 1)); + + SetDlgItemInt(hDlg, IDD_HISTORY_NUM, gpStateInfo->NumberOfHistoryBuffers, FALSE); + SendDlgItemMessage(hDlg, IDD_HISTORY_NUM, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_HISTORY_NUM, EM_SETSEL, 0, (DWORD)-1); + SendDlgItemMessage(hDlg, IDD_HISTORY_NUMSCROLL, UDM_SETRANGE, 0, + MAKELONG(999, 1)); + + if (g_fEastAsianSystem) { + // In CJK systems, we always show the codepage on both the defaults and non-defaults propsheets + if (gpStateInfo->Defaults) { + LanguageListCreate(hDlg, gpStateInfo->CodePage); + } else { + LanguageDisplay(hDlg, gpStateInfo->CodePage); + } + } else { + // On non-CJK systems, we show the codepage on a non-default propsheet, but don't allow the user to view or + // change it on the defaults propsheet + const HWND hwndLanguageGroupbox = GetDlgItem(hDlg, IDD_LANGUAGE_GROUPBOX); + if (hwndLanguageGroupbox) { + if (gpStateInfo->Defaults) { + const HWND hwndLanguageList = GetDlgItem(hDlg, IDD_LANGUAGELIST); + ShowWindow(hwndLanguageList, SW_HIDE); + ShowWindow(hwndLanguageGroupbox, SW_HIDE); + } else { + const HWND hwndLanguage = GetDlgItem(hDlg, IDD_LANGUAGE); + LanguageDisplay(hDlg, gpStateInfo->CodePage); + ShowWindow(hwndLanguage, SW_SHOW); + ShowWindow(hwndLanguageGroupbox, SW_SHOW); + } + } + } + + g_fSettingsDlgInitialized = TRUE; + return TRUE; + + case WM_COMMAND: + Item = LOWORD(wParam); + Notification = HIWORD(wParam); + return OptionsCommandCallback(hDlg, Item, Notification, (HWND)lParam); + + case WM_NOTIFY: + { + if (lParam && (wParam == IDD_HELP_SYSLINK || wParam == IDD_HELP_LEGACY_LINK)) + { + // handle hyperlink click or keyboard activation + switch(((LPNMHDR)lParam)->code) + { + case NM_CLICK: + case NM_RETURN: + { + PNMLINK pnmLink = (PNMLINK)lParam; + if (0 == pnmLink->item.iLink) + { + ShellExecute(NULL, + L"open", + pnmLink->item.szUrl, + NULL, + NULL, + SW_SHOW); + } + + break; + } + } + } + else + { + const PSHNOTIFY * const pshn = (LPPSHNOTIFY)lParam; + switch (pshn->hdr.code) { + case PSN_APPLY: + /* + * Write out the state values and exit. + */ + EndDlgPage(hDlg, !pshn->lParam); + return TRUE; + + case PSN_SETACTIVE: + ToggleV2OptionsControls(hDlg); + return 0; + + case PSN_KILLACTIVE: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID(GetFocus()); + if (Item) + { + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + } + return TRUE; + } + } + + break; + } + + case WM_VSCROLL: + /* + * Fake the dialog proc into thinking the edit control just + * lost focus so it'll update properly + */ + Item = GetDlgCtrlID((HWND)lParam) - 1; + SendMessage(hDlg, WM_COMMAND, MAKELONG(Item, EN_KILLFOCUS), 0); + return TRUE; + + default: + break; + } + + return FALSE; +} + +// enables or disables options page dialog controls depending on whether V2 is enabled or not +void ToggleV2OptionsControls(__in const HWND hDlg) +{ + EnableWindow(GetDlgItem(hDlg, IDD_LINE_SELECTION), g_fForceV2); + CheckDlgButton(hDlg, IDD_LINE_SELECTION, g_fForceV2 ? gpStateInfo->fLineSelection : FALSE); + + EnableWindow(GetDlgItem(hDlg, IDD_FILTER_ON_PASTE), g_fForceV2); + CheckDlgButton(hDlg, IDD_FILTER_ON_PASTE, g_fForceV2 ? gpStateInfo->fFilterOnPaste : FALSE); + + EnableWindow(GetDlgItem(hDlg, IDD_CTRL_KEYS_ENABLED), g_fForceV2); + CheckDlgButton(hDlg, IDD_CTRL_KEYS_ENABLED, g_fForceV2 ? !gpStateInfo->fCtrlKeyShortcutsDisabled : FALSE); + + EnableWindow(GetDlgItem(hDlg, IDD_EDIT_KEYS), g_fForceV2); + CheckDlgButton(hDlg, IDD_EDIT_KEYS, g_fForceV2 ? g_fEditKeys : FALSE); + + EnableWindow(GetDlgItem(hDlg, IDD_INTERCEPT_COPY_PASTE), g_fForceV2); + CheckDlgButton(hDlg, IDD_INTERCEPT_COPY_PASTE, g_fForceV2 ? gpStateInfo->InterceptCopyPaste : FALSE); +} diff --git a/src/propsheet/OptionsPage.h b/src/propsheet/OptionsPage.h new file mode 100644 index 000000000..21ab26cbe --- /dev/null +++ b/src/propsheet/OptionsPage.h @@ -0,0 +1,18 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- OptionsPage.h + +Abstract: +- This module contains the definitions for console options dialog. + +Author(s): + Mike Griese (migrie) Oct-2016 +--*/ + +#pragma once + +void ToggleV2OptionsControls(__in const HWND hDlg); +INT_PTR WINAPI SettingsDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/propsheet/PropSheetHandler.cpp b/src/propsheet/PropSheetHandler.cpp new file mode 100644 index 000000000..871288593 --- /dev/null +++ b/src/propsheet/PropSheetHandler.cpp @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include +#include +#include + +#include +#include + +#include + +#define PEMAGIC ((WORD)'P'+((WORD)'E'<<8)) +static CONSOLE_STATE_INFO g_csi; + +using namespace Microsoft::WRL; + +// This class exposes console property sheets for use when launching the filesystem shortcut properties dialog. +[uuid(D2942F8E-478E-41D3-870A-35A16238F4EE)] +class ConsolePropertySheetHandler WrlFinal : public RuntimeClass, IShellExtInit, IShellPropSheetExt, IPersist, FtmBase> +{ +public: + HRESULT RuntimeClassInitialize() + { + return S_OK; + } + + // IPersist + STDMETHODIMP GetClassID(_Out_ CLSID *clsid) override + { + *clsid = __uuidof(this); + return S_OK; + } + + // IShellExtInit + // Shell QI's for IShellExtInit and calls Initialize first. If we return a succeeding HRESULT, the shell will QI for + // IShellPropSheetExt and call AddPages. A failing HRESULT causes the shell to skip us. + STDMETHODIMP Initialize(_In_ PCIDLIST_ABSOLUTE /*pidlFolder*/, _In_ IDataObject *pdtobj, _In_ HKEY /*hkeyProgID*/) + { + WCHAR szLinkFileName[MAX_PATH]; + HRESULT hr = _ShouldAddPropertySheet(pdtobj, szLinkFileName, ARRAYSIZE(szLinkFileName)); + if (SUCCEEDED(hr)) + { + hr = InitializeConsoleState() ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + hr = _InitializeGlobalStateInfo(szLinkFileName); + } + } + + return hr; + } + + // IShellPropSheetExt + STDMETHODIMP AddPages(_In_ LPFNADDPROPSHEETPAGE pfnAddPage, _In_ LPARAM lParam) + { + PROPSHEETPAGE psp[NUMBER_OF_PAGES] = {}; + HRESULT hr = PopulatePropSheetPageArray(psp, ARRAYSIZE(psp), TRUE /*fRegisterCallbacks*/) ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + for (UINT ipsp = 0; ipsp < ARRAYSIZE(psp) && SUCCEEDED(hr); ipsp++) + { + HPROPSHEETPAGE hPage = CreatePropertySheetPage(&psp[ipsp]); + hr = (hPage == nullptr) ? E_FAIL : S_OK; + if (SUCCEEDED(hr)) + { + pfnAddPage(hPage, lParam); + } + } + } + + return hr; + } + + STDMETHODIMP ReplacePage(_In_ UINT /*uPageID*/, _In_ LPFNADDPROPSHEETPAGE /*pfnReplacePage*/, _In_ LPARAM /*lParam*/) + { + // Implementation not needed -- MSDN says "Replaces a page in a property sheet for a Control Panel object.", + // which we don't need to do. + return E_NOTIMPL; + } + +private: + ~ConsolePropertySheetHandler() = default; + + HRESULT _InitializeGlobalStateInfo(_In_ PCWSTR pszLinkFileName) + { + g_fHostedInFileProperties = TRUE; + gpStateInfo = &g_csi; + InitRegistryValues(gpStateInfo); + gpStateInfo->Defaults = TRUE; + GetRegistryValues(gpStateInfo); + + PWSTR pszAllocatedFileName; + HRESULT hr = SHStrDup(pszLinkFileName, &pszAllocatedFileName); + if (SUCCEEDED(hr)) + { + hr = StringCchCopyW(pszAllocatedFileName, MAX_PATH, pszLinkFileName); + if (SUCCEEDED(hr)) + { + // gpStateInfo now owns lifetime of the allocated filename + gpStateInfo->LinkTitle = pszAllocatedFileName; + pszAllocatedFileName = nullptr; + + // Not all console shortcuts have console-specific properties. We just take the registry defaults in + // those cases. + BOOL readSettings = FALSE; + NTSTATUS s = ShortcutSerialization::s_GetLinkValues(gpStateInfo, &readSettings, nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr); + hr = HRESULT_FROM_NT(s); + } + else + { + CoTaskMemFree(pszAllocatedFileName); + } + } + + if (SUCCEEDED(hr)) + { + InitializeFonts(); + hr = FindFontAndUpdateState(); + } + + return hr; + } + + /////////////////////////////////////////////////////////////////////////// + // CODE FROM THE SHELL DEPOT'S `idllib.h` + // get a link target item without resolving it. + HRESULT GetTargetIdList(_In_ IShellItem *psiLink, _COM_Outptr_ PIDLIST_ABSOLUTE *ppidl) + { + *ppidl = nullptr; + + IShellLink *psl; + HRESULT hr = psiLink->BindToHandler(NULL, BHID_SFUIObject, IID_PPV_ARGS(&psl)); + if (SUCCEEDED(hr)) + { + hr = psl->GetIDList(ppidl); + if (SUCCEEDED(hr) && (*ppidl == nullptr)) + { + hr = E_FAIL; + } + psl->Release(); + } + return hr; + } + HRESULT GetTargetItem(_In_ IShellItem *psiLink, _In_ REFIID riid, _COM_Outptr_ void **ppv) + { + *ppv = nullptr; + + PIDLIST_ABSOLUTE pidl; + HRESULT hr = GetTargetIdList(psiLink, &pidl); + if (SUCCEEDED(hr)) + { + hr = SHCreateItemFromIDList(pidl, riid, ppv); + ILFree(pidl); + } + return hr; + } + /////////////////////////////////////////////////////////////////////////// + + HRESULT _GetShellItemLinkTargetExpanded(_In_ IShellItem *pShellItem, _Out_writes_(cchFilePathExtended) PWSTR pszFilePathExtended, const size_t cchFilePathExtended) + { + ComPtr shellItemLinkTarget; + HRESULT hr = GetTargetItem(pShellItem, IID_PPV_ARGS(&shellItemLinkTarget)); + if (SUCCEEDED(hr)) + { + wil::unique_cotaskmem_string linkTargetPath; + hr = shellItemLinkTarget->GetDisplayName(SIGDN_FILESYSPATH, &linkTargetPath); + if (SUCCEEDED(hr)) + { + hr = StringCchCopy(pszFilePathExtended, cchFilePathExtended, linkTargetPath.get()); + } + } + + return hr; + } + + + HRESULT _ShouldAddPropertySheet(_In_ IDataObject *pdtobj, _Out_writes_(cchLinkFileName) PWSTR pszLinkFileName, const size_t cchLinkFileName) + { + ComPtr shellItemArray; + HRESULT hr = SHCreateShellItemArrayFromDataObject(pdtobj, IID_PPV_ARGS(&shellItemArray)); + if (SUCCEEDED(hr)) + { + DWORD dwItemCount; + hr = shellItemArray->GetCount(&dwItemCount); + if (SUCCEEDED(hr)) + { + // only consider being available for selections of a single file + hr = dwItemCount == 1 ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + ComPtr shellItem; + hr = shellItemArray->GetItemAt(0, &shellItem); + if (SUCCEEDED(hr)) + { + // First expensive portion of this method -- reads .lnk file + WCHAR szFileExpanded[MAX_PATH]; + hr = _GetShellItemLinkTargetExpanded(shellItem.Get(), szFileExpanded, ARRAYSIZE(szFileExpanded)); + if (SUCCEEDED(hr)) + { + // Second expensive portion of this method -- cracks the PE header of the .lnk file target + // if it's an executable + SHFILEINFO sfi = {0}; + DWORD_PTR dwFileType = SHGetFileInfo(szFileExpanded, + 0 /*dwFileAttributes*/, + &sfi, + sizeof(sfi), + SHGFI_EXETYPE); + if (HIWORD(dwFileType) == 0 && + LOWORD(dwFileType) == PEMAGIC) + { + // link target is a console application -- we should show our UI + hr = S_OK; + } + else + { + // link target is not a console application -- we should not show our UI + hr = E_FAIL; + } + } + } + + if (hr == S_OK) + { + // We're going to show the UI, write out the link filename while we're here. This is needed + // so we know where changes should be written. + wil::unique_cotaskmem_string linkDisplayName; + hr = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &linkDisplayName); + if (SUCCEEDED(hr)) + { + hr = StringCchCopy(pszLinkFileName, cchLinkFileName, linkDisplayName.get()); + } + } + } + } + } + + return hr; + } +}; +CoCreatableClass(ConsolePropertySheetHandler); diff --git a/src/propsheet/TerminalPage.cpp b/src/propsheet/TerminalPage.cpp new file mode 100644 index 000000000..20f0db040 --- /dev/null +++ b/src/propsheet/TerminalPage.cpp @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "TerminalPage.h" +#include "ColorControl.h" + +// From conattrs.h +const COLORREF INVALID_COLOR = 0xffffffff; + +const int COLOR_MAX = 255; + +void _UseForeground(const HWND hDlg, const bool useFg) noexcept +{ + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_REDSCROLL), useFg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_GREENSCROLL), useFg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_BLUESCROLL), useFg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_RED), useFg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_GREEN), useFg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_FG_BLUE), useFg); + + if (useFg) + { + const auto r = GetDlgItemInt(hDlg, IDD_TERMINAL_FG_RED, nullptr, FALSE); + const auto g = GetDlgItemInt(hDlg, IDD_TERMINAL_FG_GREEN, nullptr, FALSE); + const auto b = GetDlgItemInt(hDlg, IDD_TERMINAL_FG_BLUE, nullptr, FALSE); + gpStateInfo->DefaultForeground = RGB(r, g, b); + } + else + { + gpStateInfo->DefaultForeground = INVALID_COLOR; + } +} + +void _UseBackground(const HWND hDlg, const bool useBg) noexcept +{ + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_REDSCROLL), useBg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_GREENSCROLL), useBg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_BLUESCROLL), useBg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_RED), useBg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_GREEN), useBg); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_BG_BLUE), useBg); + + if (useBg) + { + auto r = GetDlgItemInt(hDlg, IDD_TERMINAL_BG_RED, nullptr, FALSE); + auto g = GetDlgItemInt(hDlg, IDD_TERMINAL_BG_GREEN, nullptr, FALSE); + auto b = GetDlgItemInt(hDlg, IDD_TERMINAL_BG_BLUE, nullptr, FALSE); + gpStateInfo->DefaultBackground = RGB(r, g, b); + } + else + { + gpStateInfo->DefaultBackground = INVALID_COLOR; + } +} + +void _UseCursorColor(const HWND hDlg, const bool useColor) noexcept +{ + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_REDSCROLL), useColor); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_GREENSCROLL), useColor); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_BLUESCROLL), useColor); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_RED), useColor); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_GREEN), useColor); + EnableWindow(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_BLUE), useColor); + + if (useColor) + { + const auto r = GetDlgItemInt(hDlg, IDD_TERMINAL_CURSOR_RED, nullptr, FALSE); + const auto g = GetDlgItemInt(hDlg, IDD_TERMINAL_CURSOR_GREEN, nullptr, FALSE); + const auto b = GetDlgItemInt(hDlg, IDD_TERMINAL_CURSOR_BLUE, nullptr, FALSE); + gpStateInfo->CursorColor = RGB(r, g, b); + } + else + { + gpStateInfo->CursorColor = INVALID_COLOR; + } +} + +void _UpdateTextAndScroll(const HWND hDlg, + const SHORT value, + const WORD textItem, + const WORD scrollItem) noexcept +{ + UpdateItem(hDlg, textItem, value); + SendDlgItemMessage(hDlg, scrollItem, UDM_SETPOS, 0, + MAKELONG(value, 0)); +} + +bool InitTerminalDialog(const HWND hDlg) noexcept +{ + // Initialize the global handle to this dialog + g_hTerminalDlg = hDlg; + // Group radios + CheckRadioButton(hDlg,IDD_TERMINAL_INVERSE_CURSOR,IDD_TERMINAL_CURSOR_USECOLOR, IDD_TERMINAL_INVERSE_CURSOR); + + // initialize size of edit controls + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_RED, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_GREEN, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_BLUE, EM_LIMITTEXT, 3, 0); + + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_RED, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_GREEN, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_BLUE, EM_LIMITTEXT, 3, 0); + + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_RED, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_GREEN, EM_LIMITTEXT, 3, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_BLUE, EM_LIMITTEXT, 3, 0); + + // Cap the color inputs to 255 + const auto colorRange = MAKELONG(COLOR_MAX, 0); + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_REDSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_GREENSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_FG_BLUESCROLL, UDM_SETRANGE, 0, colorRange); + + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_REDSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_GREENSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_BG_BLUESCROLL, UDM_SETRANGE, 0, colorRange); + + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_REDSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_GREENSCROLL, UDM_SETRANGE, 0, colorRange); + SendDlgItemMessage(hDlg, IDD_TERMINAL_CURSOR_BLUESCROLL, UDM_SETRANGE, 0, colorRange); + + const bool initialTerminalFG = gpStateInfo->DefaultForeground != INVALID_COLOR; + const bool initialTerminalBG = gpStateInfo->DefaultBackground != INVALID_COLOR; + const bool initialCursorLegacy = gpStateInfo->CursorColor == INVALID_COLOR; + if (initialTerminalFG) + { + g_fakeForegroundColor = gpStateInfo->DefaultForeground; + } + if (initialTerminalBG) + { + g_fakeBackgroundColor = gpStateInfo->DefaultBackground; + } + if (!initialCursorLegacy) + { + g_fakeCursorColor = gpStateInfo->CursorColor; + } + CheckDlgButton(hDlg, IDD_USE_TERMINAL_FG, initialTerminalFG); + CheckDlgButton(hDlg, IDD_USE_TERMINAL_BG, initialTerminalBG); + CheckRadioButton(hDlg, IDD_TERMINAL_INVERSE_CURSOR, IDD_TERMINAL_CURSOR_USECOLOR, + initialCursorLegacy ? IDD_TERMINAL_INVERSE_CURSOR : IDD_TERMINAL_CURSOR_USECOLOR); + + // Set the initial values in the edit boxes and scroll controls + const auto fgRed = GetRValue(g_fakeForegroundColor); + const auto fgGreen = GetGValue(g_fakeForegroundColor); + const auto fgBlue = GetBValue(g_fakeForegroundColor); + const auto bgRed = GetRValue(g_fakeBackgroundColor); + const auto bgGreen = GetGValue(g_fakeBackgroundColor); + const auto bgBlue = GetBValue(g_fakeBackgroundColor); + const auto cursorRed = GetRValue(g_fakeCursorColor); + const auto cursorGreen = GetGValue(g_fakeCursorColor); + const auto cursorBlue = GetBValue(g_fakeCursorColor); + + _UpdateTextAndScroll(hDlg, fgRed, IDD_TERMINAL_FG_RED, IDD_TERMINAL_FG_REDSCROLL); + _UpdateTextAndScroll(hDlg, fgGreen, IDD_TERMINAL_FG_GREEN, IDD_TERMINAL_FG_GREENSCROLL); + _UpdateTextAndScroll(hDlg, fgBlue, IDD_TERMINAL_FG_BLUE, IDD_TERMINAL_FG_BLUESCROLL); + + _UpdateTextAndScroll(hDlg, bgRed, IDD_TERMINAL_BG_RED, IDD_TERMINAL_BG_REDSCROLL); + _UpdateTextAndScroll(hDlg, bgGreen, IDD_TERMINAL_BG_GREEN, IDD_TERMINAL_BG_GREENSCROLL); + _UpdateTextAndScroll(hDlg, bgBlue, IDD_TERMINAL_BG_BLUE, IDD_TERMINAL_BG_BLUESCROLL); + + _UpdateTextAndScroll(hDlg, cursorRed, IDD_TERMINAL_CURSOR_RED, IDD_TERMINAL_CURSOR_REDSCROLL); + _UpdateTextAndScroll(hDlg, cursorGreen, IDD_TERMINAL_CURSOR_GREEN, IDD_TERMINAL_CURSOR_GREENSCROLL); + _UpdateTextAndScroll(hDlg, cursorBlue, IDD_TERMINAL_CURSOR_BLUE, IDD_TERMINAL_CURSOR_BLUESCROLL); + + _UseForeground(hDlg, initialTerminalFG); + _UseBackground(hDlg, initialTerminalBG); + _UseCursorColor(hDlg, !initialCursorLegacy); + + InvalidateRect(GetDlgItem(hDlg, IDD_TERMINAL_FGCOLOR), NULL, FALSE); + InvalidateRect(GetDlgItem(hDlg, IDD_TERMINAL_BGCOLOR), NULL, FALSE); + InvalidateRect(GetDlgItem(hDlg, IDD_TERMINAL_CURSOR_COLOR), NULL, FALSE); + + CheckRadioButton(hDlg, + IDD_TERMINAL_LEGACY_CURSOR, IDD_TERMINAL_SOLIDBOX, + IDD_TERMINAL_LEGACY_CURSOR+gpStateInfo->CursorType); + + + CheckDlgButton(hDlg, IDD_DISABLE_SCROLLFORWARD, gpStateInfo->TerminalScrolling); + + return true; +} + +void _ChangeColorControl(const HWND hDlg, + const WORD item, + const WORD redControl, + const WORD greenControl, + const WORD blueControl, + const WORD colorControl, + DWORD& setting) noexcept +{ + BOOL bOK = FALSE; + int newValue = GetDlgItemInt(hDlg, item, &bOK, TRUE); + int r = GetRValue(setting); + int g = GetGValue(setting); + int b = GetBValue(setting); + + if (bOK) + { + if (newValue > COLOR_MAX) + { + UpdateItem(hDlg, item, COLOR_MAX); + newValue = COLOR_MAX; + } + if (item == redControl) + { + r = newValue; + } + else if (item == greenControl) + { + g = newValue; + } + else if (item == blueControl) + { + b = newValue; + } + + setting = RGB(r,g,b); + } + + InvalidateRect(GetDlgItem(hDlg, colorControl), NULL, FALSE); +} + +void _ChangeForegroundRGB(const HWND hDlg, const WORD item) noexcept +{ + _ChangeColorControl(hDlg, + item, + IDD_TERMINAL_FG_RED, + IDD_TERMINAL_FG_GREEN, + IDD_TERMINAL_FG_BLUE, + IDD_TERMINAL_FGCOLOR, + gpStateInfo->DefaultForeground); + g_fakeForegroundColor = gpStateInfo->DefaultForeground; +} + +void _ChangeBackgroundRGB(const HWND hDlg, const WORD item) noexcept +{ + _ChangeColorControl(hDlg, + item, + IDD_TERMINAL_BG_RED, + IDD_TERMINAL_BG_GREEN, + IDD_TERMINAL_BG_BLUE, + IDD_TERMINAL_BGCOLOR, + gpStateInfo->DefaultBackground); + g_fakeBackgroundColor = gpStateInfo->DefaultBackground; +} + +void _ChangeCursorRGB(const HWND hDlg, const WORD item) noexcept +{ + _ChangeColorControl(hDlg, + item, + IDD_TERMINAL_CURSOR_RED, + IDD_TERMINAL_CURSOR_GREEN, + IDD_TERMINAL_CURSOR_BLUE, + IDD_TERMINAL_CURSOR_COLOR, + gpStateInfo->CursorColor); + g_fakeCursorColor = gpStateInfo->CursorColor; +} + +bool _CommandColorInput(const HWND hDlg, + const WORD item, + const WORD command, + const std::function changeFunction) noexcept +{ + bool handled = false; + + switch (command) { + case EN_UPDATE: + case EN_CHANGE: + changeFunction(hDlg, item); + handled = true; + UpdateApplyButton(hDlg); + break; + } + return handled; +} + +bool TerminalDlgCommand(const HWND hDlg, const WORD item, const WORD command) noexcept +{ + bool handled = false; + switch (item) { + case IDD_TERMINAL_CURSOR_USECOLOR: + case IDD_TERMINAL_INVERSE_CURSOR: + _UseCursorColor(hDlg, IsDlgButtonChecked(hDlg, IDD_TERMINAL_CURSOR_USECOLOR)); + handled = true; + UpdateApplyButton(hDlg); + break; + case IDD_USE_TERMINAL_FG: + _UseForeground(hDlg, IsDlgButtonChecked(hDlg, IDD_USE_TERMINAL_FG)); + handled = true; + UpdateApplyButton(hDlg); + break; + case IDD_USE_TERMINAL_BG: + _UseBackground(hDlg, IsDlgButtonChecked(hDlg, IDD_USE_TERMINAL_BG)); + handled = true; + UpdateApplyButton(hDlg); + break; + + case IDD_TERMINAL_FG_RED: + case IDD_TERMINAL_FG_GREEN: + case IDD_TERMINAL_FG_BLUE: + handled = _CommandColorInput(hDlg, item, command, _ChangeForegroundRGB); + break; + + case IDD_TERMINAL_BG_RED: + case IDD_TERMINAL_BG_GREEN: + case IDD_TERMINAL_BG_BLUE: + handled = _CommandColorInput(hDlg, item, command, _ChangeBackgroundRGB); + break; + + case IDD_TERMINAL_CURSOR_RED: + case IDD_TERMINAL_CURSOR_GREEN: + case IDD_TERMINAL_CURSOR_BLUE: + handled = _CommandColorInput(hDlg, item, command, _ChangeCursorRGB); + break; + + case IDD_TERMINAL_LEGACY_CURSOR: + case IDD_TERMINAL_VERTBAR: + case IDD_TERMINAL_UNDERSCORE: + case IDD_TERMINAL_EMPTYBOX: + case IDD_TERMINAL_SOLIDBOX: + gpStateInfo->CursorType = item - IDD_TERMINAL_LEGACY_CURSOR; + UpdateApplyButton(hDlg); + handled = true; + break; + case IDD_DISABLE_SCROLLFORWARD: + gpStateInfo->TerminalScrolling = IsDlgButtonChecked(hDlg, IDD_DISABLE_SCROLLFORWARD); + UpdateApplyButton(hDlg); + handled = true; + break; + } + + return handled; +} + +INT_PTR WINAPI TerminalDlgProc(const HWND hDlg, const UINT wMsg, const WPARAM wParam, const LPARAM lParam) +{ + static bool fHaveInitialized = false; + + switch (wMsg) { + case WM_INITDIALOG: + fHaveInitialized = true; + return InitTerminalDialog(hDlg); + case WM_COMMAND: + if (!fHaveInitialized) + { + return false; + } + return TerminalDlgCommand(hDlg, LOWORD(wParam), HIWORD(wParam)); + case WM_NOTIFY: + { + if (lParam && (wParam == IDD_HELP_TERMINAL)) + { + // handle hyperlink click or keyboard activation + switch(((LPNMHDR)lParam)->code) + { + case NM_CLICK: + case NM_RETURN: + { + PNMLINK pnmLink = (PNMLINK)lParam; + if (0 == pnmLink->item.iLink) + { + ShellExecute(NULL, + L"open", + pnmLink->item.szUrl, + NULL, + NULL, + SW_SHOW); + } + + break; + } + } + } + else + { + const PSHNOTIFY * const pshn = (LPPSHNOTIFY)lParam; + switch (pshn->hdr.code) { + case PSN_APPLY: + EndDlgPage(hDlg, !pshn->lParam); + return TRUE; + case PSN_KILLACTIVE: + { + // Fake the dialog proc into thinking the edit control just + // lost focus so it'll update properly + int item = GetDlgCtrlID(GetFocus()); + if (item) + { + SendMessage(hDlg, WM_COMMAND, MAKELONG(item, EN_KILLFOCUS), 0); + } + return TRUE; + } + } + + } + } + case WM_VSCROLL: + // Fake the dialog proc into thinking the edit control just + // lost focus so it'll update properly + SendMessage(hDlg, WM_COMMAND, MAKELONG((GetDlgCtrlID((HWND)lParam) - 1), EN_KILLFOCUS), 0); + return TRUE; + + case WM_DESTROY: + // MSFT:20740368 + // When the propsheet is opened straight from explorer, NOT from + // conhost itself, then explorer will load console.dll once, + // and re-use it for subsequent launches. This means that on + // the first launch of the propsheet, our fHaveInitialized will + // be false until we actually do the init work, but on + // subsequent launches, fHaveInitialized will be re-used, and + // found to be true, and we'll zero out the values of the + // colors. This is because the message loop decides to update + // the values of the textboxes before we get a chance to put + // the current values into them. When the textboxes update, + // they'll overwrite the current color components with whatever + // they currently have, which is 0. + // To avoid this madness, make sure to reset our initialization + // state when the dialog is closed. + fHaveInitialized = false; + break; + } + + return false; +} diff --git a/src/propsheet/TerminalPage.h b/src/propsheet/TerminalPage.h new file mode 100644 index 000000000..634558c8a --- /dev/null +++ b/src/propsheet/TerminalPage.h @@ -0,0 +1,16 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TerminalPage.h + +Abstract: +- This module contains the definitions for terminal dialog. + +Author(s): + Mike Griese (migrie) July-2018 +--*/ + +#pragma once +INT_PTR WINAPI TerminalDlgProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/propsheet/console.Resources.man b/src/propsheet/console.Resources.man new file mode 100644 index 000000000..2536f7372 --- /dev/null +++ b/src/propsheet/console.Resources.man @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/src/propsheet/console.cpp b/src/propsheet/console.cpp new file mode 100644 index 000000000..f193e65b2 --- /dev/null +++ b/src/propsheet/console.cpp @@ -0,0 +1,717 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + menu.c + +Abstract: + This file implements the system menu management. + +Author: + Therese Stowell (thereses) Jan-24-1992 (swiped from Win3.1) + +Revision History: + Mike Griese (migrie) Oct-2016 - Move to OpenConsole project + +--*/ + +#include "precomp.h" + +#pragma hdrstop + +UINT gnCurrentPage; + +#define SYSTEM_ROOT (L"%SystemRoot%") +#define SYSTEM_ROOT_LENGTH (sizeof(SYSTEM_ROOT) - sizeof(WCHAR)) + +void RecreateFontHandles(const HWND hWnd); + +void UpdateItem(HWND hDlg, UINT item, UINT nNum) +{ + SetDlgItemInt(hDlg, item, nNum, TRUE); + SendDlgItemMessage(hDlg, item, EM_SETSEL, 0, -1); +} + +// Routine Description: +// Sends an EM_UNDO message. Typically used after some user data is determined to be invalid. +void Undo(HWND hControlWindow) +{ + if (!InEM_UNDO) { + InEM_UNDO = TRUE; + SendMessage(hControlWindow, EM_UNDO, 0, 0); + InEM_UNDO = FALSE; + } +} + +// Routine Description: +// - Validates a string in the TextItem with id=Item represents a number +BOOL CheckNum(HWND hDlg, UINT Item) +{ + int i; + TCHAR szNum[5]; + BOOL fSigned; + + // The window position corrdinates can be signed, nothing else. + if (Item == IDD_WINDOW_POSX || Item == IDD_WINDOW_POSY) { + fSigned = TRUE; + } else { + fSigned = FALSE; + } + + GetDlgItemText(hDlg, Item, szNum, ARRAYSIZE(szNum)); + for (i = 0; szNum[i]; i++) { + if (!iswdigit(szNum[i]) && (!fSigned || i > 0 || szNum[i] != TEXT('-'))) { + return FALSE; + } + } + + return TRUE; +} + +void SaveConsoleSettingsIfNeeded(const HWND hwnd) +{ + if (gpStateInfo->UpdateValues) + { + // If we're looking at the default font, clear the values before we save them + if ((gpStateInfo->FontFamily == DefaultFontFamily) && + (gpStateInfo->FontSize.X == DefaultFontSize.X) && + (gpStateInfo->FontSize.Y == DefaultFontSize.Y) && + (gpStateInfo->FontWeight == FW_NORMAL) && + (wcscmp(gpStateInfo->FaceName, DefaultFaceName) == 0)) + { + + gpStateInfo->FontFamily = 0; + gpStateInfo->FontSize.X = 0; + gpStateInfo->FontSize.Y = 0; + gpStateInfo->FontWeight = 0; + gpStateInfo->FaceName[0] = TEXT('\0'); + } + + if (gpStateInfo->LinkTitle != NULL) + { + SetGlobalRegistryValues(); + if (!NT_SUCCESS(ShortcutSerialization::s_SetLinkValues(gpStateInfo, g_fEastAsianSystem, g_fForceV2))) + { + WCHAR szMessage[MAX_PATH + 100]; + WCHAR awchBuffer[MAX_PATH] = {0}; + STARTUPINFOW si; + + // An error occured try to save the link file, display a message box to that effect... + GetStartupInfoW(&si); + LoadStringW(ghInstance, IDS_LINKERROR, awchBuffer, ARRAYSIZE(awchBuffer)); + StringCchPrintf(szMessage, + ARRAYSIZE(szMessage), + awchBuffer, + gpStateInfo->LinkTitle); + LoadStringW(ghInstance, IDS_LINKERRCAP, awchBuffer, ARRAYSIZE(awchBuffer)); + + MessageBoxW(hwnd, + szMessage, + awchBuffer, + MB_APPLMODAL | MB_OK | MB_ICONSTOP | MB_SETFOREGROUND); + } + else + { + // we're up to date, so mark ourselves as such (needed for "Apply" case) + gpStateInfo->UpdateValues = FALSE; + } + } + else + { + SetRegistryValues(gpStateInfo, gnCurrentPage); + + // we're up to date, so mark ourselves as such (needed for "Apply" case) + gpStateInfo->UpdateValues = FALSE; + } + } +} + +void EndDlgPage(const HWND hDlg, const BOOL fSaveNow) +{ + HWND hParent; + HWND hTabCtrl; + + /* + * If we've already made a decision, we're done + */ + if (gpStateInfo->UpdateValues) { + SetDlgMsgResult(hDlg, PSN_APPLY, PSNRET_NOERROR); + return; + } + + /* + * Get the current page number + */ + hParent = GetParent(hDlg); + hTabCtrl = PropSheet_GetTabControl(hParent); + gnCurrentPage = TabCtrl_GetCurSel(hTabCtrl); + + gpStateInfo->UpdateValues = TRUE; + + SetDlgMsgResult(hDlg, PSN_APPLY, PSNRET_NOERROR); + + if (fSaveNow) { + // needed for "Apply" scenario + SaveConsoleSettingsIfNeeded(hDlg); + } + + PropSheet_UnChanged(hDlg, 0); +} + +#define TOOLTIP_MAXLENGTH (256) +void CreateAndAssociateToolTipToControl(const UINT dlgItem, const HWND hDlg, const UINT idsToolTip) +{ + HWND hwndTooltip = CreateWindowEx(0 /*dwExtStyle*/, + TOOLTIPS_CLASS, + NULL /*lpWindowName*/, + TTS_ALWAYSTIP, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + hDlg, + NULL /*hMenu*/, + ghInstance, + NULL /*lpParam*/); + + if (hwndTooltip) + { + WCHAR szTooltip[TOOLTIP_MAXLENGTH] = {0}; + if (LoadString(ghInstance, idsToolTip, szTooltip, ARRAYSIZE(szTooltip)) > 0) + { + TOOLINFO toolInfo = {0}; + toolInfo.cbSize = sizeof(toolInfo); + toolInfo.hwnd = hDlg; + toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + toolInfo.uId = (UINT_PTR)GetDlgItem(hDlg, dlgItem); + toolInfo.lpszText = szTooltip; + SendMessage(hwndTooltip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo); + } + } +} + +BOOL UpdateStateInfo(HWND hDlg, UINT Item, int Value) +{ + switch (Item) { + case IDD_SCRBUF_WIDTH: + gpStateInfo->ScreenBufferSize.X = (SHORT)Value; + + // If we're in V2 mode with wrap text on OR if the window is larger than the buffer, adjust the window to match. + if ((g_fForceV2 && gpStateInfo->fWrapText) || gpStateInfo->WindowSize.X > Value) { + gpStateInfo->WindowSize.X = (SHORT)Value; + UpdateItem(hDlg, IDD_WINDOW_WIDTH, Value); + } + break; + case IDD_SCRBUF_HEIGHT: + gpStateInfo->ScreenBufferSize.Y = (SHORT)Value; + if (gpStateInfo->WindowSize.Y > Value) { + gpStateInfo->WindowSize.Y = (SHORT)Value; + UpdateItem(hDlg, IDD_WINDOW_HEIGHT, Value); + } + break; + case IDD_WINDOW_WIDTH: + gpStateInfo->WindowSize.X = (SHORT)Value; + + // If we're in V2 mode with wrap text on OR if the buffer is smaller than the window, adjust the buffer to match. + if ((g_fForceV2 && gpStateInfo->fWrapText) || gpStateInfo->ScreenBufferSize.X < Value) { + gpStateInfo->ScreenBufferSize.X = (SHORT)Value; + UpdateItem(hDlg, IDD_SCRBUF_WIDTH, Value); + } + break; + case IDD_WINDOW_HEIGHT: + gpStateInfo->WindowSize.Y = (SHORT)Value; + if (gpStateInfo->ScreenBufferSize.Y < Value) { + gpStateInfo->ScreenBufferSize.Y = (SHORT)Value; + UpdateItem(hDlg, IDD_SCRBUF_HEIGHT, Value); + } + break; + case IDD_WINDOW_POSX: + if (Value < 0) + { + gpStateInfo->WindowPosX = max(SHORT_MIN, Value); + } + else + { + gpStateInfo->WindowPosX = min(SHORT_MAX, Value); + } + break; + case IDD_WINDOW_POSY: + if (Value < 0) + { + gpStateInfo->WindowPosY = max(SHORT_MIN, Value); + } + else + { + gpStateInfo->WindowPosY = min(SHORT_MAX, Value); + } + break; + case IDD_AUTO_POSITION: + gpStateInfo->AutoPosition = Value; + break; + case IDD_COLOR_SCREEN_TEXT: + gpStateInfo->ScreenAttributes = + (gpStateInfo->ScreenAttributes & 0xF0) | + (Value & 0x0F); + break; + case IDD_COLOR_SCREEN_BKGND: + gpStateInfo->ScreenAttributes = + (gpStateInfo->ScreenAttributes & 0x0F) | + (WORD)(Value << 4); + break; + case IDD_COLOR_POPUP_TEXT: + gpStateInfo->PopupAttributes = + (gpStateInfo->PopupAttributes & 0xF0) | + (Value & 0x0F); + break; + case IDD_COLOR_POPUP_BKGND: + gpStateInfo->PopupAttributes = + (gpStateInfo->PopupAttributes & 0x0F) | + (WORD)(Value << 4); + break; + case IDD_COLOR_1: + case IDD_COLOR_2: + case IDD_COLOR_3: + case IDD_COLOR_4: + case IDD_COLOR_5: + case IDD_COLOR_6: + case IDD_COLOR_7: + case IDD_COLOR_8: + case IDD_COLOR_9: + case IDD_COLOR_10: + case IDD_COLOR_11: + case IDD_COLOR_12: + case IDD_COLOR_13: + case IDD_COLOR_14: + case IDD_COLOR_15: + case IDD_COLOR_16: + gpStateInfo->ColorTable[Item - IDD_COLOR_1] = Value; + break; + case IDD_LANGUAGELIST: + /* + * Value is a code page + */ + gpStateInfo->CodePage = Value; + break; + case IDD_QUICKEDIT: + gpStateInfo->QuickEdit = Value; + break; + case IDD_INSERT: + gpStateInfo->InsertMode = Value; + break; + case IDD_HISTORY_SIZE: + gpStateInfo->HistoryBufferSize = max(Value, 1); + break; + case IDD_HISTORY_NUM: + gpStateInfo->NumberOfHistoryBuffers = max(Value, 1); + break; + case IDD_HISTORY_NODUP: + gpStateInfo->HistoryNoDup = Value; + break; + case IDD_CURSOR_SMALL: + gpStateInfo->CursorSize = 25; + // Set the cursor to legacy style + gpStateInfo->CursorType = 0; + // Check the legacy radio button on the terminal page + if (g_hTerminalDlg != INVALID_HANDLE_VALUE) + { + CheckRadioButton(g_hTerminalDlg, + IDD_TERMINAL_LEGACY_CURSOR, IDD_TERMINAL_SOLIDBOX, + IDD_TERMINAL_LEGACY_CURSOR); + } + + break; + case IDD_CURSOR_MEDIUM: + gpStateInfo->CursorSize = 50; + // Set the cursor to legacy style + gpStateInfo->CursorType = 0; + // Check the legacy radio button on the terminal page + if (g_hTerminalDlg != INVALID_HANDLE_VALUE) + { + CheckRadioButton(g_hTerminalDlg, + IDD_TERMINAL_LEGACY_CURSOR, IDD_TERMINAL_SOLIDBOX, + IDD_TERMINAL_LEGACY_CURSOR); + } + + break; + case IDD_CURSOR_LARGE: + gpStateInfo->CursorSize = 100; + // Set the cursor to legacy style + gpStateInfo->CursorType = 0; + // Check the legacy radio button on the terminal page + if (g_hTerminalDlg != INVALID_HANDLE_VALUE) + { + CheckRadioButton(g_hTerminalDlg, + IDD_TERMINAL_LEGACY_CURSOR, IDD_TERMINAL_SOLIDBOX, + IDD_TERMINAL_LEGACY_CURSOR); + } + + break; + default: + return FALSE; + } + + return TRUE; +} + +// Copied as a subset of open/src/host/srvinit.cpp's TranslateConsoleTitle +// Routine Description: +// - This routine translates path characters into '_' characters because the NT registry apis do not allow the creation of keys with +// names that contain path characters. It also converts absolute paths into %SystemRoot% relative ones. As an example, if both behaviors were +// specified it would convert a title like C:\WINNT\System32\cmd.exe to %SystemRoot%_System32_cmd.exe. +// Arguments: +// - ConsoleTitle - Pointer to string to translate. +// Return Value: +// - Pointer to translated title or nullptr. +// Note: +// - This routine allocates a buffer that must be freed. +PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle) +{ + bool fUnexpand = true; + bool fSubstitute = true; + + LPWSTR Tmp = nullptr; + + size_t cbConsoleTitle; + size_t cbSystemRoot; + + LPWSTR pwszSysRoot = new(std::nothrow) wchar_t[MAX_PATH]; + if (nullptr != pwszSysRoot) + { + if (0 != GetWindowsDirectoryW(pwszSysRoot, MAX_PATH)) + { + if (SUCCEEDED(StringCbLengthW(pwszConsoleTitle, STRSAFE_MAX_CCH, &cbConsoleTitle)) && + SUCCEEDED(StringCbLengthW(pwszSysRoot, MAX_PATH, &cbSystemRoot))) + { + int const cchSystemRoot = (int)(cbSystemRoot / sizeof(WCHAR)); + int const cchConsoleTitle = (int)(cbConsoleTitle / sizeof(WCHAR)); + cbConsoleTitle += sizeof(WCHAR); // account for nullptr terminator + + if (fUnexpand && + cchConsoleTitle >= cchSystemRoot && +#pragma prefast(suppress:26018, "We've guaranteed that cchSystemRoot is equal to or smaller than cchConsoleTitle in size.") + (CSTR_EQUAL == CompareStringOrdinal(pwszConsoleTitle, cchSystemRoot, pwszSysRoot, cchSystemRoot, TRUE))) + { + cbConsoleTitle -= cbSystemRoot; + pwszConsoleTitle += cchSystemRoot; + cbSystemRoot = SYSTEM_ROOT_LENGTH; + } + else + { + cbSystemRoot = 0; + } + + LPWSTR TranslatedConsoleTitle; + // This has to be a HeapAlloc, because it gets HeapFree'd later + Tmp = TranslatedConsoleTitle = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, (cchSystemRoot + cchConsoleTitle) * sizeof(WCHAR)); + + if (TranslatedConsoleTitle == nullptr) + { + return nullptr; + } + + memmove(TranslatedConsoleTitle, SYSTEM_ROOT, cbSystemRoot); + TranslatedConsoleTitle += (cbSystemRoot / sizeof(WCHAR)); // skip by characters -- not bytes + + for (UINT i = 0; i < cbConsoleTitle; i += sizeof(WCHAR)) + { +#pragma prefast(suppress:26018, "We are reading the null portion of the buffer on purpose and will escape on reaching it below.") + if (fSubstitute && *pwszConsoleTitle == '\\') + { +#pragma prefast(suppress:26019, "Console title must contain system root if this path was followed.") + *TranslatedConsoleTitle++ = (WCHAR)'_'; + } + else + { + *TranslatedConsoleTitle++ = *pwszConsoleTitle; + if (*pwszConsoleTitle == L'\0') + { + break; + } + } + + pwszConsoleTitle++; + } + } + } + delete[] pwszSysRoot; + } + + return Tmp; +} + +// For use by property sheets when added to file props dialog -- maintain refcount of each page and release things we've +// registered when we hit 0. Needed because the lifetime of the property sheets isn't tied to the lifetime of our +// IShellPropSheetExt object. +UINT CALLBACK PropSheetPageProc(_In_ HWND hWnd, _In_ UINT uMsg, _Inout_ LPPROPSHEETPAGE /*ppsp*/) +{ + static UINT cRefs = 0; + switch (uMsg) + { + case PSPCB_ADDREF: + { + cRefs++; + break; + } + + case PSPCB_RELEASE: + { + cRefs--; + if (cRefs == 0) + { + if (gpStateInfo->UpdateValues) + { + // only persist settings if they've changed + SaveConsoleSettingsIfNeeded(hWnd); + } + + UninitializeConsoleState(); + } + break; + } + } + + return 1; +} + +BOOL PopulatePropSheetPageArray(_Out_writes_(cPsps) PROPSHEETPAGE *pPsp, const size_t cPsps, const BOOL fRegisterCallbacks) +{ + BOOL fRet = (cPsps == NUMBER_OF_PAGES); + if (fRet) + { + // This has been validated above. OACR is being silly. Restate it so it can see the condition. + __analysis_assume(cPsps == NUMBER_OF_PAGES); + + PROPSHEETPAGE* const pOptionsPage = &(pPsp[OPTIONS_PAGE_INDEX]); + PROPSHEETPAGE* const pFontPage = &(pPsp[FONT_PAGE_INDEX]); + PROPSHEETPAGE* const pLayoutPage = &(pPsp[LAYOUT_PAGE_INDEX]); + PROPSHEETPAGE* const pColorsPage = &(pPsp[COLORS_PAGE_INDEX]); + PROPSHEETPAGE* const pTerminalPage = &(pPsp[TERMINAL_PAGE_INDEX]); + + pOptionsPage->dwSize = sizeof(PROPSHEETPAGE); + pOptionsPage->hInstance = ghInstance; + if (g_fIsComCtlV6Present) + { + pOptionsPage->pszTemplate = (gpStateInfo->Defaults) ? MAKEINTRESOURCE(DID_SETTINGS) : MAKEINTRESOURCE(DID_SETTINGS2); + } + else + { + pOptionsPage->pszTemplate = (gpStateInfo->Defaults) ? MAKEINTRESOURCE(DID_SETTINGS_COMCTL5) : MAKEINTRESOURCE(DID_SETTINGS2_COMCTL5); + } + pOptionsPage->pfnDlgProc = SettingsDlgProc; + pOptionsPage->lParam = OPTIONS_PAGE_INDEX; + pOptionsPage->dwFlags = PSP_DEFAULT; + + pFontPage->dwSize = sizeof(PROPSHEETPAGE); + pFontPage->hInstance = ghInstance; + pFontPage->pszTemplate = MAKEINTRESOURCE(DID_FONTDLG); + pFontPage->pfnDlgProc = (DLGPROC) FontDlgProc; + pFontPage->lParam = FONT_PAGE_INDEX; + pOptionsPage->dwFlags = PSP_DEFAULT; + + pLayoutPage->dwSize = sizeof(PROPSHEETPAGE); + pLayoutPage->hInstance = ghInstance; + pLayoutPage->pszTemplate = MAKEINTRESOURCE(DID_SCRBUFSIZE); + pLayoutPage->pfnDlgProc = ScreenSizeDlgProc; + pLayoutPage->lParam = LAYOUT_PAGE_INDEX; + pOptionsPage->dwFlags = PSP_DEFAULT; + + pColorsPage->dwSize = sizeof(PROPSHEETPAGE); + pColorsPage->hInstance = ghInstance; + pColorsPage->pszTemplate = MAKEINTRESOURCE(DID_COLOR); + pColorsPage->pfnDlgProc = ColorDlgProc; + pColorsPage->lParam = COLORS_PAGE_INDEX; + pOptionsPage->dwFlags = PSP_DEFAULT; + if (g_fForceV2) + { + pTerminalPage->dwSize = sizeof(PROPSHEETPAGE); + pTerminalPage->hInstance = ghInstance; + pTerminalPage->pszTemplate = MAKEINTRESOURCE(DID_TERMINAL); + pTerminalPage->pfnDlgProc = TerminalDlgProc; + pTerminalPage->lParam = TERMINAL_PAGE_INDEX; + pTerminalPage->dwFlags = PSP_DEFAULT; + } + + // Register callbacks if requested (used for file property sheet purposes) + if (fRegisterCallbacks) + { + for (UINT iPage = 0; iPage < cPsps; iPage++) + { + pPsp[iPage].pfnCallback = &PropSheetPageProc; + pPsp[iPage].dwFlags |= PSP_USECALLBACK; + } + } + + fRet = TRUE; + } + + return fRet; +} + +// Routine Description: +// - Creates the property sheet to change console settings. +INT_PTR ConsolePropertySheet(__in HWND hWnd, __in PCONSOLE_STATE_INFO pStateInfo) +{ + PROPSHEETPAGE psp[NUMBER_OF_PAGES]; + PROPSHEETHEADER psh; + INT_PTR Result = IDCANCEL; + WCHAR awchBuffer[MAX_PATH] = {0}; + + gpStateInfo = pStateInfo; + + // In v2 console, consider this an East Asian system if we're currently in a CJK charset. In v1, look at the system codepage. + if (gpStateInfo->fIsV2Console) + { + g_fEastAsianSystem = IS_ANY_DBCS_CHARSET(CodePageToCharSet(gpStateInfo->CodePage)); + } + else + { + g_fEastAsianSystem = IsEastAsianCP(GetOEMCP()); + } + + // + // Initialize the state information. + // + if (gpStateInfo->Defaults) { + InitRegistryValues(pStateInfo); + GetRegistryValues(pStateInfo); + } + + // + // Initialize the font cache and current font index + // + + InitializeFonts(); + g_currentFontIndex = FindCreateFont(gpStateInfo->FontFamily, + gpStateInfo->FaceName, + gpStateInfo->FontSize, + gpStateInfo->FontWeight, + gpStateInfo->CodePage); + + // since we just triggered font enumeration, recreate our font handles to adapt for DPI + RecreateFontHandles(hWnd); + + // + // Get the current page number + // + + gnCurrentPage = GetRegistryValues(NULL); + + // + // Initialize the property sheet structures + // + + RtlZeroMemory(psp, sizeof(psp)); + PopulatePropSheetPageArray(psp, ARRAYSIZE(psp), FALSE /*fRegisterCallbacks*/); + + psh.dwSize = sizeof(PROPSHEETHEADER); + psh.dwFlags = PSH_PROPTITLE | PSH_USEHICON | PSH_PROPSHEETPAGE | + PSH_NOAPPLYNOW | PSH_USECALLBACK | PSH_NOCONTEXTHELP; + if (gpStateInfo->Defaults) { + LoadString(ghInstance, IDS_TITLE, awchBuffer, ARRAYSIZE(awchBuffer)); + } else { + awchBuffer[0] = L'"'; + ExpandEnvironmentStrings(gpStateInfo->OriginalTitle, + &awchBuffer[1], + ARRAYSIZE(awchBuffer) - 2); + StringCchCat(awchBuffer, ARRAYSIZE(awchBuffer), TEXT("\"")); + gpStateInfo->OriginalTitle = TranslateConsoleTitle(gpStateInfo->OriginalTitle); + } + + psh.hwndParent = hWnd; + psh.hIcon = gpStateInfo->hIcon; + psh.hInstance = ghInstance; + psh.pszCaption = awchBuffer; + psh.nPages = g_fForceV2 ? NUMBER_OF_PAGES : V1_NUMBER_OF_PAGES; + psh.nStartPage = min(gnCurrentPage, ARRAYSIZE(psp)); + psh.ppsp = psp; + psh.pfnCallback = NULL; + + // + // Create the property sheet + // + + Result = PropertySheet(&psh); + + // + // Save our changes to the registry + // + const BOOL fUpdatedValues = gpStateInfo->UpdateValues; + SaveConsoleSettingsIfNeeded(hWnd); + gpStateInfo->UpdateValues = fUpdatedValues; + + if (!gpStateInfo->Defaults) { + if (gpStateInfo->OriginalTitle != NULL) { + HeapFree(GetProcessHeap(), 0, gpStateInfo->OriginalTitle); + } + } + + // + // Destroy the font cache. + // + DestroyFonts(); + + return Result; +} + +void RegisterClasses(HINSTANCE hModule) +{ + WNDCLASS wc; + wc.lpszClassName = TEXT("SimpleColor"); + wc.hInstance = hModule; + wc.lpfnWndProc = (WNDPROC) SimpleColorControlProc; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hIcon = NULL; + wc.lpszMenuName = NULL; + wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + RegisterClass(&wc); + + wc.lpszClassName = TEXT("ColorTableColor"); + wc.hInstance = hModule; + wc.lpfnWndProc = (WNDPROC) ColorTableControlProc; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hIcon = NULL; + wc.lpszMenuName = NULL; + wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.cbClsExtra = 0; + wc.cbWndExtra = 0; + RegisterClass(&wc); + + wc.lpszClassName = TEXT("WOAWinPreview"); + wc.lpfnWndProc = (WNDPROC) PreviewWndProc; + wc.hbrBackground = (HBRUSH) (COLOR_BACKGROUND + 1); + wc.style = 0; + RegisterClass(&wc); + + wc.lpszClassName = TEXT("WOAFontPreview"); + wc.lpfnWndProc = (WNDPROC) FontPreviewWndProc; + wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + wc.style = 0; + RegisterClass(&wc); +} + +void UnregisterClasses(HINSTANCE hModule) +{ + UnregisterClass(TEXT("cpColor"), hModule); + UnregisterClass(TEXT("WOAWinPreview"), hModule); + UnregisterClass(TEXT("WOAFontPreview"), hModule); +} + +HRESULT FindFontAndUpdateState() +{ + g_currentFontIndex = FindCreateFont(gpStateInfo->FontFamily, + gpStateInfo->FaceName, + gpStateInfo->FontSize, + gpStateInfo->FontWeight, + gpStateInfo->CodePage); + + gpStateInfo->FontFamily = FontInfo[g_currentFontIndex].Family; + gpStateInfo->FontSize = FontInfo[g_currentFontIndex].Size; + gpStateInfo->FontWeight = FontInfo[g_currentFontIndex].Weight; + return StringCchCopyW(gpStateInfo->FaceName, ARRAYSIZE(gpStateInfo->FaceName), FontInfo[g_currentFontIndex].FaceName); +} diff --git a/src/propsheet/console.def b/src/propsheet/console.def new file mode 100644 index 000000000..652778cb3 --- /dev/null +++ b/src/propsheet/console.def @@ -0,0 +1,6 @@ +LIBRARY Console + +EXPORTS + CPlApplet + DllGetClassObject private + DllCanUnloadNow private \ No newline at end of file diff --git a/src/propsheet/console.dll.mui.lci b/src/propsheet/console.dll.mui.lci new file mode 100644 index 000000000..f43fbf9d9 --- /dev/null +++ b/src/propsheet/console.dll.mui.lci @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/propsheet/console.h b/src/propsheet/console.h new file mode 100644 index 000000000..90108c079 --- /dev/null +++ b/src/propsheet/console.h @@ -0,0 +1,262 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + console.h + +Abstract: + + This module contains the definitions for the console applet + +Author: + + Jerry Shea (jerrysh) Feb-3-1992 + +Revision History: + Mike Griese, migrie, Oct 2016: + Moved to cpp and the OpenConsole project + +--*/ +#pragma once + +#include "font.h" +#include "OptionsPage.h" +#include "LayoutPage.h" +#include "ColorsPage.h" +#include "TerminalPage.h" +#include "ColorControl.h" + +// +// Icon ID. +// + +#define IDI_CONSOLE 1 + +// +// String table constants +// + +#define IDS_NAME 1 +#define IDS_INFO 2 +#define IDS_TITLE 3 +#define IDS_RASTERFONT 4 +#define IDS_FONTSIZE 5 +#define IDS_SELECTEDFONT 6 +#define IDS_SAVE 7 +#define IDS_LINKERRCAP 8 +#define IDS_LINKERROR 9 +#define IDS_FONTSTRING 10 +#define IDS_TOOLTIP_LINE_SELECTION 11 +#define IDS_TOOLTIP_FILTER_ON_PASTE 12 +#define IDS_TOOLTIP_LINE_WRAP 13 +#define IDS_TOOLTIP_CTRL_KEYS 14 +#define IDS_TOOLTIP_EDIT_KEYS 15 +// unused 16 +#define IDS_TOOLTIP_OPACITY 17 +#define IDS_TOOLTIP_INTERCEPT_COPY_PASTE 18 + +NTSTATUS +MakeAltRasterFont( + __in UINT CodePage, + __out COORD *AltFontSize, + __out BYTE *AltFontFamily, + __out ULONG *AltFontIndex, + __out_ecount(LF_FACESIZE) LPTSTR AltFaceName); + +NTSTATUS InitializeDbcsMisc(); + +BYTE +CodePageToCharSet( + UINT CodePage + ); + +BOOL +ShouldAllowAllMonoTTFonts(); + +LPTTFONTLIST +SearchTTFont( + __in_opt LPCTSTR ptszFace, + BOOL fCodePage, + UINT CodePage + ); + +BOOL +IsAvailableTTFont( + LPCTSTR ptszFace + ); + +BOOL +IsAvailableTTFontCP( + LPCWSTR pwszFace, + UINT CodePage + ); + +BOOL +IsDisableBoldTTFont( + LPCTSTR ptszFace + ); + +LPTSTR +GetAltFaceName( + LPCTSTR ptszFace + ); + +NTSTATUS DestroyDbcsMisc(); + +int +LanguageListCreate( + HWND hDlg, + UINT CodePage + ); + +int +LanguageDisplay( + HWND hDlg, + UINT CodePage + ) ; + +// +// registry.c +// +NTSTATUS +MyRegOpenKey( + __in_opt HANDLE hKey, + __in LPCWSTR lpSubKey, + __out PHANDLE phResult + ); + +NTSTATUS +MyRegEnumValue( + __in HANDLE hKey, + __in DWORD dwIndex, + __in DWORD dwValueLength, + __out_bcount(dwValueLength) LPWSTR lpValueName, + __in_range(4, 1024) DWORD dwDataLength, + __out_bcount(dwDataLength) LPBYTE lpData + ); + +// +// Function prototypes +// + +INT_PTR ConsolePropertySheet( + __in HWND hWnd, + __in PCONSOLE_STATE_INFO pStateInfo); + +VOID RegisterClasses( + HINSTANCE hModule); + +VOID UnregisterClasses( + HINSTANCE hModule); + +INT_PTR APIENTRY FontDlgProc( + HWND hDlg, + UINT wMsg, + WPARAM wParam, + LPARAM lParam); + +VOID InitRegistryValues( + __out PCONSOLE_STATE_INFO pStateInfo); + +DWORD GetRegistryValues( + __out_opt PCONSOLE_STATE_INFO StateInfo); + +VOID SetGlobalRegistryValues(); + +VOID SetRegistryValues( + PCONSOLE_STATE_INFO StateInfo, + DWORD dwPage); + +PCONSOLE_STATE_INFO InitStateValues( + HWND hwnd); + +LRESULT FontPreviewWndProc( + HWND hWnd, + UINT wMsg, + WPARAM wParam, + LPARAM lParam); + +LRESULT PreviewWndProc( + HWND hWnd, + UINT wMsg, + WPARAM wParam, + LPARAM lParam); + +VOID EndDlgPage( + const HWND hDlg, + const BOOL fSaveNow); + +BOOL UpdateStateInfo( + HWND hDlg, + UINT Item, + int Value); + +BOOL InitializeConsoleState(); +void UninitializeConsoleState(); +void UpdateApplyButton(const HWND hDlg); +HRESULT FindFontAndUpdateState(); + +BOOL PopulatePropSheetPageArray(_Out_writes_(cPsps) PROPSHEETPAGE *pPsp, const size_t cPsps, const BOOL fRegisterCallbacks); + +void CreateAndAssociateToolTipToControl(const UINT dlgItem, const HWND hDlg, const UINT idsToolTip); + +BOOL CheckNum(HWND hDlg, UINT Item); +void UpdateItem(HWND hDlg, UINT item, UINT nNum); +void Undo(HWND hControlWindow); + +// +// Macros +// +#define AttrToRGB(Attr) (gpStateInfo->ColorTable[(Attr) & 0x0F]) +#define ScreenTextColor(pStateInfo) \ + (AttrToRGB(LOBYTE(pStateInfo->ScreenAttributes) & 0x0F)) +#define ScreenBkColor(pStateInfo) \ + (AttrToRGB(LOBYTE(pStateInfo->ScreenAttributes >> 4))) +#define PopupTextColor(pStateInfo) \ + (AttrToRGB(LOBYTE(pStateInfo->PopupAttributes) & 0x0F)) +#define PopupBkColor(pStateInfo) \ + (AttrToRGB(LOBYTE(pStateInfo->PopupAttributes >> 4))) + +#if DBG + #define _DBGFONTS 0x00000001 + #define _DBGFONTS2 0x00000002 + #define _DBGCHARS 0x00000004 + #define _DBGOUTPUT 0x00000008 + #define _DBGALL 0xFFFFFFFF + extern ULONG gDebugFlag; + + #define DBGFONTS(_params_) + #define DBGFONTS2(_params_) + #define DBGCHARS(_params_) + #define DBGOUTPUT(_params_) +#else + #define DBGFONTS(_params_) + #define DBGFONTS2(_params_) + #define DBGCHARS(_params_) + #define DBGOUTPUT(_params_) +#endif + +// Macro definitions that handle codepages +// +#define CP_US (UINT)437 +#define CP_JPN (UINT)932 +#define CP_WANSUNG (UINT)949 +#define CP_TC (UINT)950 +#define CP_SC (UINT)936 + +#define IsBilingualCP(cp) ((cp)==CP_JPN || (cp)==CP_WANSUNG) +#define IsEastAsianCP(cp) ((cp)==CP_JPN || (cp)==CP_WANSUNG || (cp)==CP_TC || (cp)==CP_SC) + +const unsigned int TRANSPARENCY_RANGE_MIN = 0x4D; + +const unsigned int OPTIONS_PAGE_INDEX = 0; +const unsigned int FONT_PAGE_INDEX = 1; +const unsigned int LAYOUT_PAGE_INDEX = 2; +const unsigned int COLORS_PAGE_INDEX = 3; +const unsigned int TERMINAL_PAGE_INDEX = 4; +// number of property sheet pages +static const int V1_NUMBER_OF_PAGES = 4; +static const int NUMBER_OF_PAGES = 5; diff --git a/src/propsheet/console.man b/src/propsheet/console.man new file mode 100644 index 000000000..e7c4ed116 --- /dev/null +++ b/src/propsheet/console.man @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/propsheet/console.rc b/src/propsheet/console.rc new file mode 100644 index 000000000..8ab505a6e --- /dev/null +++ b/src/propsheet/console.rc @@ -0,0 +1,666 @@ +/****************************** Module Header ******************************\ +* Module Name: console.rc +* +* Copyright (c) 1985-95, Microsoft Corporation +* +* Constants +* +* History: +* 08-21-91 Created. +\***************************************************************************/ + + +#include + +#ifndef EXTERNAL_BUILD +#include +#endif +#include +#include "dialogs.h" +#include "console.h" + +IDI_CONSOLE ICON "..\\..\\res\\console.ico" + +#include "strid.rc" + +// +// Dialogs +// + + +// +// This is the template for the defaults settings dialog for use with ComCtlv6 +// + +DID_SETTINGS DIALOG 0, 0, 240, 240 +CAPTION " Options " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + GROUPBOX "Cursor Size", -1, 10, 11, 100, 56 + AUTORADIOBUTTON "&Small", IDD_CURSOR_SMALL, 14, 23, 84, 10, + WS_TABSTOP | WS_GROUP + AUTORADIOBUTTON "&Medium", IDD_CURSOR_MEDIUM, 14, 33, 84, 10, + AUTORADIOBUTTON "&Large", IDD_CURSOR_LARGE, 14, 43, 84, 10, + + GROUPBOX "Command History", -1, 115, 11, 120, 56, WS_GROUP + LTEXT "&Buffer Size:", -1, 119, 25, 78, 9 + EDITTEXT IDD_HISTORY_SIZE, 197, 23, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_SIZESCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Number of Buffers:", -1, 119, 40, 78, 9 + EDITTEXT IDD_HISTORY_NUM, 197, 38, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_NUMSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + AUTOCHECKBOX "&Discard Old Duplicates", IDD_HISTORY_NODUP, 119,55, 108, 9 + + GROUPBOX "Edit Options",-1,10,70,225,64 + CONTROL "&QuickEdit Mode", IDD_QUICKEDIT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 82, 75, 10 + CONTROL "&Insert Mode", IDD_INSERT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 92, 75, 10 + CONTROL "Enable Ctrl &key shortcuts", IDD_CTRL_KEYS_ENABLED, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 102, 200, 10 + CONTROL "&Filter clipboard contents on paste", IDD_FILTER_ON_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 112, 200, 10 + CONTROL "Use Ctrl+Shift+C/V as &Copy/Paste", IDD_INTERCEPT_COPY_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 122, 200, 10 + + GROUPBOX "Text Selection",-1,10,136,225,32 + CONTROL "&Enable line wrapping selection", IDD_LINE_SELECTION, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 146, 200, 10 + CONTROL "E&xtended text selection keys", IDD_EDIT_KEYS, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 156, 200, 10 + + GROUPBOX "Default code page", IDD_LANGUAGE_GROUPBOX, 10, 160, 225, 29, WS_GROUP + COMBOBOX IDD_LANGUAGELIST, 16, 171, 213, 78, + CBS_SORT | + CBS_DISABLENOSCROLL | + CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP | WS_GROUP + + CONTROL "&Use legacy console (requires relaunch, affects all consoles)", IDD_FORCEV2, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 10, 199, 200, 10 + + CONTROL "Learn more about legacy console mode", + IDD_HELP_LEGACY_LINK, "SysLink", WS_TABSTOP, 21, 211, 179, 10 + + CONTROL "Find out more about new console features", + IDD_HELP_SYSLINK, "SysLink", WS_TABSTOP, 10, 225, 200, 10 + + +END + +// +// This is the template for the specifics settings dialog for use with ComCtlv6 +// + +DID_SETTINGS2 DIALOG 0, 0, 245, 240 +CAPTION " Options " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + GROUPBOX "Cursor Size", -1, 10, 11, 100, 56 + AUTORADIOBUTTON "&Small", IDD_CURSOR_SMALL, 14, 23, 84, 10, + WS_TABSTOP | WS_GROUP + AUTORADIOBUTTON "&Medium", IDD_CURSOR_MEDIUM, 14, 33, 84, 10, + AUTORADIOBUTTON "&Large", IDD_CURSOR_LARGE, 14, 43, 84, 10, + + GROUPBOX "Command History", -1, 115, 11, 120, 56, WS_GROUP + LTEXT "&Buffer Size:", -1, 119, 25, 78, 9 + EDITTEXT IDD_HISTORY_SIZE, 197, 23, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_SIZESCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Number of Buffers:", -1, 119, 40, 78, 9 + EDITTEXT IDD_HISTORY_NUM, 197, 38, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_NUMSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + AUTOCHECKBOX "&Discard Old Duplicates", IDD_HISTORY_NODUP, 119,55, 108, 9 + + GROUPBOX "Edit Options",-1,10,70,225,64 + CONTROL "&QuickEdit Mode", IDD_QUICKEDIT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 82, 75, 10 + CONTROL "&Insert Mode", IDD_INSERT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 92, 75, 10 + CONTROL "Enable Ctrl &key shortcuts", IDD_CTRL_KEYS_ENABLED, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 102, 200, 10 + CONTROL "&Filter clipboard contents on paste", IDD_FILTER_ON_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 112, 200, 10 + CONTROL "Use Ctrl+Shift+C/V as &Copy/Paste", IDD_INTERCEPT_COPY_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 122, 200, 10 + + GROUPBOX "Text Selection",-1,10,136,225,32 + CONTROL "&Enable line wrapping selection", IDD_LINE_SELECTION, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 146, 200, 10 + CONTROL "E&xtended text selection keys", IDD_EDIT_KEYS, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 156, 200, 10 + + GROUPBOX "Current code page", IDD_LANGUAGE_GROUPBOX, 10, 170, 225, 24, WS_GROUP + LTEXT "",IDD_LANGUAGE, 16, 181, 184, 10, WS_GROUP + + CONTROL "&Use legacy console (requires relaunch, affects all consoles)", IDD_FORCEV2, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 10, 199, 200, 10 + + CONTROL "Learn more about legacy console mode", + IDD_HELP_LEGACY_LINK, "SysLink", WS_TABSTOP, 21, 211, 179, 10 + + CONTROL "Find out more about new console features", + IDD_HELP_SYSLINK, "SysLink", WS_TABSTOP, 10, 225, 200, 10 + +END + +// +// This is the template for the defaults settings dialog for use with ComCtlv5 +// At the time of writing, this only differed from the above by the Hyperlink control (which is only in v6) +// + +DID_SETTINGS_COMCTL5 DIALOG 0, 0, 240, 226 +CAPTION " Options " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + GROUPBOX "Cursor Size", -1, 10, 11, 100, 56 + AUTORADIOBUTTON "&Small", IDD_CURSOR_SMALL, 14, 23, 84, 10, + WS_TABSTOP | WS_GROUP + AUTORADIOBUTTON "&Medium", IDD_CURSOR_MEDIUM, 14, 33, 84, 10, + AUTORADIOBUTTON "&Large", IDD_CURSOR_LARGE, 14, 43, 84, 10, + + GROUPBOX "Command History", -1, 115, 11, 120, 56, WS_GROUP + LTEXT "&Buffer Size:", -1, 119, 25, 78, 9 + EDITTEXT IDD_HISTORY_SIZE, 197, 23, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_SIZESCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Number of Buffers:", -1, 119, 40, 78, 9 + EDITTEXT IDD_HISTORY_NUM, 197, 38, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_NUMSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + AUTOCHECKBOX "&Discard Old Duplicates", IDD_HISTORY_NODUP, 119,55, 108, 9 + + GROUPBOX "Edit Options",-1,10,70,225,54 + CONTROL "&QuickEdit Mode", IDD_QUICKEDIT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 82, 75, 10 + CONTROL "&Insert Mode", IDD_INSERT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 92, 75, 10 + CONTROL "Enable Ctrl &key shortcuts", IDD_CTRL_KEYS_ENABLED, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 102, 200, 10 + CONTROL "&Filter clipboard contents on paste", IDD_FILTER_ON_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 112, 200, 10 + + GROUPBOX "Text Selection",-1,10,126,225,32 + CONTROL "&Enable line wrapping selection", IDD_LINE_SELECTION, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 136, 200, 10 + CONTROL "E&xtended text selection keys", IDD_EDIT_KEYS, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 146, 200, 10 + + GROUPBOX "Default code page", IDD_LANGUAGE_GROUPBOX, 10, 160, 225, 29, WS_GROUP + COMBOBOX IDD_LANGUAGELIST, 16, 171, 213, 78, + CBS_SORT | + CBS_DISABLENOSCROLL | + CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP | WS_GROUP + + CONTROL "&Use legacy console (requires relaunch, affects all consoles)", IDD_FORCEV2, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 10, 194, 200, 10 + +END + +// +// This is the template for the specifics settings dialog for use with ComCtlv5 +// At the time of writing, this only differed from the above by the Hyperlink control (which is only in v6) +// + +DID_SETTINGS2_COMCTL5 DIALOG 0, 0, 245, 226 +CAPTION " Options " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + GROUPBOX "Cursor Size", -1, 10, 11, 100, 56 + AUTORADIOBUTTON "&Small", IDD_CURSOR_SMALL, 14, 23, 84, 10, + WS_TABSTOP | WS_GROUP + AUTORADIOBUTTON "&Medium", IDD_CURSOR_MEDIUM, 14, 33, 84, 10, + AUTORADIOBUTTON "&Large", IDD_CURSOR_LARGE, 14, 43, 84, 10, + + GROUPBOX "Command History", -1, 115, 11, 120, 56, WS_GROUP + LTEXT "&Buffer Size:", -1, 119, 25, 78, 9 + EDITTEXT IDD_HISTORY_SIZE, 197, 23, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_SIZESCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Number of Buffers:", -1, 119, 40, 78, 9 + EDITTEXT IDD_HISTORY_NUM, 197, 38, 31, 12, WS_GROUP | WS_TABSTOP + CONTROL "", IDD_HISTORY_NUMSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + AUTOCHECKBOX "&Discard Old Duplicates", IDD_HISTORY_NODUP, 119,55, 108, 9 + + GROUPBOX "Edit Options",-1,10,70,225,54 + CONTROL "&QuickEdit Mode", IDD_QUICKEDIT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 82, 75, 10 + CONTROL "&Insert Mode", IDD_INSERT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 92, 75, 10 + CONTROL "Enable Ctrl &key shortcuts", IDD_CTRL_KEYS_ENABLED, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 102, 200, 10 + CONTROL "&Filter clipboard contents on paste", IDD_FILTER_ON_PASTE, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 112, 200, 10 + + GROUPBOX "Text Selection",-1,10,126,225,32 + CONTROL "&Enable line wrapping selection", IDD_LINE_SELECTION, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 136, 200, 10 + CONTROL "E&xtended text selection keys", IDD_EDIT_KEYS, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 146, 200, 10 + + GROUPBOX "Current code page", IDD_LANGUAGE_GROUPBOX, 10, 160, 225, 24, WS_GROUP + LTEXT "",IDD_LANGUAGE, 16, 171, 184, 10, WS_GROUP + + CONTROL "&Use legacy console (requires relaunch, affects all consoles)", IDD_FORCEV2, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 10, 194, 200, 10 +END + +// +// This is the template for the font selection dialog +// + +DID_FONTDLG DIALOG 0, 0, 233, 226 +CAPTION " Font " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN +// PixelSize listbox & PointSize combobox (top left) +// + GROUPBOX "&Size", IDD_FONTSIZE, 5, 4, 110, 88 + LISTBOX IDD_PIXELSLIST, 12, 17, 50, 73, + LBS_DISABLENOSCROLL | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDD_POINTSLIST, 12, 17, 30, 76, + CBS_SORT | + CBS_DISABLENOSCROLL | WS_VSCROLL | WS_TABSTOP + +// Window Preview (top right) +// + LTEXT "Window Preview", IDD_PREVIEWLABEL, 125, 4, 109, 8 + CONTROL "", IDD_PREVIEWWINDOW, "WOAWinPreview", + WS_CHILD, 125, 17, 109, 83 + +// FaceName listbox (top middle) + GROUPBOX "&Font", IDD_STATIC, 5, 96, 228, 68 + LISTBOX IDD_FACENAME, 12, 108, 117, 42, + LBS_SORT | WS_VSCROLL | WS_TABSTOP | + LBS_OWNERDRAWFIXED | LBS_NOINTEGRALHEIGHT | LBS_HASSTRINGS + + LTEXT "TrueType fonts are recommended for high DPI displays as raster fonts may not display clearly.", + -1, 134, 107, 94, 49 + +// Bold Checkbox (bottom middle) +// + CONTROL "&Bold fonts", IDD_BOLDFONT, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 12, 151, 60, 10 + +// Bottom portion (middle) +// + GROUPBOX "", IDD_GROUP, 5, 170, 228, 50 + CONTROL "", IDD_FONTWINDOW, "WOAFontPreview", + WS_CHILD, 12, 180, 117, 34 + LTEXT "Each character is:", IDD_STATIC2, 135, 180, 75, 8, NOT + WS_GROUP + RTEXT "", IDD_FONTWIDTH, 134, 190, 12, 8, NOT WS_GROUP + LTEXT "screen pixels wide", IDD_STATIC3, 148, 190, 65, 8, NOT + WS_GROUP + LTEXT "screen pixels high", IDD_STATIC4, 148, 200, 65, 8, NOT + WS_GROUP + RTEXT "", IDD_FONTHEIGHT, 134, 200, 12, 8, NOT WS_GROUP + +END + +// +// This is the template for the screenbuffer size dialog +// + +DID_SCRBUFSIZE DIALOG 0, 0, 233, 226 +CAPTION " Layout " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN +// Window Preview (top left) +// + LTEXT "Window Preview", IDD_PREVIEWLABEL, 130, 11, 109, 8 + CONTROL "", IDD_PREVIEWWINDOW, "WOAWinPreview", + WS_CHILD, 130, 21, 109, 83 + + GROUPBOX "Screen Buffer Size", -1, 10, 11, 110, 56 + LTEXT "&Width:", -1, 14, 25, 54, 9 + EDITTEXT IDD_SCRBUF_WIDTH, 78, 23, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_SCRBUF_WIDTHSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Height:", -1, 14, 39, 54, 9 + EDITTEXT IDD_SCRBUF_HEIGHT, 78, 37, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_SCRBUF_HEIGHTSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + CONTROL "W&rap text output on resize", IDD_LINE_WRAP, "Button", + BS_AUTOCHECKBOX | WS_TABSTOP, 14, 52, 100, 12 + + GROUPBOX "Window Size", -1, 10, 69, 110, 42 + LTEXT "W&idth:", -1, 14, 83, 54, 9 + EDITTEXT IDD_WINDOW_WIDTH, 78, 81, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_WINDOW_WIDTHSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "H&eight:", -1, 14, 97, 54, 9 + EDITTEXT IDD_WINDOW_HEIGHT, 78, 95, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_WINDOW_HEIGHTSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + + GROUPBOX "Window Position", -1, 10, 113, 110, 56 + LTEXT "&Left:", -1, 14, 127, 54, 9 + EDITTEXT IDD_WINDOW_POSX, 78, 125, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_WINDOW_POSXSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Top:", -1, 14, 141, 54, 9 + EDITTEXT IDD_WINDOW_POSY, 78, 139, 36, 12, + ES_AUTOHSCROLL | WS_GROUP | WS_TABSTOP + CONTROL "", IDD_WINDOW_POSYSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + CONTROL "Let system &position window", IDD_AUTO_POSITION, "Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP, 14, 156, 100, 10 +END + +// +// This is the template for the screen colors dialog +// + +DID_COLOR DIALOG 0, 0, 233, 226 +CAPTION " Colors " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + AUTORADIOBUTTON "Screen &Text", IDD_COLOR_SCREEN_TEXT, 10, 10, 104, 12, + WS_TABSTOP|WS_GROUP + AUTORADIOBUTTON "Screen &Background", IDD_COLOR_SCREEN_BKGND, 10, 22, 104, 12, + AUTORADIOBUTTON "&Popup Text", IDD_COLOR_POPUP_TEXT, 10, 34, 104, 12, + AUTORADIOBUTTON "Pop&up Background", IDD_COLOR_POPUP_BKGND, 10, 46, 104, 12, + + CONTROL "", IDD_COLOR_1, "ColorTableColor", + WS_BORDER | WS_CHILD | WS_GROUP | WS_TABSTOP, + 12, 70, 13, 13 + CONTROL "", IDD_COLOR_2, "ColorTableColor", WS_BORDER | WS_CHILD, + 25, 70, 13, 13 + CONTROL "", IDD_COLOR_3, "ColorTableColor", WS_BORDER | WS_CHILD, + 38, 70, 13, 13 + CONTROL "", IDD_COLOR_4, "ColorTableColor", WS_BORDER | WS_CHILD, + 51, 70, 13, 13 + CONTROL "", IDD_COLOR_5, "ColorTableColor", WS_BORDER | WS_CHILD, + 64, 70, 13, 13 + CONTROL "", IDD_COLOR_6, "ColorTableColor", WS_BORDER | WS_CHILD, + 77, 70, 13, 13 + CONTROL "", IDD_COLOR_7, "ColorTableColor", WS_BORDER | WS_CHILD, + 90, 70, 13, 13 + CONTROL "", IDD_COLOR_8, "ColorTableColor", WS_BORDER | WS_CHILD, + 103, 70, 13, 13 + CONTROL "", IDD_COLOR_9, "ColorTableColor", WS_BORDER | WS_CHILD, + 116, 70, 13, 13 + CONTROL "", IDD_COLOR_10, "ColorTableColor", WS_BORDER | WS_CHILD, + 129, 70, 13, 13 + CONTROL "", IDD_COLOR_11, "ColorTableColor", WS_BORDER | WS_CHILD, + 142, 70, 13, 13 + CONTROL "", IDD_COLOR_12, "ColorTableColor", WS_BORDER | WS_CHILD, + 155, 70, 13, 13 + CONTROL "", IDD_COLOR_13, "ColorTableColor", WS_BORDER | WS_CHILD, + 168, 70, 13, 13 + CONTROL "", IDD_COLOR_14, "ColorTableColor", WS_BORDER | WS_CHILD, + 181, 70, 13, 13 + CONTROL "", IDD_COLOR_15, "ColorTableColor", WS_BORDER | WS_CHILD, + 194, 70, 13, 13 + CONTROL "", IDD_COLOR_16, "ColorTableColor", WS_BORDER | WS_CHILD, + 207, 70, 13, 13 + + GROUPBOX "Selected Screen Colors", -1, 10, 84, 213, 46 + CONTROL "", IDD_COLOR_SCREEN_COLORS, "WOAFontPreview", + WS_GROUP | WS_CHILD, 15, 94, 204, 31 + + GROUPBOX "Selected Popup Colors", -1, 10, 134, 213, 46 + CONTROL "", IDD_COLOR_POPUP_COLORS, "WOAFontPreview", + WS_GROUP | WS_CHILD, 15, 144, 204, 31 + + GROUPBOX "Selected Color Values", -1, 120, 9, 103, 56 + LTEXT "&Red:", -1, 124, 23, 54, 9 + EDITTEXT IDD_COLOR_RED, 167, 21, 30, 12, WS_TABSTOP | WS_GROUP | + ES_AUTOHSCROLL + CONTROL "", IDD_COLOR_REDSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "&Green:", -1, 124, 37, 54, 9 + EDITTEXT IDD_COLOR_GREEN, 167, 35, 30, 12, WS_GROUP | WS_TABSTOP | + ES_AUTOHSCROLL + CONTROL "", IDD_COLOR_GREENSCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + LTEXT "B&lue:", -1, 124, 51, 54, 9 + EDITTEXT IDD_COLOR_BLUE, 167, 49, 30, 12, WS_GROUP | WS_TABSTOP | + ES_AUTOHSCROLL + CONTROL "", IDD_COLOR_BLUESCROLL, UPDOWN_CLASS, + UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | + UDS_ARROWKEYS | UDS_NOTHOUSANDS, 0, 0, 0, 0 + + GROUPBOX "&Opacity", IDD_OPACITY_GROUPBOX, 10, 182, 213, 38 + LTEXT "30%", IDD_OPACITY_LOW_LABEL, 15, 194, 16, 12 + CONTROL "Opacity", IDD_TRANSPARENCY, "msctls_trackbar32", + TBS_NOTICKS | TBS_HORZ | WS_TABSTOP, 35, 192, 135, 16 + LTEXT "100%", IDD_OPACITY_HIGH_LABEL, 174, 194, 18, 12 + LTEXT "", IDD_OPACITY_VALUE, 200, 193, 18, 10, SS_CENTER | SS_SUNKEN +END + + +// +// This is the template for the terminal dialog +// +// These defines help keep it sane when you're placing components relative to other components +// padding +#define P_0 2 +#define P_1 8 +#define P_2 (P_1*2) +#define P_3 (P_1*3) +#define P_4 (P_1*4) + +#define COLOR_SIZE 13 +// default Colors group box +#define T_COLORS_X 10 +#define T_COLORS_Y 10 +#define T_COLORS_W (225) +#define T_COLORS_H 65 +#define T_COLORS_CHECK_Y (T_COLORS_Y+P_4) +#define T_COLORS_TEXT_W 32 +#define T_COLORS_EDIT_W 30 +#define T_COLORS_EDIT_H 12 +#define T_COLORS_FG_X (T_COLORS_X+P_4) +#define T_COLORS_FG_W (100) +#define T_COLORS_RED_Y (T_COLORS_CHECK_Y+10) +#define T_COLORS_GREEN_Y (T_COLORS_RED_Y+T_COLORS_EDIT_H+P_0) +#define T_COLORS_BLUE_Y (T_COLORS_GREEN_Y+T_COLORS_EDIT_H+P_0) +#define T_COLORS_FG_TEXT_X (T_COLORS_X+P_4+P_1+COLOR_SIZE) +#define T_COLORS_FG_INPUT_X (T_COLORS_FG_TEXT_X+T_COLORS_TEXT_W) +#define T_COLORS_BG_X (T_COLORS_FG_X+T_COLORS_FG_W+P_1) +#define T_COLORS_BG_TEXT_X (T_COLORS_BG_X+P_1+COLOR_SIZE) +#define T_COLORS_BG_INPUT_X (T_COLORS_BG_TEXT_X+T_COLORS_TEXT_W) + +// cursor styles group box +#define T_CSTYLE_X T_COLORS_X +#define T_CSTYLE_Y (T_COLORS_Y+T_COLORS_H+P_1) +#define T_CSTYLE_W 100 +#define T_CSTYLE_H 75 +// radio button dimensions +#define T_CSTYLE_R_W (T_CSTYLE_W-P_4-P_4) +#define T_CSTYLE_R_H (10) +// radio button positions +#define T_CSTYLE_R_1_Y (T_CSTYLE_Y+P_2) +#define T_CSTYLE_R_2_Y (T_CSTYLE_R_1_Y+T_CSTYLE_R_H) +#define T_CSTYLE_R_3_Y (T_CSTYLE_R_2_Y+T_CSTYLE_R_H) +#define T_CSTYLE_R_4_Y (T_CSTYLE_R_3_Y+T_CSTYLE_R_H) +#define T_CSTYLE_R_5_Y (T_CSTYLE_R_4_Y+T_CSTYLE_R_H) + +#define T_CCOLOR_X (T_CSTYLE_X+T_CSTYLE_W+P_1) +#define T_CCOLOR_Y (T_CSTYLE_Y) +#define T_CCOLOR_W (117) // 117 lines this up perfectly with the default colors group box +#define T_CCOLOR_R_W (T_CCOLOR_W-P_4-P_4) +#define T_CCOLOR_COLOR_X (T_CCOLOR_X+P_4) +#define T_CCOLOR_TEXT_X (T_CCOLOR_COLOR_X+COLOR_SIZE+P_1) +#define T_CCOLOR_EDIT_X (T_CCOLOR_TEXT_X+T_CCOLOR_TEXT_W) +#define T_CCOLOR_TEXT_W (T_COLORS_TEXT_W) +#define T_CCOLORS_EDIT_W (T_COLORS_EDIT_W) +#define T_CCOLORS_EDIT_H (T_COLORS_EDIT_H) +#define T_CCOLOR_R_Y (T_CSTYLE_R_3_Y) +#define T_CCOLOR_G_Y (T_CCOLOR_R_Y+T_CCOLORS_EDIT_H+P_0) +#define T_CCOLOR_B_Y (T_CCOLOR_G_Y+T_CCOLORS_EDIT_H+P_0) +#define T_CCOLOR_H T_CSTYLE_H + +// terminal scrolling group box +#define T_SCROLL_X T_CSTYLE_X +#define T_SCROLL_Y (T_CCOLOR_Y+T_CCOLOR_H+P_1) +#define T_SCROLL_W 100 +#define T_SCROLL_H 40 + +#define UPDOWN_STYLES (UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ARROWKEYS | UDS_NOTHOUSANDS | UDS_ALIGNRIGHT) +DID_TERMINAL DIALOG 0, 0, 245, 226 +CAPTION " Terminal " +STYLE WS_VISIBLE | WS_CAPTION | WS_CHILD | DS_MODALFRAME +FONT 8,"MS Shell Dlg" +BEGIN + + // GROUPBOX text, id, x, y, width, height [, style [, extended-style]] + // CONTROL text, id, class, style, x, y, width, height [, extended-style] + + GROUPBOX "Terminal Colors", -1, T_COLORS_X, T_COLORS_Y, T_COLORS_W, T_COLORS_H, WS_GROUP + + AUTOCHECKBOX "Use Separate Foreground", IDD_USE_TERMINAL_FG, T_COLORS_X+P_1, T_COLORS_CHECK_Y, T_COLORS_FG_W, 10 + + CONTROL "", IDD_TERMINAL_FGCOLOR, "SimpleColor", WS_BORDER | WS_CHILD | WS_GROUP , + T_COLORS_X+P_2, T_COLORS_RED_Y, COLOR_SIZE, COLOR_SIZE + + LTEXT "Red:", -1, T_COLORS_FG_TEXT_X, T_COLORS_RED_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_FG_RED, T_COLORS_FG_INPUT_X, T_COLORS_RED_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_FG_REDSCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Green:", -1, T_COLORS_FG_TEXT_X, T_COLORS_GREEN_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_FG_GREEN, T_COLORS_FG_INPUT_X, T_COLORS_GREEN_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_FG_GREENSCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Blue:", -1, T_COLORS_FG_TEXT_X, T_COLORS_BLUE_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_FG_BLUE, T_COLORS_FG_INPUT_X, T_COLORS_BLUE_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_FG_BLUESCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + AUTOCHECKBOX "Use Separate Background", IDD_USE_TERMINAL_BG, T_COLORS_BG_X, T_COLORS_CHECK_Y, T_COLORS_FG_W, 10 + + CONTROL "", IDD_TERMINAL_BGCOLOR, "SimpleColor", WS_BORDER | WS_CHILD | WS_GROUP , + T_COLORS_BG_X, T_COLORS_RED_Y, COLOR_SIZE, COLOR_SIZE + + LTEXT "Red:", -1, T_COLORS_BG_TEXT_X, T_COLORS_RED_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_BG_RED, T_COLORS_BG_INPUT_X, T_COLORS_RED_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_BG_REDSCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Green:", -1, T_COLORS_BG_TEXT_X, T_COLORS_GREEN_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_BG_GREEN, T_COLORS_BG_INPUT_X, T_COLORS_GREEN_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_BG_GREENSCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Blue:", -1, T_COLORS_BG_TEXT_X, T_COLORS_BLUE_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_BG_BLUE, T_COLORS_BG_INPUT_X, T_COLORS_BLUE_Y, T_COLORS_EDIT_W, T_COLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_BG_BLUESCROLL, UPDOWN_CLASS, + UPDOWN_STYLES, 0, 0, 0, 0 + + + GROUPBOX "Cursor Shape", -1, T_CSTYLE_X, T_CSTYLE_Y, T_CSTYLE_W, T_CSTYLE_H + AUTORADIOBUTTON "Use Legacy Style", IDD_TERMINAL_LEGACY_CURSOR, T_CSTYLE_X+P_1, T_CSTYLE_R_1_Y, T_CSTYLE_R_W, T_CSTYLE_R_H, WS_TABSTOP|WS_GROUP + AUTORADIOBUTTON "Underscore", IDD_TERMINAL_UNDERSCORE, T_CSTYLE_X+P_1, T_CSTYLE_R_2_Y, T_CSTYLE_R_W, T_CSTYLE_R_H, + AUTORADIOBUTTON "Vertical Bar", IDD_TERMINAL_VERTBAR, T_CSTYLE_X+P_1, T_CSTYLE_R_3_Y, T_CSTYLE_R_W, T_CSTYLE_R_H, + AUTORADIOBUTTON "Empty Box", IDD_TERMINAL_EMPTYBOX, T_CSTYLE_X+P_1, T_CSTYLE_R_4_Y, T_CSTYLE_R_W, T_CSTYLE_R_H, + AUTORADIOBUTTON "Solid Box", IDD_TERMINAL_SOLIDBOX, T_CSTYLE_X+P_1, T_CSTYLE_R_5_Y, T_CSTYLE_R_W, T_CSTYLE_R_H, + + + GROUPBOX "Cursor Colors", -1, T_CCOLOR_X, T_CCOLOR_Y, T_CCOLOR_W, T_CCOLOR_H, WS_GROUP + + AUTORADIOBUTTON "Inverse Color", IDD_TERMINAL_INVERSE_CURSOR, T_CCOLOR_X+P_1, T_CSTYLE_R_1_Y, T_CCOLOR_R_W, T_CSTYLE_R_H, WS_TABSTOP|WS_GROUP + + AUTORADIOBUTTON "Use Color", IDD_TERMINAL_CURSOR_USECOLOR, T_CCOLOR_X+P_1, T_CSTYLE_R_2_Y, T_CCOLOR_R_W, T_CSTYLE_R_H, + + CONTROL "", IDD_TERMINAL_CURSOR_COLOR, "SimpleColor", WS_BORDER | WS_CHILD | WS_GROUP, + T_CCOLOR_X+P_2, T_CSTYLE_R_3_Y, COLOR_SIZE, COLOR_SIZE + + LTEXT "Red:", -1, T_CCOLOR_TEXT_X, T_CCOLOR_R_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_CURSOR_RED, T_CCOLOR_EDIT_X, T_CCOLOR_R_Y, T_CCOLORS_EDIT_W, T_CCOLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_CURSOR_REDSCROLL, UPDOWN_CLASS, UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Green:", -1, T_CCOLOR_TEXT_X, T_CCOLOR_G_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_CURSOR_GREEN, T_CCOLOR_EDIT_X, T_CCOLOR_G_Y, T_CCOLORS_EDIT_W, T_CCOLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_CURSOR_GREENSCROLL, UPDOWN_CLASS, UPDOWN_STYLES, 0, 0, 0, 0 + + LTEXT "Blue:", -1, T_CCOLOR_TEXT_X, T_CCOLOR_B_Y, T_COLORS_TEXT_W, 9 + EDITTEXT IDD_TERMINAL_CURSOR_BLUE, T_CCOLOR_EDIT_X, T_CCOLOR_B_Y, T_CCOLORS_EDIT_W, T_CCOLORS_EDIT_H, WS_TABSTOP | WS_GROUP | ES_AUTOHSCROLL + CONTROL "", IDD_TERMINAL_CURSOR_BLUESCROLL, UPDOWN_CLASS, UPDOWN_STYLES, 0, 0, 0, 0 + + + GROUPBOX "Terminal Scrolling", -1, T_SCROLL_X, T_SCROLL_Y, T_SCROLL_W, T_SCROLL_H + AUTOCHECKBOX "Disable Scroll-Forward", IDD_DISABLE_SCROLLFORWARD, T_SCROLL_X+P_1, T_SCROLL_Y+P_2, T_SCROLL_W-P_4-P_4, 10 + + CONTROL "Find out more about experimental terminal settings", + IDD_HELP_TERMINAL, "SysLink", WS_TABSTOP, 10, 225, 200, 10 +END + + +// +// Strings +// + +STRINGTABLE PRELOAD +BEGIN + IDS_NAME, "Console" + IDS_INFO, "Configures console properties." + IDS_TITLE, "Console Windows" + IDS_RASTERFONT, "Raster Fonts" + IDS_FONTSIZE, "Point size should be between %d and %d" + IDS_SELECTEDFONT, "Selected Font" + IDS_LINKERRCAP, "Error Updating Shortcut" + IDS_LINKERROR, "Unable to modify the shortcut:\n%s.\nCheck to make sure it has not been deleted or renamed." + IDS_TOOLTIP_LINE_SELECTION, "Instead of being rectangular, text selection wraps lines." + IDS_TOOLTIP_FILTER_ON_PASTE, "When pasting, remove tabs and convert smart quotes to regular quotes." + IDS_TOOLTIP_LINE_WRAP, "When resizing the window, wrap text to fit width." + IDS_TOOLTIP_CTRL_KEYS, "Allow new Ctrl-key shortcuts (may interfere with some applications)." + IDS_TOOLTIP_EDIT_KEYS, "Enable enhanced keyboard editing on command line." + IDS_TOOLTIP_OPACITY, "Adjust the transparency of the console window." + IDS_TOOLTIP_INTERCEPT_COPY_PASTE, "Use Ctrl+Shift+C/V as copy/paste shortcuts, regardless of input mode" +END + + +// +// Version resource information +// + +#define VER_FILETYPE VFT_DLL +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Control Panel Console Applet" +#define VER_INTERNALNAME_STR "Console\0" +#define VER_ORIGINALFILENAME_STR "CONSOLE.DLL" + +#ifndef EXTERNAL_BUILD +#include "common.ver" +#endif + +// +// Bitmaps +// +BM_TRUETYPE_ICON BITMAP "..\\..\\res\\truetype.bmp" + diff --git a/src/propsheet/dbcs.cpp b/src/propsheet/dbcs.cpp new file mode 100644 index 000000000..71b4e4c72 --- /dev/null +++ b/src/propsheet/dbcs.cpp @@ -0,0 +1,252 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + dbcs.c + +Abstract: + + This module contains the code for console DBCS font dialog + +Author: + + kazum Feb-27-1995 + +Revision History: + +--*/ + +#include "precomp.h" +#include +#pragma hdrstop + +NTSTATUS +MakeAltRasterFont( + __in UINT CodePage, + __out COORD *AltFontSize, + __out BYTE *AltFontFamily, + __out ULONG *AltFontIndex, + __out_ecount(LF_FACESIZE) LPTSTR AltFaceName + ) +{ + DWORD i; + DWORD Find; + ULONG FontIndex; + COORD FontSize = FontInfo[DefaultFontIndex].Size; + COORD FontDelta; + BOOL fDbcsCharSet = IS_ANY_DBCS_CHARSET( CodePageToCharSet( CodePage ) ); + + FontIndex = 0; + Find = (DWORD)-1; + for (i=0; i < NumberOfFonts; i++) + { + if (!TM_IS_TT_FONT(FontInfo[i].Family) && + IS_ANY_DBCS_CHARSET(FontInfo[i].tmCharSet) == fDbcsCharSet + ) + { + FontDelta.X = (SHORT)abs(FontSize.X - FontInfo[i].Size.X); + FontDelta.Y = (SHORT)abs(FontSize.Y - FontInfo[i].Size.Y); + if (Find > (DWORD)(FontDelta.X + FontDelta.Y)) + { + Find = (DWORD)(FontDelta.X + FontDelta.Y); + FontIndex = i; + } + } + } + + *AltFontIndex = FontIndex; + StringCchCopy(AltFaceName, LF_FACESIZE, FontInfo[*AltFontIndex].FaceName); + *AltFontSize = FontInfo[*AltFontIndex].Size; + *AltFontFamily = FontInfo[*AltFontIndex].Family; + + DBGFONTS(("MakeAltRasterFont : AltFontIndex = %ld\n", *AltFontIndex)); + + return STATUS_SUCCESS; +} + +NTSTATUS +InitializeDbcsMisc( + VOID) +{ + return TrueTypeFontList::s_Initialize(); +} + +BYTE +CodePageToCharSet( + UINT CodePage) +{ + CHARSETINFO csi; + + if (!TranslateCharsetInfo((DWORD *)IntToPtr(CodePage), &csi, TCI_SRCCODEPAGE)) { + csi.ciCharset = OEM_CHARSET; + } + + return (BYTE)csi.ciCharset; +} + +LPTTFONTLIST +SearchTTFont( + __in_opt LPCTSTR ptszFace, + BOOL fCodePage, + UINT CodePage + ) +{ + return TrueTypeFontList::s_SearchByName(ptszFace, fCodePage, CodePage); +} + +BOOL +IsAvailableTTFont( + LPCTSTR ptszFace) +{ + if (SearchTTFont(ptszFace, FALSE, 0)) { + return TRUE; + } else { + return FALSE; + } +} + +BOOL +IsAvailableTTFontCP( + LPCTSTR ptszFace, + UINT CodePage) +{ + if (SearchTTFont(ptszFace, TRUE, CodePage)) { + return TRUE; + } else { + return FALSE; + } +} + +BOOL +IsDisableBoldTTFont( + LPCTSTR ptszFace) +{ + LPTTFONTLIST pTTFontList; + + pTTFontList = SearchTTFont(ptszFace, FALSE, 0); + if (pTTFontList != NULL) { + return pTTFontList->fDisableBold; + } else { + return FALSE; + } +} + +LPTSTR +GetAltFaceName( + LPCTSTR ptszFace) +{ + LPTTFONTLIST pTTFontList; + + pTTFontList = SearchTTFont(ptszFace, FALSE, 0); + if (pTTFontList != NULL) { + if (wcscmp(ptszFace, pTTFontList->FaceName1) == 0) { + return pTTFontList->FaceName2; + } + + if (wcscmp(ptszFace, pTTFontList->FaceName2) == 0) { + return pTTFontList->FaceName1; + } + } + + return NULL; +} + +NTSTATUS +DestroyDbcsMisc( + VOID) +{ + return TrueTypeFontList::s_Destroy(); +} + +int +LanguageListCreate( + HWND hDlg, + UINT CodePage + ) + +/*++ + + Initializes the Language list by enumerating all Locale Information. + + Returns +--*/ + +{ + HWND hWndLanguageCombo; + LONG lListIndex; + CPINFOEX cpinfo; + UINT oemcp; + + /* + * Create ComboBox items + */ + hWndLanguageCombo = GetDlgItem(hDlg, IDD_LANGUAGELIST); + SendMessage(hWndLanguageCombo, CB_RESETCONTENT, 0, 0L); + + // Add our current CJK code page to the list + oemcp = GetOEMCP(); + if (GetCPInfoExW(oemcp, 0, &cpinfo)) + { + lListIndex = (LONG)SendMessage(hWndLanguageCombo, CB_ADDSTRING, 0, (LPARAM)cpinfo.CodePageName); + if (lListIndex != CB_ERR) + { + SendMessage(hWndLanguageCombo, CB_SETITEMDATA, (DWORD)lListIndex, oemcp); + + if (CodePage == oemcp) + { + SendMessage(hWndLanguageCombo, CB_SETCURSEL, lListIndex, 0L); + } + } + } + + // Add SBCS 437 OEM - United States to the list + if (GetCPInfoExW(437, 0, &cpinfo)) + { + lListIndex = (LONG)SendMessage(hWndLanguageCombo, CB_ADDSTRING, 0, (LPARAM)cpinfo.CodePageName); + if (lListIndex != CB_ERR) + { + SendMessage(hWndLanguageCombo, CB_SETITEMDATA, (DWORD)lListIndex, 437); + + if (CodePage == 437) + { + SendMessage(hWndLanguageCombo, CB_SETCURSEL, lListIndex, 0L); + } + } + } + + /* + * Get the LocaleIndex from the currently selected item. + * (i will be LB_ERR if no currently selected item). + */ + lListIndex = (LONG)SendMessage(hWndLanguageCombo, CB_GETCURSEL, 0, 0L); + const int iRet = (int)SendMessage(hWndLanguageCombo, CB_GETITEMDATA, lListIndex, 0L); + + EnableWindow(hWndLanguageCombo, g_fEastAsianSystem); + + return iRet; +} + + +int LanguageDisplay(HWND hDlg, UINT CodePage) +{ + CPINFOEX cpinfo; + + if (GetCPInfoExW(CodePage, 0, &cpinfo)) + { + SetDlgItemText(hDlg, IDD_LANGUAGE, cpinfo.CodePageName); + } + + return TRUE; +} + +// For a given codepage, determine what the default truetype font should be +NTSTATUS GetTTFontFaceForCodePage(const UINT uiCodePage, // the codepage to examine (note: not charset) + _Out_writes_(cchFaceName) PWSTR pszFaceName, // where to write the facename we find + const size_t cchFaceName) // space available in pszFaceName +{ + return TrueTypeFontList::s_SearchByCodePage(uiCodePage, pszFaceName, cchFaceName); +} + diff --git a/src/propsheet/dialogs.h b/src/propsheet/dialogs.h new file mode 100644 index 000000000..8bf113973 --- /dev/null +++ b/src/propsheet/dialogs.h @@ -0,0 +1,164 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + dialogs.h + +Abstract: + + This module contains the definitions for the console dialog boxes + +Author: + + Therese Stowell (thereses) Feb-3-1992 (swiped from Win3.1) + +Revision History: + +--*/ + +#pragma once + +#define DID_SETTINGS 100 +// unused 101 +// unused 102 +#define IDD_QUICKEDIT 103 +#define IDD_INSERT 104 +#define IDD_CURSOR_SMALL 105 +#define IDD_CURSOR_MEDIUM 106 +#define IDD_CURSOR_LARGE 107 +#define IDD_HISTORY_SIZE 108 +#define IDD_HISTORY_SIZESCROLL 109 +#define IDD_HISTORY_NUM 110 +#define IDD_HISTORY_NUMSCROLL 111 +#define IDD_HISTORY_NODUP 112 +#define IDD_LANGUAGELIST 113 +// v-HirShi Nov.2.1996 +#define DID_SETTINGS2 114 +#define IDD_LANGUAGE 115 +#define IDD_LANGUAGE_GROUPBOX 116 +#define DID_SETTINGS_COMCTL5 117 +#define DID_SETTINGS2_COMCTL5 118 + +#define DID_FONTDLG 200 +#define IDD_STATIC 201 +#define IDD_FACENAME 202 +#define IDD_BOLDFONT 203 +#define IDD_STATIC2 204 +#define IDD_PREVIEWLABEL 206 +#define IDD_GROUP 207 +#define IDD_STATIC3 208 +#define IDD_STATIC4 209 +#define IDD_FONTWIDTH 210 +#define IDD_FONTHEIGHT 211 +#define IDD_FONTSIZE 212 +#define IDD_POINTSLIST 213 +#define IDD_PIXELSLIST 214 +#define IDD_PREVIEWWINDOW 215 +#define IDD_FONTWINDOW 216 + +#define DID_SCRBUFSIZE 300 +#define IDD_SCRBUF_WIDTH 301 +#define IDD_SCRBUF_WIDTHSCROLL 302 +#define IDD_SCRBUF_HEIGHT 303 +#define IDD_SCRBUF_HEIGHTSCROLL 304 +#define IDD_WINDOW_WIDTH 305 +#define IDD_WINDOW_WIDTHSCROLL 306 +#define IDD_WINDOW_HEIGHT 307 +#define IDD_WINDOW_HEIGHTSCROLL 308 +#define IDD_WINDOW_POSX 309 +#define IDD_WINDOW_POSXSCROLL 310 +#define IDD_WINDOW_POSY 311 +#define IDD_WINDOW_POSYSCROLL 312 +#define IDD_AUTO_POSITION 313 + +#define DID_COLOR 400 +#define IDD_COLOR_SCREEN_TEXT 401 +#define IDD_COLOR_SCREEN_BKGND 402 +#define IDD_COLOR_POPUP_TEXT 403 +#define IDD_COLOR_POPUP_BKGND 404 +#define IDD_COLOR_1 405 +#define IDD_COLOR_2 406 +#define IDD_COLOR_3 407 +#define IDD_COLOR_4 408 +#define IDD_COLOR_5 409 +#define IDD_COLOR_6 410 +#define IDD_COLOR_7 411 +#define IDD_COLOR_8 412 +#define IDD_COLOR_9 413 +#define IDD_COLOR_10 414 +#define IDD_COLOR_11 415 +#define IDD_COLOR_12 416 +#define IDD_COLOR_13 417 +#define IDD_COLOR_14 418 +#define IDD_COLOR_15 419 +#define IDD_COLOR_16 420 +#define IDD_COLOR_SCREEN_COLORS 421 +#define IDD_COLOR_POPUP_COLORS 422 +#define IDD_COLOR_RED 423 +#define IDD_COLOR_REDSCROLL 424 +#define IDD_COLOR_GREEN 425 +#define IDD_COLOR_GREENSCROLL 426 +#define IDD_COLOR_BLUE 427 +#define IDD_COLOR_BLUESCROLL 428 + +// unused 500 +#define IDD_FORCEV2 501 +#define IDD_LINE_SELECTION 502 +#define IDD_FILTER_ON_PASTE 503 +#define IDD_LINE_WRAP 504 +#define IDD_CTRL_KEYS_ENABLED 505 +#define IDD_TRANSPARENCY 506 +#define IDD_EDIT_KEYS 507 +// unused 508 +// unused 509 +#define IDD_OPACITY_GROUPBOX 510 +#define IDD_OPACITY_LOW_LABEL 511 +#define IDD_OPACITY_HIGH_LABEL 512 +#define IDD_HELP_SYSLINK 513 +#define IDD_OPACITY_VALUE 514 +#define IDD_INTERCEPT_COPY_PASTE 515 +#define IDD_HELP_LEGACY_LINK 516 + + +#define DID_TERMINAL 600 +#define IDD_USE_TERMINAL_FG 601 +#define IDD_TERMINAL_FG_REDSCROLL 602 +#define IDD_TERMINAL_FG_GREENSCROLL 603 +#define IDD_TERMINAL_FG_BLUESCROLL 604 +#define IDD_USE_TERMINAL_BG 605 +#define IDD_TERMINAL_BG_REDSCROLL 606 +#define IDD_TERMINAL_BG_GREENSCROLL 607 +#define IDD_TERMINAL_BG_BLUESCROLL 608 +#define IDD_TERMINAL_FGCOLOR 609 +#define IDD_TERMINAL_BGCOLOR 610 +#define IDD_TERMINAL_INVERSE_CURSOR 611 +#define IDD_TERMINAL_CURSOR_USECOLOR 612 +#define IDD_TERMINAL_CURSOR_COLOR 613 +#define IDD_TERMINAL_CURSOR_REDSCROLL 614 +#define IDD_TERMINAL_CURSOR_GREENSCROLL 615 +#define IDD_TERMINAL_CURSOR_BLUESCROLL 616 +#define IDD_TERMINAL_LEGACY_CURSOR 617 +#define IDD_TERMINAL_VERTBAR 618 +#define IDD_TERMINAL_UNDERSCORE 619 +#define IDD_TERMINAL_EMPTYBOX 620 +#define IDD_TERMINAL_SOLIDBOX 621 +#define IDD_DISABLE_SCROLLFORWARD 622 +#define IDD_TERMINAL_FG_RED 623 +#define IDD_TERMINAL_FG_GREEN 624 +#define IDD_TERMINAL_FG_BLUE 625 +#define IDD_TERMINAL_BG_RED 626 +#define IDD_TERMINAL_BG_GREEN 627 +#define IDD_TERMINAL_BG_BLUE 628 +#define IDD_TERMINAL_CURSOR_RED 629 +#define IDD_TERMINAL_CURSOR_GREEN 630 +#define IDD_TERMINAL_CURSOR_BLUE 631 +#define IDD_HELP_TERMINAL 632 + +#define BM_TRUETYPE_ICON 1000 + + + + diff --git a/src/propsheet/dll.cpp b/src/propsheet/dll.cpp new file mode 100644 index 000000000..7f7ae4737 --- /dev/null +++ b/src/propsheet/dll.cpp @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +using namespace Microsoft::WRL; + +#if !defined(__WRL_WINRT_STRICT__) +_Check_return_ +STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ void** ppv) +{ + return Module::GetModule().GetClassObject(rclsid, riid, ppv); +} +#endif + +STDAPI DllCanUnloadNow() +{ + return Module::GetModule().Terminate() ? S_OK : S_FALSE; +} diff --git a/src/propsheet/font.h b/src/propsheet/font.h new file mode 100644 index 000000000..0fd6baa73 --- /dev/null +++ b/src/propsheet/font.h @@ -0,0 +1,114 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + font.h + +Abstract: + + This module contains the data structures, data types, + and procedures related to fonts. + +Author: + + Therese Stowell (thereses) 15-Jan-1991 + +Revision History: + +--*/ +#pragma once +#ifndef FONT_H +#define FONT_H + +#define INITIAL_FONTS 20 +#define FONT_INCREMENT 3 + +#define EF_NEW 0x0001 // a newly available face +#define EF_OLD 0x0002 // a previously available face +#define EF_ENUMERATED 0x0004 // all sizes have been enumerated +#define EF_OEMFONT 0x0008 // an OEM face +#define EF_TTFONT 0x0010 // a TT face +#define EF_DEFFACE 0x0020 // the default face +#define EF_DBCSFONT 0x0040 // the DBCS font + +/* + * FONT_INFO + * + * The distinction between the desired and actual font dimensions obtained + * is important in the case of TrueType fonts, in which there is no guarantee + * that what you ask for is what you will get. + * + * Note that the correspondence between "Desired" and "Actual" is broken + * whenever the user changes his display driver, because GDI uses driver + * parameters to control the font rasterization. + * + * The SizeDesired is {0, 0} if the font is a raster font. + */ +typedef struct _FONT_INFO { + HFONT hFont; + COORD Size; // font size obtained + COORD SizeWant; // 0;0 if Raster font + LONG Weight; + LPTSTR FaceName; + BYTE Family; + BYTE tmCharSet; +} FONT_INFO, *PFONT_INFO; + +#pragma warning(push) +#pragma warning(disable:4200) // nonstandard extension used : zero-sized array in struct/union + +typedef struct tagFACENODE { + struct tagFACENODE *pNext; + DWORD dwFlag; + TCHAR atch[]; +} FACENODE, *PFACENODE; + +#pragma warning(pop) + +#define TM_IS_TT_FONT(x) (((x) & TMPF_TRUETYPE) == TMPF_TRUETYPE) +#define IS_BOLD(w) ((w) >= FW_SEMIBOLD) +#define SIZE_EQUAL(s1, s2) (((s1).X == (s2).X) && ((s1).Y == (s2).Y)) +#define POINTS_PER_INCH 72 +#define MIN_PIXEL_HEIGHT 5 +#define MAX_PIXEL_HEIGHT 72 + + +// +// Function prototypes +// + +VOID +InitializeFonts(VOID); + +VOID +DestroyFonts(VOID); + +NTSTATUS +EnumerateFonts(DWORD Flags); + +int +FindCreateFont( + __in DWORD Family, + __in_ecount(LF_FACESIZE) LPTSTR ptszFace, + __in COORD Size, + __in LONG Weight, + __in UINT CodePage); + +BOOL +DoFontEnum( + __in_opt HDC hDC, + __in_ecount_opt(LF_FACESIZE) LPTSTR ptszFace, + __in_ecount_opt(nTTPoints) PSHORT pTTPoints, + __in UINT nTTPoints); + +NTSTATUS GetTTFontFaceForCodePage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pszFaceName, + const size_t cchFaceName); + +bool IsFontSizeCustom(__in PCWSTR pwszFaceName, __in const SHORT sSize); +void CreateSizeForAllTTFonts(__in const SHORT sSize); + +#endif /* !FONT_H */ diff --git a/src/propsheet/fontdlg.cpp b/src/propsheet/fontdlg.cpp new file mode 100644 index 000000000..cbfd12951 --- /dev/null +++ b/src/propsheet/fontdlg.cpp @@ -0,0 +1,1513 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + fontdlg.dlg + +Abstract: + + This module contains the code for console font dialog + +Author: + + Therese Stowell (thereses) Feb-3-1992 (swiped from Win3.1) + +Revision History: + +--*/ + +#include "precomp.h" +#pragma hdrstop +#include "fontdlg.h" + +#define DEFAULT_TT_FONT_FACENAME L"__DefaultTTFont__" + +/* ----- Prototypes ----- */ + +int FontListCreate( + __in HWND hDlg, + __in_ecount_opt(LF_FACESIZE) LPWSTR pwszTTFace, + __in BOOL bNewFaceList); + +BOOL PreviewUpdate( + HWND hDlg, + BOOL bLB); + +int SelectCurrentSize( + HWND hDlg, + BOOL bLB, + int FontIndex); + +BOOL PreviewInit( + HWND hDlg); + +VOID DrawItemFontList(const HWND hDlg, const LPDRAWITEMSTRUCT lpdis); + +void RecreateFontHandles(const HWND hWnd); + +/* ----- Globals ----- */ + +HBITMAP hbmTT = NULL; // handle of TT logo bitmap +BITMAP bmTT; // attributes of TT source bitmap + +BOOL gbPointSizeError = FALSE; +BOOL gbBold = FALSE; +BOOL gbUserChoseBold = FALSE; // TRUE if bold state was due to user choice + +BOOL +SelectCurrentFont( + HWND hDlg, + int FontIndex + ); + +// Globals strings loaded from resource +WCHAR wszSelectedFont[CCH_SELECTEDFONT + 1]; +WCHAR wszRasterFonts[CCH_RASTERFONTS + 1]; + +UINT GetItemHeight(const HWND hDlg) +{ + // Load the TrueType logo bitmap + if (hbmTT != NULL) + { + DeleteObject(hbmTT); + hbmTT = NULL; + } + + hbmTT = LoadBitmap(ghInstance, MAKEINTRESOURCE(BM_TRUETYPE_ICON)); + GetObject(hbmTT, sizeof(BITMAP), &bmTT); + + // Compute the height of face name listbox entries + HDC hDC = GetDC(hDlg); + HFONT hFont = GetWindowFont(hDlg); + if (hFont) + { + hFont = (HFONT) SelectObject(hDC, hFont); + } + TEXTMETRIC tm; + GetTextMetrics(hDC, &tm); + if (hFont) + { + SelectObject(hDC, hFont); + } + ReleaseDC(hDlg, hDC); + return max(tm.tmHeight, bmTT.bmHeight); +} + +// The V1 console doesn't support arbitrary TTF fonts, so only allow the enumeration of all TT fonts in the conditions below: +BOOL ShouldAllowAllMonoTTFonts() +{ + return (gpStateInfo->fIsV2Console || // allow if connected to a v2 conhost or + (gpStateInfo->Defaults && g_fForceV2)); // we're in defaults and v2 is turned on +} + +// Given pwszTTFace and optional pwszAltTTFace, determine if the font is only available in bold weights +BOOL IsBoldOnlyTTFont(_In_ PCWSTR pwszTTFace, _In_opt_ PCWSTR pwszAltTTFace) +{ + BOOL fFoundNormalWeightFont = FALSE; + + for (ULONG i = 0; i < NumberOfFonts; i++) + { + // only care about truetype fonts + if (!TM_IS_TT_FONT(FontInfo[i].Family)) + { + continue; + } + + // only care about fonts in the correct charset + if (g_fEastAsianSystem) + { + if (!IS_DBCS_OR_OEM_CHARSET(FontInfo[i].tmCharSet)) + { + continue; + } + } + else + { + if (IS_DBCS_OR_OEM_CHARSET(FontInfo[i].tmCharSet)) + { + continue; + } + } + + // only care if this TT font's name matches + if ((0 != lstrcmp(FontInfo[i].FaceName, pwszTTFace)) && // wrong face name and + (pwszAltTTFace != NULL || // either pwszAltTTFace is NULL or + (0 != lstrcmp(FontInfo[i].FaceName, pwszAltTTFace)))) // pwszAltTTFace is wrong too + { + // A TrueType font, but not the one we're interested in + continue; + } + + // the current entry is one of the entries that we care about. is it non-bold? + if (!IS_BOLD(FontInfo[i].Weight)) + { + // yes, non-bold. note it as such and bail. + fFoundNormalWeightFont = TRUE; + break; + } + } + + return !fFoundNormalWeightFont; +} + +// Given a handle to our dialog: +// 1. Get currently entered font size +// 2. Check to see if the size is a valid custom size +// 3. If the size is custom, add it to the points size list +static void AddCustomFontSizeToListIfNeeded(__in const HWND hDlg) +{ + WCHAR wszBuf[3]; // only need space for point sizes. the max we allow is "72" + + // check to see if we have text + if (GetDlgItemText(hDlg, IDD_POINTSLIST, wszBuf, ARRAYSIZE(wszBuf)) > 0) + { + // we have text, now retrieve it as an actual size + BOOL fTranslated; + const SHORT nPointSize = (SHORT)GetDlgItemInt(hDlg, IDD_POINTSLIST, &fTranslated, TRUE); + if (fTranslated && + nPointSize >= MIN_PIXEL_HEIGHT && + nPointSize <= MAX_PIXEL_HEIGHT && + IsFontSizeCustom(gpStateInfo->FaceName, nPointSize)) + { + // we got a proper custom size. let's see if it's in our point size list + LONG iSize = (LONG)SendDlgItemMessage(hDlg, IDD_POINTSLIST, CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)wszBuf); + if (iSize == CB_ERR) + { + // the size isn't in our list, so we haven't created them yet. do so now. + CreateSizeForAllTTFonts(nPointSize); + + // add the size to the dialog list and select it + iSize = (LONG)SendDlgItemMessage(hDlg, IDD_POINTSLIST, CB_ADDSTRING, 0, (LPARAM)wszBuf); + SendDlgItemMessage(hDlg, IDD_POINTSLIST, CB_SETCURSEL, iSize, 0); + + // get the current font selection + LONG lCurrentFont = (LONG)SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETCURSEL, 0, 0); + + // now get the current font's face name + WCHAR wszFontFace[LF_FACESIZE]; + SendDlgItemMessage(hDlg, + IDD_FACENAME, + LB_GETTEXT, + (WPARAM)lCurrentFont, + (LPARAM)wszFontFace); + + // now cause the hFont for this face/size combination to get loaded -- we need to do this so that the + // font preview has an hFont with which to render + COORD coordFontSize; + coordFontSize.X = 0; + coordFontSize.Y = nPointSize; + const int iFont = FindCreateFont(FF_MODERN | TMPF_VECTOR | TMPF_TRUETYPE, + wszFontFace, + coordFontSize, + 0, + gpStateInfo->CodePage); + SendDlgItemMessage(hDlg, IDD_POINTSLIST, CB_SETITEMDATA, (WPARAM)iSize, (LPARAM)iFont); + } + } + } +} + +INT_PTR +APIENTRY +FontDlgProc( + HWND hDlg, + UINT wMsg, + WPARAM wParam, + LPARAM lParam + ) + +/*++ + + Dialog proc for the font selection dialog box. + Returns the near offset into the far table of LOGFONT structures. + +--*/ + +{ + HWND hWndFocus; + HWND hWndList; + int FontIndex = g_currentFontIndex; // init to keep compiler happy + BOOL bLB; + + switch (wMsg) { + case WM_INITDIALOG: + /* + * Load the font description strings + */ + LoadString(ghInstance, IDS_RASTERFONT, + wszRasterFonts, ARRAYSIZE(wszRasterFonts)); + DBGFONTS(("wszRasterFonts = \"%ls\"\n", wszRasterFonts)); + + LoadString(ghInstance, IDS_SELECTEDFONT, + wszSelectedFont, ARRAYSIZE(wszSelectedFont)); + DBGFONTS(("wszSelectedFont = \"%ls\"\n", wszSelectedFont)); + + /* Save current font size as dialog window's user data */ + + if (g_fEastAsianSystem) { + SetWindowLongPtr(hDlg, GWLP_USERDATA, + MAKELONG(FontInfo[g_currentFontIndex].tmCharSet, + FontInfo[g_currentFontIndex].Size.Y)); + } else { + SetWindowLongPtr(hDlg, GWLP_USERDATA, + MAKELONG(FontInfo[g_currentFontIndex].Size.X, + FontInfo[g_currentFontIndex].Size.Y)); + } + + if (g_fHostedInFileProperties || gpStateInfo->Defaults) + { + FindFontAndUpdateState(); + } + + // IMPORTANT NOTE: When the propsheet and conhost disagree on a font (e.g. user has switched charsets and forgot + // to change to a more appropriate font), we will fall back to terminal in the propsheet. We refer to + // FontInfo[g_currentFontIndex] below which will either be the user's preference (if appropriate) or terminal. + // This gets set prior to here in ConsolePropertySheet() via the FindCreateFont() function. + // + // Before this change, we referred directly to what was being passed in to gpStateInfo, which might represent an + // inappropriate font. Failure to find the appropriate font would end up causing us to show an incorrect + // combination of "Raster fonts" for the face and the point size list, which is supposed to be for TT. Since the + // correct raster font sizes weren't being listed, we weren't able to select a font, which left us with a blank + // edit box, which ultimately caused us to show the "Point size must be between 5 and 72" dialog, which was very + // annoying. + // + // Don't let this happen again. + + /* Create the list of suitable fonts */ + gbEnumerateFaces = TRUE; + bLB = !TM_IS_TT_FONT(FontInfo[g_currentFontIndex].Family); + + gbBold = IS_BOLD(FontInfo[g_currentFontIndex].Weight); + CheckDlgButton(hDlg, IDD_BOLDFONT, gbBold); + if (gbBold) + { + // if we're bold, we need to figure out if it's because the user chose this font or if it's because the font + // is only available in bold + if (IsBoldOnlyTTFont(FontInfo[g_currentFontIndex].FaceName, NULL)) + { + // Bold-only TT font, disable the bold checkbox + EnableWindow(GetDlgItem(hDlg, IDD_BOLDFONT), FALSE); + } + else + { + // Bold was a user choice. Leave the bold checkbox enabled, and keep track of the fact that the user + // chose this. + gbUserChoseBold = TRUE; + } + } + + FontListCreate(hDlg, + bLB ? NULL : FontInfo[g_currentFontIndex].FaceName, + TRUE); + + /* Initialize the preview window - selects current face & size too */ + bLB = PreviewInit(hDlg); + PreviewUpdate(hDlg, bLB); + + /* Make sure the list box has the focus */ + hWndList = GetDlgItem(hDlg, bLB ? IDD_PIXELSLIST : IDD_POINTSLIST); + SetFocus(hWndList); + break; + + case WM_FONTCHANGE: + gbEnumerateFaces = TRUE; + bLB = !TM_IS_TT_FONT(gpStateInfo->FontFamily); + FontListCreate(hDlg, NULL, TRUE); + FontIndex = FindCreateFont(gpStateInfo->FontFamily, + gpStateInfo->FaceName, + gpStateInfo->FontSize, + gpStateInfo->FontWeight, + gpStateInfo->CodePage); + SelectCurrentSize(hDlg, bLB, FontIndex); + return TRUE; + + case WM_PAINT: + if (fChangeCodePage) { + fChangeCodePage = FALSE; + + /* Create the list of suitable fonts */ + bLB = !TM_IS_TT_FONT(gpStateInfo->FontFamily); + FontListCreate(hDlg, + !bLB ? NULL : gpStateInfo->FaceName, + TRUE); + FontIndex = FontListCreate(hDlg, + bLB ? NULL : gpStateInfo->FaceName, + TRUE); + + /* Initialize the preview window - selects current face & size too */ + bLB = PreviewInit(hDlg); + PreviewUpdate(hDlg, bLB); + } + break; + + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case IDD_BOLDFONT: + gbBold = IsDlgButtonChecked(hDlg, IDD_BOLDFONT); + gbUserChoseBold = gbBold; // explicit user action to enable or disable bold. mark it. + UpdateApplyButton(hDlg); + goto RedoFontListAndPreview; + + case IDD_FACENAME: + switch (HIWORD(wParam)) + { + case LBN_SELCHANGE: +RedoFontListAndPreview: + { + // if the font we're switching away from is a bold-only TT font, and the user didn't explicitly ask + // for bold earlier, then unset bold. note that we're depending on the fact that by this point + // FontIndex hasn't yet been updated to refer to the new font that the user selected. + if (IS_BOLD(FontInfo[FontIndex].Weight) && + IsBoldOnlyTTFont(FontInfo[FontIndex].FaceName, NULL) && + !gbUserChoseBold) + { + gbBold = FALSE; + } + + WCHAR atchNewFace[LF_FACESIZE]; + LONG l; + + DBGFONTS(("LBN_SELCHANGE from FACENAME\n")); + l = (LONG)SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETCURSEL, 0, 0L); + bLB = (BOOL)SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETITEMDATA, l, 0L); + if (!bLB) { + SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETTEXT, l, (LPARAM)atchNewFace); + DBGFONTS(("LBN_EDITUPDATE, got TT face \"%ls\"\n", atchNewFace)); + } + FontIndex = FontListCreate(hDlg, + bLB ? NULL : atchNewFace, + FALSE); + SelectCurrentSize(hDlg, bLB, FontIndex); + PreviewUpdate(hDlg, bLB); + UpdateApplyButton(hDlg); + return TRUE; + } + } + break; + + case IDD_POINTSLIST: + switch (HIWORD(wParam)) { + case CBN_SELCHANGE: + DBGFONTS(("CBN_SELCHANGE from POINTSLIST\n")); + PreviewUpdate(hDlg, FALSE); + UpdateApplyButton(hDlg); + return TRUE; + + case CBN_KILLFOCUS: + DBGFONTS(("CBN_KILLFOCUS from POINTSLIST\n")); + if (!gbPointSizeError) { + hWndFocus = GetFocus(); + if (hWndFocus != NULL && IsChild(hDlg, hWndFocus) && + hWndFocus != GetDlgItem(hDlg, IDCANCEL)) { + AddCustomFontSizeToListIfNeeded(hDlg); + PreviewUpdate(hDlg, FALSE); + } + } + return TRUE; + + default: + DBGFONTS(("unhandled CBN_%x from POINTSLIST\n",HIWORD(wParam))); + break; + } + break; + + case IDD_PIXELSLIST: + switch (HIWORD(wParam)) { + case LBN_SELCHANGE: + DBGFONTS(("LBN_SELCHANGE from PIXELSLIST\n")); + PreviewUpdate(hDlg, TRUE); + UpdateApplyButton(hDlg); + return TRUE; + + default: + break; + } + break; + + default: + break; + } + break; + + case WM_NOTIFY: + { + const PSHNOTIFY * const pshn = (LPPSHNOTIFY)lParam; + switch (pshn->hdr.code) { + case PSN_KILLACTIVE: + // + // If the TT combo box is visible, update selection + // + hWndList = GetDlgItem(hDlg, IDD_POINTSLIST); + if (hWndList != NULL && IsWindowVisible(hWndList)) { + if (!PreviewUpdate(hDlg, FALSE)) { + SetDlgMsgResult(hDlg, PSN_KILLACTIVE, TRUE); + return TRUE; + } + SetDlgMsgResult(hDlg, PSN_KILLACTIVE, FALSE); + } + + FontIndex = g_currentFontIndex; + + if (FontInfo[FontIndex].SizeWant.Y == 0) { + // Raster Font, so save actual size + gpStateInfo->FontSize = FontInfo[FontIndex].Size; + } else { + // TT Font, so save desired size + gpStateInfo->FontSize = FontInfo[FontIndex].SizeWant; + } + + gpStateInfo->FontWeight = FontInfo[FontIndex].Weight; + gpStateInfo->FontFamily = FontInfo[FontIndex].Family; + StringCchCopy(gpStateInfo->FaceName, + ARRAYSIZE(gpStateInfo->FaceName), + FontInfo[FontIndex].FaceName); + + return TRUE; + + case PSN_APPLY: + /* + * Write out the state values and exit. + */ + EndDlgPage(hDlg, !pshn->lParam); + return TRUE; + } + break; + } + + /* + * For WM_MEASUREITEM and WM_DRAWITEM, since there is only one + * owner-draw item (combobox) in the entire dialog box, we don't have + * to do a GetDlgItem to figure out who he is. + */ + case WM_MEASUREITEM: + ((LPMEASUREITEMSTRUCT)lParam)->itemHeight = GetItemHeight(hDlg); + return TRUE; + + case WM_DRAWITEM: + DrawItemFontList(hDlg, (LPDRAWITEMSTRUCT)lParam); + return TRUE; + + case WM_DESTROY: + /* + * Delete the TrueType logo bitmap + */ + if (hbmTT != NULL) { + DeleteObject(hbmTT); + hbmTT = NULL; + } + return TRUE; + + case WM_DPICHANGED_BEFOREPARENT: + // DPI has changed -- recreate our font handles to get appropriately scaled fonts + RecreateFontHandles(hDlg); + + // Now reset our item height. This is to work around a limitation of automatic dialog DPI scaling where + // WM_MEASUREITEM doesn't get sent when the DPI has changed. + SendDlgItemMessage(hDlg, IDD_FACENAME, LB_SETITEMHEIGHT, 0, GetItemHeight(hDlg)); + break; + + default: + break; + } + + return FALSE; +} + +// Iterate through all of our fonts to find the font entries that match the desired family, charset, name (TT), and +// boldness (TT). Each entry in FontInfo represents a specific combination of font states. We expect to encounter +// numerous entries for each size/boldness/charset of TT fonts. If fAddBoldFonts is true, we'll add a font even if it's +// bold, regardless of whether the user has chosen bold fonts or not. +void AddFontSizesToList(PCWSTR pwszTTFace, + PCWSTR pwszAltTTFace, + const LONG_PTR dwExStyle, + const BOOL fDbcsCharSet, + const BOOL fRasterFont, + const HWND hWndShow, + const BOOL fAddBoldFonts) +{ + WCHAR wszText[80]; + int iLastShowX = 0; + int iLastShowY = 0; + int nSameSize = 0; + + for (ULONG i = 0; i < NumberOfFonts; i++) { + if (!fRasterFont == !TM_IS_TT_FONT(FontInfo[i].Family)) { + DBGFONTS((" Font %x not right type\n", i)); + continue; + } + if (fDbcsCharSet) { + if (!IS_DBCS_OR_OEM_CHARSET(FontInfo[i].tmCharSet)) { + DBGFONTS((" Font %x not right type for DBCS character set\n", i)); + continue; + } + } else { + if (IS_ANY_DBCS_CHARSET(FontInfo[i].tmCharSet)) { + DBGFONTS((" Font %x not right type for SBCS character set\n", i)); + continue; + } + } + + if (!fRasterFont) { + if ((0 != lstrcmp(FontInfo[i].FaceName, pwszTTFace)) && + (0 != lstrcmp(FontInfo[i].FaceName, pwszAltTTFace))) { + /* + * A TrueType font, but not the one we're interested in, + * so don't add it to the list of point sizes. + */ + DBGFONTS((" Font %x is TT, but not %ls\n", i, pwszTTFace)); + continue; + } + + // if we're being asked to add bold fonts, add unconditionally according to weight. Otherwise, only this + // entry to the list of it's in line with user choice. Raster fonts aren't available in bold. + if (!fAddBoldFonts && gbBold != IS_BOLD(FontInfo[i].Weight)) { + DBGFONTS((" Font %x has weight %d, but we wanted %sbold\n", + i, FontInfo[i].Weight, gbBold ? "" : "not ")); + continue; + } + } + + int ShowX; + if (FontInfo[i].SizeWant.X > 0) { + ShowX = FontInfo[i].SizeWant.X; + } else { + ShowX = FontInfo[i].Size.X; + } + + int ShowY; + if (FontInfo[i].SizeWant.Y > 0) { + ShowY = FontInfo[i].SizeWant.Y; + } else { + ShowY = FontInfo[i].Size.Y; + } + + /* + * Add the size description string to the end of the right list + */ + if (TM_IS_TT_FONT(FontInfo[i].Family)) { + // point size + StringCchPrintf(wszText, + ARRAYSIZE(wszText), + TEXT("%2d"), + FontInfo[i].SizeWant.Y); + } else { + // pixel size + if ((iLastShowX == ShowX) && (iLastShowY == ShowY)) { + nSameSize++; + } else { + iLastShowX = ShowX; + iLastShowY = ShowY; + nSameSize = 0; + } + + /* + * The number nSameSize is appended to the string to distinguish + * between Raster fonts of the same size. It is not intended to + * be visible and exists off the edge of the list + */ + if (((dwExStyle & WS_EX_RIGHT) && !(dwExStyle & WS_EX_LAYOUTRTL)) || + (!(dwExStyle & WS_EX_RIGHT) && (dwExStyle & WS_EX_LAYOUTRTL))) { + // flip it so that the hidden part be at the far left + StringCchPrintf(wszText, + ARRAYSIZE(wszText), + TEXT("#%d %2d x %2d"), + nSameSize, + ShowX, + ShowY); + } else { + StringCchPrintf(wszText, + ARRAYSIZE(wszText), + TEXT("%2d x %2d #%d"), + ShowX, + ShowY, + nSameSize); + } + } + + LONG lListIndex = lcbFINDSTRINGEXACT(hWndShow, fRasterFont, wszText); + if (lListIndex == LB_ERR) { + lListIndex = lcbADDSTRING(hWndShow, fRasterFont, wszText); + } + DBGFONTS((" added %ls to %sSLIST(%p) index %lx\n", + wszText, + fRasterFont ? "PIXEL" : "POINT", + hWndShow, lListIndex)); + lcbSETITEMDATA(hWndShow, fRasterFont, (DWORD)lListIndex, i); + } +} + +/*++ + + Initializes the font list by enumerating all fonts and picking the + proper ones for our list. + + Returns + FontIndex of selected font (LB_ERR if none) +--*/ +int +FontListCreate( + __in HWND hDlg, + __in_ecount_opt(LF_FACESIZE) LPWSTR pwszTTFace, + __in BOOL bNewFaceList) +{ + LONG lListIndex; + ULONG i; + HWND hWndShow; // List or Combo box + HWND hWndHide; // Combo or List box + HWND hWndFaceCombo; + BOOL bLB; + UINT CodePage = gpStateInfo->CodePage; + BOOL fFindTTFont = FALSE; + LPWSTR pwszAltTTFace; + LONG_PTR dwExStyle; + + FAIL_FAST_IF(!(OEMCP != 0)); // must be initialized + + bLB = ((pwszTTFace == nullptr) || (pwszTTFace[0] == TEXT('\0'))); + if (bLB) { + pwszAltTTFace = NULL; + } else { + if (ShouldAllowAllMonoTTFonts() || IsAvailableTTFont(pwszTTFace)) { + pwszAltTTFace = GetAltFaceName(pwszTTFace); + } else { + pwszAltTTFace = pwszTTFace; + } + } + + DBGFONTS(("FontListCreate %p, %s, %s new FaceList\n", hDlg, + bLB ? "Raster" : "TrueType", + bNewFaceList ? "Make" : "No" )); + + /* + * This only enumerates face names and font sizes if necessary. + */ + EnumerateFonts(bLB ? EF_OEMFONT : EF_TTFONT); + + /* init the TTFaceNames */ + + DBGFONTS((" Create %s fonts\n", bLB ? "Raster" : "TrueType")); + + if (bNewFaceList) { + PFACENODE panFace; + hWndFaceCombo = GetDlgItem(hDlg, IDD_FACENAME); + + // empty faces list + SendMessage(hWndFaceCombo, LB_RESETCONTENT, 0, 0); + + // before doing anything else, add raster fonts to the list. Note that the item data being set here indicates + // that it's a raster font. the actual font indices are stored as item data on the pixels list. + lListIndex = (LONG)SendMessage(hWndFaceCombo, LB_ADDSTRING, 0, (LPARAM)wszRasterFonts); + SendMessage(hWndFaceCombo, LB_SETITEMDATA, lListIndex, TRUE); + DBGFONTS(("Added \"%ls\", set Item Data %d = TRUE\n", wszRasterFonts, lListIndex)); + + // now enumerate all of the new truetype font face names we've loaded that are appropriate for our codepage. add them to + // the faces list. if we find an exact match for pwszTTFace or pwszAltTTFace, note that in fFindTTFont. + for (panFace = gpFaceNames; panFace; panFace = panFace->pNext) { + if ((panFace->dwFlag & (EF_TTFONT|EF_NEW)) != (EF_TTFONT|EF_NEW)) { + continue; + } + if (!g_fEastAsianSystem && (panFace->dwFlag & EF_DBCSFONT)) { + continue; + } + + // NOTE: in v2 we don't depend on the registry list to determine if a TT font should be listed in the font + // face dialog list -- this is handled in DoFontEnum by using the FontEnumForV2Console enumerator + if (ShouldAllowAllMonoTTFonts() || + (g_fEastAsianSystem && IsAvailableTTFontCP(panFace->atch, CodePage)) || + (!g_fEastAsianSystem && IsAvailableTTFontCP(panFace->atch, 0))) { + + if (!bLB && + (lstrcmp(pwszTTFace, panFace->atch) == 0 || + lstrcmp(pwszAltTTFace, panFace->atch) == 0)) { + fFindTTFont = TRUE; + } + + lListIndex = (LONG)SendMessage(hWndFaceCombo, LB_ADDSTRING, 0, (LPARAM)panFace->atch); + SendMessage(hWndFaceCombo, LB_SETITEMDATA, lListIndex, FALSE); + DBGFONTS(("Added \"%ls\", set Item Data %d = FALSE\n", + panFace->atch, lListIndex)); + } + } + + // if we haven't found the specific TT font we're looking for, choose *any* TT font that's appropriate for our + // codepage + if (!bLB && !fFindTTFont) { + for (panFace = gpFaceNames; panFace; panFace = panFace->pNext) { + if ((panFace->dwFlag & (EF_TTFONT | EF_NEW)) != (EF_TTFONT | EF_NEW)) { + continue; + } + + if (!g_fEastAsianSystem && (panFace->dwFlag & EF_DBCSFONT)) { + continue; + } + + if (( g_fEastAsianSystem && IsAvailableTTFontCP(panFace->atch, CodePage)) || + (!g_fEastAsianSystem && IsAvailableTTFontCP(panFace->atch, 0))) + { + + if (lstrcmp(pwszTTFace, panFace->atch) != 0) { + // found a reasonably appropriate font that isn't the one being requested (we couldn't find that + // one). use this one instead. + StringCchCopy(pwszTTFace, + LF_FACESIZE, + panFace->atch); + break; + } + } + } + } + } + + // update the state of the bold checkbox. check the box if the currently selected TT font is bold. some TT fonts + // aren't allowed to be bold depending on the charset. also, raster fonts aren't allowed to be bold. + hWndShow = GetDlgItem(hDlg, IDD_BOLDFONT); + + /* + * For JAPAN, We uses "MS Gothic" TT font. + * So, Bold of this font is not 1:2 width between SBCS:DBCS. + */ + if (g_fEastAsianSystem && IsDisableBoldTTFont(pwszTTFace)) { + EnableWindow(hWndShow, FALSE); + gbBold = FALSE; + CheckDlgButton(hDlg, IDD_BOLDFONT, FALSE); + } else { + CheckDlgButton(hDlg, IDD_BOLDFONT, (bLB || !gbBold) ? FALSE : TRUE); + EnableWindow(hWndShow, bLB ? FALSE : TRUE); + } + + // if the current font is raster, disable and hide the point size list. + // if the current font is TT, disable and hide the pixel size list. + hWndHide = GetDlgItem(hDlg, bLB ? IDD_POINTSLIST : IDD_PIXELSLIST); + ShowWindow(hWndHide, SW_HIDE); + EnableWindow(hWndHide, FALSE); + + // if the current font is raster, enable and show the pixel size list. + // if the current font is TT, enable and show the point size list. + hWndShow = GetDlgItem(hDlg, bLB ? IDD_PIXELSLIST : IDD_POINTSLIST); + ShowWindow(hWndShow, SW_SHOW); + EnableWindow(hWndShow, TRUE); + + // if we're building a new face list (basically any time we're not handling a selection change), empty the contents + // of the pixel size list (raster) or point size list (TT) as appropriate. + if (bNewFaceList) { + lcbRESETCONTENT(hWndShow, bLB); + } + + dwExStyle = GetWindowLongPtr(hWndShow, GWL_EXSTYLE); + if ((dwExStyle & WS_EX_LAYOUTRTL) && !(dwExStyle & WS_EX_RTLREADING)) { + // if mirrored RTL Reading means LTR !! + SetWindowLongPtr(hWndShow, GWL_EXSTYLE, dwExStyle | WS_EX_RTLREADING); + } + + /* Initialize hWndShow list/combo box */ + + const BOOL fIsBoldOnlyTTFont = (!bLB && IsBoldOnlyTTFont(pwszTTFace, pwszAltTTFace)); + + AddFontSizesToList(pwszTTFace, + pwszAltTTFace, + dwExStyle, + g_fEastAsianSystem, + bLB, + hWndShow, + fIsBoldOnlyTTFont); + + if (fIsBoldOnlyTTFont) + { + // since this is a bold-only font, check and disable the bold checkbox + EnableWindow(GetDlgItem(hDlg, IDD_BOLDFONT), FALSE); + CheckDlgButton(hDlg, IDD_BOLDFONT, TRUE); + } + + /* + * Get the FontIndex from the currently selected item. + * (i will be LB_ERR if no currently selected item). + */ + lListIndex = lcbGETCURSEL(hWndShow, bLB); + i = lcbGETITEMDATA(hWndShow, bLB, lListIndex); + + DBGFONTS(("FontListCreate returns 0x%x\n", i)); + FAIL_FAST_IF(!(i == LB_ERR || (ULONG)i < NumberOfFonts)); + return i; +} + + +/** DrawItemFontList + * + * Answer the WM_DRAWITEM message sent from the font list box or + * facename list box. + * + * Entry: + * lpdis -> DRAWITEMSTRUCT describing object to be drawn + * + * Returns: + * None. + * + * The object is drawn. + */ +VOID DrawItemFontList(const HWND hDlg, const LPDRAWITEMSTRUCT lpdis) +{ + HDC hDC, hdcMem; + DWORD rgbBack, rgbText, rgbFill; + WCHAR wszFace[LF_FACESIZE]; + HBITMAP hOld; + int dy, dxttbmp; + HBRUSH hbrFill; + HWND hWndItem; + BOOL bLB; + + if ((int)lpdis->itemID < 0) { + return; + } + + hDC = lpdis->hDC; + + if (lpdis->itemAction & ODA_FOCUS) { + if (lpdis->itemState & ODS_SELECTED) { + DrawFocusRect(hDC, &lpdis->rcItem); + } + } else { + if (lpdis->itemState & ODS_SELECTED) { + rgbText = SetTextColor(hDC, GetSysColor(COLOR_HIGHLIGHTTEXT)); + rgbBack = SetBkColor(hDC, rgbFill = GetSysColor(COLOR_HIGHLIGHT)); + } else { + rgbText = SetTextColor(hDC, GetSysColor(COLOR_WINDOWTEXT)); + rgbBack = SetBkColor(hDC, rgbFill = GetSysColor(COLOR_WINDOW)); + } + // draw selection background + hbrFill = CreateSolidBrush(rgbFill); + if (hbrFill) { + FillRect(hDC, &lpdis->rcItem, hbrFill); + DeleteObject(hbrFill); + } + + // get the string + hWndItem = lpdis->hwndItem; + if (!IsWindow(hWndItem)) { + return; + } + + /* + * This line is here mostly to quiet prefast, which expects a + * LB_GETTEXTLEN to be sent before a LB_GETTEXT. However, this call + * is useless for three reasons: First, the size can change between + * the two calls, so the return value is useless. Second, since these + * are fonts their lengths are the same. Third, a buffer overrun here + * isn't really interesting from a security perspective, anyway. + */ + if (SendMessage(hWndItem, LB_GETTEXTLEN, lpdis->itemID, 0) >= ARRAYSIZE(wszFace)) { + return; + } + + SendMessage(hWndItem, LB_GETTEXT, lpdis->itemID, (LPARAM)wszFace); + bLB = (BOOL)SendMessage(hWndItem, LB_GETITEMDATA, lpdis->itemID, 0L); + dxttbmp = bLB ? 0 : bmTT.bmWidth; + + DBGFONTS(("DrawItemFontList must redraw \"%ls\" %s\n", wszFace, + bLB ? "Raster" : "TrueType")); + + // draw the text + TabbedTextOut(hDC, lpdis->rcItem.left + dxttbmp, + lpdis->rcItem.top, wszFace, + (UINT)wcslen(wszFace), 0, NULL, dxttbmp); + + // and the TT bitmap if needed + if (!bLB) { + hdcMem = CreateCompatibleDC(hDC); + if (hdcMem) { + hOld = (HBITMAP) SelectObject(hdcMem, hbmTT); + + dy = ((lpdis->rcItem.bottom - lpdis->rcItem.top) - bmTT.bmHeight) / 2; + + BitBlt(hDC, lpdis->rcItem.left, lpdis->rcItem.top + dy, + dxttbmp, GetItemHeight(hDlg), hdcMem, + 0, 0, SRCINVERT); + + if (hOld) + SelectObject(hdcMem, hOld); + DeleteDC(hdcMem); + } + } + + SetTextColor(hDC, rgbText); + SetBkColor(hDC, rgbBack); + + if (lpdis->itemState & ODS_FOCUS) { + DrawFocusRect(hDC, &lpdis->rcItem); + } + } +} + + +UINT +GetPointSizeInRange( + HWND hDlg, + INT Min, + INT Max) +/*++ + +Routine Description: + + Get a size from the Point Size ComboBox edit field + +Return Value: + + Point Size - of the edit field limited by Min/Max size + 0 - if the field is empty or invalid + +--*/ + +{ + WCHAR szBuf[90]; + int nTmp = 0; + BOOL bOK; + + if (GetDlgItemText(hDlg, IDD_POINTSLIST, szBuf, ARRAYSIZE(szBuf))) { + nTmp = GetDlgItemInt(hDlg, IDD_POINTSLIST, &bOK, TRUE); + if (bOK && nTmp >= Min && nTmp <= Max) { + return nTmp; + } + } + + return 0; +} + + +/* ----- Preview routines ----- */ + +LRESULT +FontPreviewWndProc( + HWND hWnd, + UINT wMessage, + WPARAM wParam, + LPARAM lParam + ) + +/* FontPreviewWndProc + * Handles the font preview window + */ + +{ + PAINTSTRUCT ps; + RECT rect; + HFONT hfontOld; + HBRUSH hbrNew; + HBRUSH hbrOld; + COLORREF rgbText; + COLORREF rgbBk; + + switch (wMessage) { + case WM_ERASEBKGND: + break; + + case WM_PAINT: + BeginPaint(hWnd, &ps); + + /* Draw the font sample */ + if (GetWindowLong(hWnd, GWL_ID) == IDD_COLOR_POPUP_COLORS) { + rgbText = GetNearestColor(ps.hdc, PopupTextColor(gpStateInfo)); + rgbBk = GetNearestColor(ps.hdc, PopupBkColor(gpStateInfo)); + } else { + rgbText = GetNearestColor(ps.hdc, ScreenTextColor(gpStateInfo)); + rgbBk = GetNearestColor(ps.hdc, ScreenBkColor(gpStateInfo)); + } + SetTextColor(ps.hdc, rgbText); + SetBkColor(ps.hdc, rgbBk); + GetClientRect(hWnd, &rect); + hfontOld = (HFONT) SelectObject(ps.hdc, FontInfo[g_currentFontIndex].hFont); + hbrNew = CreateSolidBrush(rgbBk); + hbrOld = (HBRUSH) SelectObject(ps.hdc, hbrNew); + PatBlt(ps.hdc, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + PATCOPY); + InflateRect(&rect, -2, -2); + DrawText(ps.hdc, g_szPreviewText, -1, &rect, 0); + SelectObject(ps.hdc, hbrOld); + DeleteObject(hbrNew); + SelectObject(ps.hdc, hfontOld); + + EndPaint(hWnd, &ps); + break; + + case WM_DPICHANGED: + // DPI has changed -- recreate our font handles to get appropriately scaled fonts + RecreateFontHandles(hWnd); + break; + + default: + return DefWindowProc(hWnd, wMessage, wParam, lParam); + } + return 0L; +} + + +/* + * Get the font index for a new font + * If necessary, attempt to create the font. + * Always return a valid FontIndex (even if not correct) + * Family: Find/Create a font with of this Family + * 0 - don't care + * pwszFace: Find/Create a font with this face name. + * NULL or TEXT("") - use DefaultFaceName + * Size: Must match SizeWant or actual Size. + */ +int +FindCreateFont( + __in DWORD Family, + __in_ecount(LF_FACESIZE) LPWSTR pwszFace, + __in COORD Size, + __in LONG Weight, + __in UINT CodePage) +{ +#define NOT_CREATED_NOR_FOUND -1 +#define CREATED_BUT_NOT_FOUND -2 + + int FontIndex = NOT_CREATED_NOR_FOUND; + BOOL bFontOK; + WCHAR AltFaceName[LF_FACESIZE]; + COORD AltFontSize; + BYTE AltFontFamily; + ULONG AltFontIndex = 0, i; + LPWSTR pwszAltFace; + + BYTE CharSet = CodePageToCharSet(CodePage); + + FAIL_FAST_IF(!(OEMCP != 0)); + + DBGFONTS(("FindCreateFont Family=%x %ls (%d,%d) %d %d %x\n", + Family, pwszFace, Size.X, Size.Y, Weight, CodePage, CharSet)); + + if (g_fEastAsianSystem) { + if (IS_DBCS_OR_OEM_CHARSET(CharSet)) { + if (pwszFace == NULL || *pwszFace == TEXT('\0')) { + pwszFace = DefaultFaceName; + } + if (Size.Y == 0) { + Size = DefaultFontSize; + } + } else { + MakeAltRasterFont(CodePage, &AltFontSize, &AltFontFamily, &AltFontIndex, AltFaceName); + + if (pwszFace == NULL || *pwszFace == TEXT('\0')) { + pwszFace = AltFaceName; + } + if (Size.Y == 0) { + Size.X = AltFontSize.X; + Size.Y = AltFontSize.Y; + } + } + } else { + if (pwszFace == NULL || *pwszFace == TEXT('\0')) { + pwszFace = DefaultFaceName; + } + if (Size.Y == 0) { + Size = DefaultFontSize; + } + } + + // If _DefaultTTFont_ is specified, find the appropriate face name for our current codepage. + if (wcscmp(pwszFace, DEFAULT_TT_FONT_FACENAME) == 0) + { + // retrieve default font face name for this codepage, and then set it as our current face + WCHAR szDefaultCodepageTTFont[LF_FACESIZE] = {0}; + if (NT_SUCCESS(GetTTFontFaceForCodePage(CodePage, szDefaultCodepageTTFont, ARRAYSIZE(szDefaultCodepageTTFont))) && + NT_SUCCESS(StringCchCopyW(DefaultTTFaceName, ARRAYSIZE(DefaultTTFaceName), szDefaultCodepageTTFont))) + { + pwszFace = DefaultTTFaceName; + Size.X = 0; + } + } + + if (ShouldAllowAllMonoTTFonts() || IsAvailableTTFont(pwszFace)) { + pwszAltFace = GetAltFaceName(pwszFace); + } else { + pwszAltFace = pwszFace; + } + + /* + * Try to find the exact font + */ +TryFindExactFont: + for (i = 0; i < NumberOfFonts; i++) { + /* + * If looking for a particular Family, skip non-matches + */ + if (Family != 0 && (BYTE)Family != FontInfo[i].Family) { + continue; + } + + /* + * Skip non-matching sizes + */ + if ((FontInfo[i].SizeWant.Y != Size.Y) && + !SIZE_EQUAL(FontInfo[i].Size, Size)) { + continue; + } + + /* + * Skip non-matching weights + */ + if ((Weight != 0) && (Weight != FontInfo[i].Weight)) { + continue; + } + + if (!TM_IS_TT_FONT(FontInfo[i].Family) && + (FontInfo[i].tmCharSet != CharSet && + !(FontInfo[i].tmCharSet == OEM_CHARSET && g_fEastAsianSystem))) { + continue; + } + + /* + * Size (and maybe Family) match. If we don't care about the name or + * if it matches, use this font. Otherwise, if name doesn't match and + * it is a raster font, consider it. + */ + if ((pwszFace == NULL) || (pwszFace[0] == TEXT('\0')) || + (lstrcmp(FontInfo[i].FaceName, pwszFace) == 0) || + (lstrcmp(FontInfo[i].FaceName, pwszAltFace) == 0)) { + FontIndex = i; + goto FoundFont; + } else if (!TM_IS_TT_FONT(FontInfo[i].Family)) { + FontIndex = i; + } + } + + if (FontIndex == NOT_CREATED_NOR_FOUND) { + /* + * Didn't find the exact font, so try to create it + */ + if (Size.Y < 0) { + Size.Y = -Size.Y; + } + bFontOK = DoFontEnum(NULL, pwszFace, &Size.Y, 1); + if (bFontOK) { + DBGFONTS(("FindCreateFont created font!\n")); + FontIndex = CREATED_BUT_NOT_FOUND; + goto TryFindExactFont; + } else { + DBGFONTS(("FindCreateFont failed to create font!\n")); + } + } else if (FontIndex >= 0) { + // a close Raster Font fit - only the name doesn't match. + goto FoundFont; + } + + /* + * Failed to find exact match, even after enumeration, so now try to find + * a font of same family and same size or bigger. + */ + for (i = 0; i < NumberOfFonts; i++) { + if (g_fEastAsianSystem) { + if (Family != 0 && (BYTE)Family != FontInfo[i].Family) { + continue; + } + + if (!TM_IS_TT_FONT(FontInfo[i].Family) && + FontInfo[i].tmCharSet != CharSet) { + continue; + } + } else { + if ((BYTE)Family != FontInfo[i].Family) { + continue; + } + } + + if (FontInfo[i].Size.Y >= Size.Y && FontInfo[i].Size.X >= Size.X) { + // Same family, size >= desired. + FontIndex = i; + break; + } + } + + if (FontIndex < 0) { + DBGFONTS(("FindCreateFont defaults!\n")); + if (g_fEastAsianSystem) { + if (CodePage == OEMCP) { + FontIndex = DefaultFontIndex; + } else { + FontIndex = AltFontIndex; + } + } else { + FontIndex = DefaultFontIndex; + } + } + +FoundFont: + FAIL_FAST_IF(!(FontIndex < (int)NumberOfFonts)); + DBGFONTS(("FindCreateFont returns %x : %ls (%d,%d)\n", FontIndex, + FontInfo[FontIndex].FaceName, + FontInfo[FontIndex].Size.X, FontInfo[FontIndex].Size.Y)); + return FontIndex; + +#undef NOT_CREATED_NOR_FOUND +#undef CREATED_BUT_NOT_FOUND +} + +/* + * SelectCurrentSize - Select the right line of the Size listbox/combobox. + * bLB : Size controls is a listbox (TRUE for RasterFonts) + * FontIndex : Index into FontInfo[] cache + * If < 0 then choose a good font. + * Returns + * FontIndex : Index into FontInfo[] cache + */ +int +SelectCurrentSize(HWND hDlg, BOOL bLB, int FontIndex) +{ + int iCB; + HWND hWndList; + + DBGFONTS(("SelectCurrentSize %p %s %x\n", + hDlg, bLB ? "Raster" : "TrueType", FontIndex)); + + hWndList = GetDlgItem(hDlg, bLB ? IDD_PIXELSLIST : IDD_POINTSLIST); + iCB = lcbGETCOUNT(hWndList, bLB); + DBGFONTS((" Count of items in %p = %lx\n", hWndList, iCB)); + + if (FontIndex >= 0) { + /* + * look for FontIndex + */ + while (iCB > 0) { + iCB--; + if (lcbGETITEMDATA(hWndList, bLB, iCB) == FontIndex) { + lcbSETCURSEL(hWndList, bLB, iCB); + break; + } + } + } else { + /* + * Look for a reasonable default size: looking backwards, find + * the first one same height or smaller. + */ + DWORD Size = GetWindowLong(hDlg, GWLP_USERDATA); + + if (g_fEastAsianSystem && + bLB && + FontInfo[g_currentFontIndex].tmCharSet != LOBYTE(LOWORD(Size))) { + WCHAR AltFaceName[LF_FACESIZE]; + COORD AltFontSize; + BYTE AltFontFamily; + ULONG AltFontIndex = 0; + + MakeAltRasterFont(gpStateInfo->CodePage, + &AltFontSize, + &AltFontFamily, + &AltFontIndex, + AltFaceName); + + while (iCB > 0) { + iCB--; + if (lcbGETITEMDATA(hWndList, bLB, iCB) == (int)AltFontIndex) { + lcbSETCURSEL(hWndList, bLB, iCB); + break; + } + } + } else { + while (iCB > 0) { + iCB--; + FontIndex = lcbGETITEMDATA(hWndList, bLB, iCB); + if (FontInfo[FontIndex].Size.Y <= HIWORD(Size)) { + lcbSETCURSEL(hWndList, bLB, iCB); + break; + } + } + } + } + + DBGFONTS(("SelectCurrentSize returns 0x%x\n", FontIndex)); + return FontIndex; +} + + +BOOL +SelectCurrentFont( + HWND hDlg, + int FontIndex) +{ + BOOL bLB; + + DBGFONTS(("SelectCurrentFont hDlg=%p, FontIndex=%x\n", hDlg, FontIndex)); + + bLB = !TM_IS_TT_FONT(FontInfo[FontIndex].Family); + + SendDlgItemMessage(hDlg, + IDD_FACENAME, + LB_SELECTSTRING, + (WPARAM)-1, + bLB ? (LPARAM)wszRasterFonts : (LPARAM)FontInfo[FontIndex].FaceName); + + SelectCurrentSize(hDlg, bLB, FontIndex); + return bLB; +} + + +BOOL +PreviewInit( + HWND hDlg + ) + +/* PreviewInit + * Prepares the preview code, sizing the window and the dialog to + * make an attractive preview. + * Returns TRUE if Raster Fonts, FALSE if TT Font + */ + +{ + int nFont; + + DBGFONTS(("PreviewInit hDlg=%p\n", hDlg)); + + /* + * Set the current font + */ + nFont = FindCreateFont(gpStateInfo->FontFamily, + gpStateInfo->FaceName, + gpStateInfo->FontSize, + gpStateInfo->FontWeight, + gpStateInfo->CodePage); + + DBGFONTS(("Changing Font Number from %d to %d\n", + g_currentFontIndex, nFont)); + FAIL_FAST_IF(!((ULONG)nFont < NumberOfFonts)); + g_currentFontIndex = nFont; + + if (g_fHostedInFileProperties) + { + gpStateInfo->FontFamily = FontInfo[g_currentFontIndex].Family; + gpStateInfo->FontSize = FontInfo[g_currentFontIndex].Size; + gpStateInfo->FontWeight = FontInfo[g_currentFontIndex].Weight; + StringCchCopyW(gpStateInfo->FaceName, ARRAYSIZE(gpStateInfo->FaceName), FontInfo[g_currentFontIndex].FaceName); + } + + return SelectCurrentFont(hDlg, nFont); +} + + +BOOL +PreviewUpdate( + HWND hDlg, + BOOL bLB + ) + +/*++ + + Does the preview of the selected font. + +--*/ + +{ + PFONT_INFO lpFont; + int FontIndex; + LONG lIndex; + HWND hWnd; + WCHAR wszText[60]; + WCHAR wszFace[LF_FACESIZE + CCH_SELECTEDFONT]; + HWND hWndList; + DWORD_PTR Parameters[2]; + + DBGFONTS(("PreviewUpdate hDlg=%p, %s\n", hDlg, + bLB ? "Raster" : "TrueType")); + + hWndList = GetDlgItem(hDlg, bLB ? IDD_PIXELSLIST : IDD_POINTSLIST); + + /* When we select a font, we do the font preview by setting it into + * the appropriate list box + */ + lIndex = lcbGETCURSEL(hWndList, bLB); + DBGFONTS(("PreviewUpdate GETCURSEL gets %x\n", lIndex)); + if ((lIndex < 0) && !bLB) { + COORD NewSize; + + lIndex = (LONG)SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETCURSEL, 0, 0L); + SendDlgItemMessage(hDlg, IDD_FACENAME, LB_GETTEXT, lIndex, (LPARAM)wszFace); + NewSize.X = 0; + NewSize.Y = (SHORT)GetPointSizeInRange(hDlg, MIN_PIXEL_HEIGHT, MAX_PIXEL_HEIGHT); + + if (NewSize.Y == 0) { + WCHAR wszBuf[60]; + /* + * Use wszText, wszBuf to put up an error msg for bad point size + */ + gbPointSizeError = TRUE; + LoadString(ghInstance, IDS_FONTSIZE, wszBuf, ARRAYSIZE(wszBuf)); + StringCchPrintf(wszText, + ARRAYSIZE(wszText), + wszBuf, + MIN_PIXEL_HEIGHT, + MAX_PIXEL_HEIGHT); + + GetWindowText(hDlg, wszBuf, ARRAYSIZE(wszBuf)); + MessageBox(hDlg, wszText, wszBuf, MB_OK|MB_ICONINFORMATION); + SetFocus(hWndList); + gbPointSizeError = FALSE; + return FALSE; + } + + FontIndex = FindCreateFont(FF_MODERN | TMPF_VECTOR | TMPF_TRUETYPE, + wszFace, + NewSize, + 0, + gpStateInfo->CodePage); + } else { + FontIndex = lcbGETITEMDATA(hWndList, bLB, lIndex); + } + + if (FontIndex < 0) { + FontIndex = DefaultFontIndex; + } + + /* + * If we've selected a new font, tell the property sheet we've changed + */ + FAIL_FAST_IF(!((ULONG)FontIndex < NumberOfFonts)); + if ((ULONG)FontIndex >= NumberOfFonts) { + FontIndex = 0; + } + + if (g_currentFontIndex != (ULONG)FontIndex) { + g_currentFontIndex = FontIndex; + } + + lpFont = &FontInfo[FontIndex]; + + /* Display the new font */ + + Parameters[0] = (DWORD_PTR)wszSelectedFont; + Parameters[1] = (DWORD_PTR)lpFont->FaceName; + FormatMessageW(FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ARGUMENT_ARRAY, + ghInstance, + MSG_FONTSTRING_FORMATTING, + LANG_NEUTRAL, + wszFace, + ARRAYSIZE(wszFace), + (va_list*)Parameters); + SetDlgItemText(hDlg, IDD_GROUP, wszFace); + + /* Put the font size in the static boxes */ + StringCchPrintf(wszText, ARRAYSIZE(wszText), TEXT("%u"), lpFont->Size.X); + hWnd = GetDlgItem(hDlg, IDD_FONTWIDTH); + SetWindowText(hWnd, wszText); + InvalidateRect(hWnd, NULL, TRUE); + StringCchPrintf(wszText, ARRAYSIZE(wszText), TEXT("%u"), lpFont->Size.Y); + hWnd = GetDlgItem(hDlg, IDD_FONTHEIGHT); + SetWindowText(hWnd, wszText); + InvalidateRect(hWnd, NULL, TRUE); + + /* Force the preview windows to repaint */ + hWnd = GetDlgItem(hDlg, IDD_PREVIEWWINDOW); + SendMessage(hWnd, CM_PREVIEW_UPDATE, 0, 0); + hWnd = GetDlgItem(hDlg, IDD_FONTWINDOW); + InvalidateRect(hWnd, NULL, TRUE); + + DBGFONTS(("Font %x, (%d,%d) %ls\n", + FontIndex, + FontInfo[FontIndex].Size.X, + FontInfo[FontIndex].Size.Y, + FontInfo[FontIndex].FaceName)); + + return TRUE; +} diff --git a/src/propsheet/fontdlg.h b/src/propsheet/fontdlg.h new file mode 100644 index 000000000..4957cb6b0 --- /dev/null +++ b/src/propsheet/fontdlg.h @@ -0,0 +1,77 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + fontdlg.h + +Abstract: + + This module contains the definitions for console font dialog + +Author: + + Therese Stowell (thereses) Feb-3-1992 (swiped from Win3.1) + +Revision History: + +--*/ + +#pragma once + +#ifndef FONTDLG_H +#define FONTDLG_H + +/* ----- Literals ----- */ + +#define MAXDIMENSTRING 40 // max text in combo box +#define DX_TTBITMAP 20 +#define DY_TTBITMAP 12 +#define CCH_RASTERFONTS 24 +#define CCH_SELECTEDFONT 30 + + +/* ----- Macros ----- */ +/* + * High-level macros + * + * These macros handle the SendMessages that go tofrom list boxes + * and combo boxes. + * + * The "xxx_lcb" prefix stands for leaves CritSect & "list or combo box". + * + * Basically, we're providing mnemonic names for what would otherwise + * look like a whole slew of confusing SendMessage's. + * + */ +#define lcbRESETCONTENT(hWnd, bLB) \ + SendMessage(hWnd, bLB ? LB_RESETCONTENT : CB_RESETCONTENT, 0, 0L) + +#define lcbGETTEXT(hWnd, bLB, w) \ + SendMessage(hWnd, bLB ? LB_GETTEXT : CB_GETLBTEXT, w, 0L) + +#define lcbFINDSTRINGEXACT(hWnd, bLB, pwsz) \ + (LONG)SendMessage(hWnd, bLB ? LB_FINDSTRINGEXACT : CB_FINDSTRINGEXACT, \ + (WPARAM)-1, (LPARAM)pwsz) + +#define lcbADDSTRING(hWnd, bLB, pwsz) \ + (LONG)SendMessage(hWnd, bLB ? LB_ADDSTRING : CB_ADDSTRING, 0, (LPARAM)pwsz) + +#define lcbSETITEMDATA(hWnd, bLB, w, nFont) \ + SendMessage(hWnd, bLB ? LB_SETITEMDATA : CB_SETITEMDATA, w, nFont) + +#define lcbGETITEMDATA(hWnd, bLB, w) \ + (LONG)SendMessage(hWnd, bLB ? LB_GETITEMDATA : CB_GETITEMDATA, w, 0L) + +#define lcbGETCOUNT(hWnd, bLB) \ + (LONG)SendMessage(hWnd, bLB ? LB_GETCOUNT : CB_GETCOUNT, 0, 0L) + +#define lcbGETCURSEL(hWnd, bLB) \ + (LONG)SendMessage(hWnd, bLB ? LB_GETCURSEL : CB_GETCURSEL, 0, 0L) + +#define lcbSETCURSEL(hWnd, bLB, w) \ + SendMessage(hWnd, bLB ? LB_SETCURSEL : CB_SETCURSEL, w, 0L) + +#endif /* #ifndef FONTDLG_H */ diff --git a/src/propsheet/globals.cpp b/src/propsheet/globals.cpp new file mode 100644 index 000000000..b7f424632 --- /dev/null +++ b/src/propsheet/globals.cpp @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +PCONSOLE_STATE_INFO gpStateInfo; + +LONG gcxScreen; +LONG gcyScreen; + +BOOL g_fForceV2; +BOOL g_fEditKeys; +BYTE g_bPreviewOpacity = 0x00; //sentinel value for initial test on dialog entry. Once initialized, won't be less than TRANSPARENCY_RANGE_MIN + +BOOL g_fHostedInFileProperties = FALSE; + +UINT OEMCP; +BOOL g_fEastAsianSystem; +bool g_fIsComCtlV6Present; + +const wchar_t g_szPreviewText[] = \ + L"C:\\WINDOWS> dir \n" \ + L"SYSTEM 10-01-99 5:00a\n" \ + L"SYSTEM32 10-01-99 5:00a\n" \ + L"README TXT 26926 10-01-99 5:00a\n" \ + L"WINDOWS BMP 46080 10-01-99 5:00a\n" \ + L"NOTEPAD EXE 337232 10-01-99 5:00a\n" \ + L"CLOCK AVI 39594 10-01-99 5:00p\n" \ + L"WIN INI 7005 10-01-99 5:00a\n"; + +BOOL fChangeCodePage = FALSE; + +WCHAR DefaultFaceName[LF_FACESIZE]; +WCHAR DefaultTTFaceName[LF_FACESIZE]; +COORD DefaultFontSize; +BYTE DefaultFontFamily; +ULONG DefaultFontIndex = 0; +ULONG g_currentFontIndex = 0; + +PFONT_INFO FontInfo = NULL; +ULONG NumberOfFonts; +ULONG FontInfoLength; +BOOL gbEnumerateFaces = FALSE; +PFACENODE gpFaceNames = NULL; + +BOOL g_fSettingsDlgInitialized = FALSE; + +BOOL InEM_UNDO=FALSE; + +// These values are used to "remember" the colors across a disable/re-enable, +// so that if we disable the setting then re-enable it, we can re-initalize +// it with the same value it had before. +COLORREF g_fakeForegroundColor = RGB(242, 242, 242); // Default bright white +COLORREF g_fakeBackgroundColor = RGB(12, 12, 12); // Default black +COLORREF g_fakeCursorColor = RGB(242, 242, 242); // Default bright white + +HWND g_hTerminalDlg = static_cast(INVALID_HANDLE_VALUE); diff --git a/src/propsheet/globals.h b/src/propsheet/globals.h new file mode 100644 index 000000000..b3128dbe0 --- /dev/null +++ b/src/propsheet/globals.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Module Name: +// globals.h +// +// Abstract: +// One seperate container for many of the global variables in the propsheet +// +// Author: +// Mike Griese (mikegr) 2016-Oct +// +// Revision History: + +#pragma once +#include "font.h" + +extern HINSTANCE ghInstance; +extern PCONSOLE_STATE_INFO gpStateInfo; +extern PFONT_INFO FontInfo; +extern ULONG NumberOfFonts; +extern ULONG FontInfoLength; +extern ULONG g_currentFontIndex; +extern ULONG DefaultFontIndex; +extern WCHAR DefaultFaceName[LF_FACESIZE]; +extern WCHAR DefaultTTFaceName[LF_FACESIZE]; +extern COORD DefaultFontSize; +extern BYTE DefaultFontFamily; +extern const wchar_t g_szPreviewText[]; + +//Initial default fonts and face names +extern PFACENODE gpFaceNames; + +extern BOOL gbEnumerateFaces; +extern LONG gcxScreen; +extern LONG gcyScreen; +extern BOOL g_fForceV2; +extern BOOL g_fEditKeys; +extern BYTE g_bPreviewOpacity ; +extern BOOL g_fHostedInFileProperties; + +extern UINT OEMCP; +extern BOOL g_fEastAsianSystem; +extern bool g_fIsComCtlV6Present; + +extern BOOL fChangeCodePage; + +extern BOOL g_fSettingsDlgInitialized; +extern BOOL InEM_UNDO; + +extern COLORREF g_fakeForegroundColor; +extern COLORREF g_fakeBackgroundColor; +extern COLORREF g_fakeCursorColor; + +extern HWND g_hTerminalDlg; diff --git a/src/propsheet/init.cpp b/src/propsheet/init.cpp new file mode 100644 index 000000000..c6b6e7365 --- /dev/null +++ b/src/propsheet/init.cpp @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "cpl_core.h" + + +HINSTANCE ghInstance; + + +/*************************************************************\ + * DllInitialize() + * + * Purpose: Main entry point + * + * + * Parameters: HINSTANCE hInstDLL - Instance handle of DLL + * DWORD dwReason - Reason DLL was called + * LPVOID lpvReserved - NULL +\*************************************************************/ +BOOL WINAPI DllMain( + HINSTANCE hInstDLL, + DWORD dwReason, + LPVOID /*lpvReserved*/) +{ + if (dwReason != DLL_PROCESS_ATTACH) { + return TRUE; + } + + ghInstance = hInstDLL; + + DisableThreadLibraryCalls(hInstDLL); + + return TRUE; +} + + +/*************************************************************\ + * CPlApplet() + * + * Purpose: Control Panel entry point - used when launching from a running conhost session. For property sheets + * being used in a filesystem shortcut properties dialog, see ConsolePropertySheetHandler. + * + * + * Parameters: HWND hwnd - Window handle + * WORD wMsg - Control Panel message + * LPARAM lParam1 - Long parameter + * LPARAM lParam2 - Long parameter +\*************************************************************/ +LONG CPlApplet( + HWND hwnd, + WORD wMsg, + LPARAM lParam1, + LPARAM lParam2) +{ + LPCPLINFO lpOldCPlInfo; + LPNEWCPLINFO lpCPlInfo; + INITCOMMONCONTROLSEX iccex; + + switch (wMsg) { + + case CPL_INIT: + + iccex.dwSize = sizeof(iccex); + iccex.dwICC = ICC_WIN95_CLASSES; + InitCommonControlsEx( &iccex ); + + return InitializeConsoleState(); + + case CPL_GETCOUNT: + return 1; + + case CPL_INQUIRE: + + lpOldCPlInfo = (LPCPLINFO)lParam2; + + lpOldCPlInfo->idIcon = IDI_CONSOLE; + lpOldCPlInfo->idName = IDS_NAME; + lpOldCPlInfo->idInfo = IDS_INFO; + lpOldCPlInfo->lData = 0; + return TRUE; + + case CPL_NEWINQUIRE: + + lpCPlInfo = (LPNEWCPLINFO)lParam2; + + lpCPlInfo->hIcon = LoadIcon(NULL, MAKEINTRESOURCE(IDI_CONSOLE)); + + if (!LoadString(ghInstance, IDS_NAME, lpCPlInfo->szName, + ARRAYSIZE(lpCPlInfo->szName))) { + lpCPlInfo->szName[0] = TEXT('\0'); + } + + if (!LoadString(ghInstance, IDS_INFO, lpCPlInfo->szInfo, + ARRAYSIZE(lpCPlInfo->szInfo))) { + lpCPlInfo->szInfo[0] = TEXT('\0'); + } + + lpCPlInfo->dwSize = sizeof(NEWCPLINFO); + lpCPlInfo->dwHelpContext = 0; + lpCPlInfo->szHelpFile[0] = TEXT('\0'); + + return (LONG)TRUE; + + case CPL_DBLCLK: + ConsolePropertySheet(hwnd, (PCONSOLE_STATE_INFO)lParam1); + break; + + case CPL_EXIT: + UninitializeConsoleState(); + break; + } + + return (LONG)0; +} diff --git a/src/propsheet/menu.h b/src/propsheet/menu.h new file mode 100644 index 000000000..b378388b7 --- /dev/null +++ b/src/propsheet/menu.h @@ -0,0 +1,31 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + menu.h + +Abstract: + + This module contains the definitions for console system menu + +Author: + + Therese Stowell (thereses) Feb-3-1992 (swiped from Win3.1) + +Revision History: + +--*/ + +/* + * DIALOG IDs + * + */ + +#pragma once + +#define CM_SETCOLOR (WM_USER+1) +#define CM_PREVIEW_UPDATE (WM_USER+2) + diff --git a/src/propsheet/misc.cpp b/src/propsheet/misc.cpp new file mode 100644 index 000000000..22c00faf4 --- /dev/null +++ b/src/propsheet/misc.cpp @@ -0,0 +1,988 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + misc.c + +Abstract: + + This file implements the NT console server font routines. + +Author: + + Therese Stowell (thereses) 22-Jan-1991 + +Revision History: + +--*/ + +#include "precomp.h" +#include +#include +#pragma hdrstop + +#if DBG +ULONG gDebugFlag = 0; +#endif + + +#define MAX_FONT_INFO_ALLOC (ULONG_MAX / sizeof(FONT_INFO)) + +#define FE_ABANDONFONT 0 +#define FE_SKIPFONT 1 +#define FE_FONTOK 2 + +#define TERMINAL_FACENAME L"Terminal" + +/* + * TTPoints -- Initial font pixel heights for TT fonts + */ +SHORT TTPoints[] = { + 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 24, 28, 36, 72 +}; + +/* + * TTPointsDbcs -- Initial font pixel heights for TT fonts of DBCS. + * So, This list except odd point size because font width is (SBCS:DBCS != 1:2). + */ +SHORT TTPointsDbcs[] = { + 6, 8, 10, 12, 14, 16, 18, 20, 24, 28, 36, 72 +}; + + +typedef struct _FONTENUMDATA { + HDC hDC; + BOOL bFindFaces; + ULONG ulFE; + __field_ecount_opt(nTTPoints) PSHORT pTTPoints; + UINT nTTPoints; +} FONTENUMDATA, *PFONTENUMDATA; + + +PFACENODE +AddFaceNode( + __in_ecount(LF_FACESIZE) LPCWSTR ptsz) +{ + PFACENODE pNew, *ppTmp; + size_t cch; + + /* + * Is it already here? + */ + for (ppTmp = &gpFaceNames; *ppTmp; ppTmp = &((*ppTmp)->pNext)) { + if (0 == lstrcmp(((*ppTmp)->atch), ptsz)) { + // already there ! + return *ppTmp; + } + } + + cch = wcslen(ptsz); + pNew = (PFACENODE)HeapAlloc(GetProcessHeap(), + 0, + sizeof(FACENODE) + ((cch + 1) * sizeof(WCHAR))); + if (pNew == NULL) { + return NULL; + } + + pNew->pNext = NULL; + pNew->dwFlag = 0; + StringCchCopy(pNew->atch, cch + 1, ptsz); + *ppTmp = pNew; + return pNew; +} + + +VOID +DestroyFaceNodes( + VOID) +{ + PFACENODE pNext, pTmp; + + pTmp = gpFaceNames; + while (pTmp != NULL) { + pNext = pTmp->pNext; + HeapFree(GetProcessHeap(), 0, pTmp); + pTmp = pNext; + } + + gpFaceNames = NULL; +} + +// TODO: Refactor into lib for use by both conhost and console.dll +// see http://osgvsowi/677457 +UINT GetCurrentDPI(const HWND hWnd, const BOOL fReturnYDPI) +{ + UINT dpiX = 0; + UINT dpiY = 0; + GetDpiForMonitor(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), + MDT_EFFECTIVE_DPI, + &dpiX, + &dpiY); + + return (fReturnYDPI) ? dpiY : dpiX; +} + +int GetDPIScaledPixelSize(const int px, const int iCurrentDPI) +{ + return MulDiv(px, iCurrentDPI, 96); +} + +int GetDPIYScaledPixelSize(const HWND hWnd, const int px) +{ + return GetDPIScaledPixelSize(px, GetCurrentDPI(hWnd, TRUE)); +} + +int GetDPIXScaledPixelSize(const HWND hWnd, const int px) +{ + return GetDPIScaledPixelSize(px, GetCurrentDPI(hWnd, FALSE)); +} + +// If we're running the V2 console, enumerate all of our TrueType fonts and rescale them as appropriate to match the +// current monitor's DPI. This function gets triggered when either the DPI of a single monitor changes, or when the +// properties dialog is moved between monitors of differing DPIs. +void RecreateFontHandles(const HWND hWnd) +{ + if (gpStateInfo->fIsV2Console) + { + for (UINT iCurrFont = 0; iCurrFont < NumberOfFonts; iCurrFont++) + { + // if the current font is a TrueType font + if (TM_IS_TT_FONT(FontInfo[iCurrFont].Family)) + { + LOGFONT lf = {0}; + lf.lfWidth = GetDPIXScaledPixelSize(hWnd, FontInfo[iCurrFont].Size.X); + lf.lfHeight = GetDPIYScaledPixelSize(hWnd, FontInfo[iCurrFont].Size.Y); + lf.lfWeight = FontInfo[iCurrFont].Weight; + lf.lfCharSet = FontInfo[iCurrFont].tmCharSet; + + // NOTE: not using what GDI gave us because some fonts don't quite roundtrip (e.g. MS Gothic and VL Gothic) + lf.lfPitchAndFamily = (FIXED_PITCH | FF_MODERN); + if (SUCCEEDED(StringCchCopy(lf.lfFaceName, ARRAYSIZE(lf.lfFaceName), FontInfo[iCurrFont].FaceName))) + { + HFONT hRescaledFont = CreateFontIndirect(&lf); + if (hRescaledFont != NULL) + { + // Only replace the existing font if we've got a replacement. The worst that can happen is that + // we fail to create our scaled font, so the user sees an incorrectly-scaled font preview. + DeleteObject(FontInfo[iCurrFont].hFont); + FontInfo[iCurrFont].hFont = hRescaledFont; + } + } + } + } + } +} + + +// Routine Description: +// - Add the font described by the LOGFONT structure to the font table if +// it's not already there. +int +AddFont( + ENUMLOGFONT *pelf, + NEWTEXTMETRIC* pntm, + int nFontType, + HDC hDC, + PFACENODE pFN) +{ + HFONT hFont; + TEXTMETRIC tm; + ULONG nFont; + COORD SizeToShow, SizeActual, SizeWant, SizeOriginal; + BYTE tmFamily; + SIZE Size; + BOOL fCreatingBoldFont = FALSE; + LPTSTR ptszFace = pelf->elfLogFont.lfFaceName; + + /* get font info */ + SizeWant.X = (SHORT)pelf->elfLogFont.lfWidth; + SizeWant.Y = (SHORT)pelf->elfLogFont.lfHeight; + + /* save original size request so that we can use it unmodified when doing DPI calculations */ + SizeOriginal.X = (SHORT)pelf->elfLogFont.lfWidth; + SizeOriginal.Y = (SHORT)pelf->elfLogFont.lfHeight; + +CreateBoldFont: + pelf->elfLogFont.lfQuality = DEFAULT_QUALITY; + hFont = CreateFontIndirect(&pelf->elfLogFont); + if (!hFont) { + DBGFONTS((" REJECT font (can't create)\n")); + return FE_SKIPFONT; // same font in other sizes may still be suitable + } + + DBGFONTS2((" hFont = %p\n", hFont)); + + SelectObject(hDC, hFont); + GetTextMetrics(hDC, &tm); + + GetTextExtentPoint32(hDC, TEXT("0"), 1, &Size); + SizeActual.X = (SHORT)Size.cx; + SizeActual.Y = (SHORT)(tm.tmHeight + tm.tmExternalLeading); + DBGFONTS2((" actual size %d,%d\n", SizeActual.X, SizeActual.Y)); + tmFamily = tm.tmPitchAndFamily; + if (TM_IS_TT_FONT(tmFamily) && (SizeWant.Y >= 0)) { + SizeToShow = SizeWant; + if (SizeWant.X == 0) { + // Asking for zero width height gets a default aspect-ratio width. + // It's better to show that width rather than 0. + SizeToShow.X = SizeActual.X; + } + } else { + SizeToShow = SizeActual; + } + + DBGFONTS2((" SizeToShow = (%d,%d), SizeActual = (%d,%d)\n", + SizeToShow.X, SizeToShow.Y, SizeActual.X, SizeActual.Y)); + + /* + * NOW, determine whether this font entry has already been cached + * LATER : it may be possible to do this before creating the font, if + * we can trust the dimensions & other info from pntm. + * Sort by size: + * 1) By pixelheight (negative Y values) + * 2) By height (as shown) + * 3) By width (as shown) + */ + for (nFont = 0; nFont < NumberOfFonts; ++nFont) { + COORD SizeShown; + + if (FontInfo[nFont].hFont == NULL) { + DBGFONTS(("! Font %x has a NULL hFont\n", nFont)); + continue; + } + + if (FontInfo[nFont].SizeWant.X > 0) { + SizeShown.X = FontInfo[nFont].SizeWant.X; + } else { + SizeShown.X = FontInfo[nFont].Size.X; + } + + if (FontInfo[nFont].SizeWant.Y > 0) { + // This is a font specified by cell height. + SizeShown.Y = FontInfo[nFont].SizeWant.Y; + } else { + SizeShown.Y = FontInfo[nFont].Size.Y; + if (FontInfo[nFont].SizeWant.Y < 0) { + // This is a TT font specified by character height. + if (SizeWant.Y < 0 && SizeWant.Y > FontInfo[nFont].SizeWant.Y) { + // Requested pixelheight is smaller than this one. + DBGFONTS(("INSERT %d pt at %x, before %d pt\n", + -SizeWant.Y, nFont, -FontInfo[nFont].SizeWant.Y)); + break; + } + } + } + + // Note that we're relying on pntm->tmWeight below because some fonts (e.g. Iosevka Extralight) show up as bold + // via GetTextMetrics. pntm->tmWeight doesn't have this issue. However, on the second pass through (see + // :CreateBoldFont) we should use what's in tm.tmWeight + if (SIZE_EQUAL(SizeShown, SizeToShow) && + FontInfo[nFont].Family == tmFamily && + FontInfo[nFont].Weight == ((fCreatingBoldFont) ? tm.tmWeight : pntm->tmWeight) && + 0 == lstrcmp(FontInfo[nFont].FaceName, ptszFace)) + { + /* + * Already have this font + */ + DBGFONTS2((" Already have the font\n")); + DeleteObject(hFont); + return FE_FONTOK; + } + + + if ((SizeToShow.Y < SizeShown.Y) || + (SizeToShow.Y == SizeShown.Y && SizeToShow.X < SizeShown.X)) { + /* + * This new font is smaller than nFont + */ + DBGFONTS(("INSERT at %x, SizeToShow = (%d,%d)\n", nFont, + SizeToShow.X,SizeToShow.Y)); + break; + } + } + + /* + * If we have to grow our font table, do it. + */ + if (NumberOfFonts == FontInfoLength) { + PFONT_INFO Temp = NULL; + + FontInfoLength += FONT_INCREMENT; + if (FontInfoLength < MAX_FONT_INFO_ALLOC) + { + Temp = (PFONT_INFO)HeapReAlloc(GetProcessHeap(), + 0, + FontInfo, + sizeof(FONT_INFO) * FontInfoLength); + } + + if (Temp == NULL) { + FontInfoLength -= FONT_INCREMENT; + return FE_ABANDONFONT; // no point enumerating more - no memory! + } + FontInfo = Temp; + } + + /* + * The font we are adding should be inserted into the list, if it is + * smaller than the last one. + */ + if (nFont < NumberOfFonts) { + RtlMoveMemory(&FontInfo[nFont+1], + &FontInfo[nFont], + sizeof(FONT_INFO) * (NumberOfFonts - nFont)); + } + + /* + * If we're adding a truetype font for the V2 console, secretly swap out the current hFont with one that's scaled + * appropriately for DPI + */ + if (nFontType == TRUETYPE_FONTTYPE && gpStateInfo->fIsV2Console) { + DeleteObject(hFont); + pelf->elfLogFont.lfWidth = GetDPIXScaledPixelSize(gpStateInfo->hWnd, SizeOriginal.X); + pelf->elfLogFont.lfHeight = GetDPIYScaledPixelSize(gpStateInfo->hWnd, SizeOriginal.Y); + hFont = CreateFontIndirect(&pelf->elfLogFont); + if (!hFont) { + return FE_SKIPFONT; + } + } + + /* + * Store the font info + */ + FontInfo[nFont].hFont = hFont; + FontInfo[nFont].Family = tmFamily; + FontInfo[nFont].Size = SizeActual; + if (TM_IS_TT_FONT(tmFamily)) { + FontInfo[nFont].SizeWant = SizeWant; + } else { + FontInfo[nFont].SizeWant.X = 0; + FontInfo[nFont].SizeWant.Y = 0; + } + + FontInfo[nFont].Weight = tm.tmWeight; + FontInfo[nFont].FaceName = pFN->atch; + + FontInfo[nFont].tmCharSet = tm.tmCharSet; + + ++NumberOfFonts; + + /* + * If this is a true type font, create a bold version too. + */ + if (nFontType == TRUETYPE_FONTTYPE && !IS_BOLD(FontInfo[nFont].Weight)) { + pelf->elfLogFont.lfWeight = FW_BOLD; + pelf->elfLogFont.lfWidth = SizeOriginal.X; + pelf->elfLogFont.lfHeight = SizeOriginal.Y; + fCreatingBoldFont = TRUE; + goto CreateBoldFont; + } + + return FE_FONTOK; // and continue enumeration +} + + +VOID +InitializeFonts( + VOID) +{ + EnumerateFonts(EF_DEFFACE); // Just the Default font +} + + +VOID +DestroyFonts( + VOID) +{ + ULONG FontIndex; + + if (FontInfo != NULL) { + for (FontIndex = 0; FontIndex < NumberOfFonts; FontIndex++) { + DeleteObject(FontInfo[FontIndex].hFont); + } + HeapFree(GetProcessHeap(), 0, FontInfo); + FontInfo = NULL; + NumberOfFonts = 0; + } + + DestroyFaceNodes(); +} + +/* + * Returns bit combination + * FE_ABANDONFONT - do not continue enumerating this font + * FE_SKIPFONT - skip this font but keep enumerating + * FE_FONTOK - font was created and added to cache or already there + * + * Is called exactly once by GDI for each font in the system. This + * routine is used to store the FONT_INFO structure. + */ +int FontEnumForV2Console(ENUMLOGFONT *pelf, NEWTEXTMETRIC *pntm, int nFontType, LPARAM lParam) +{ + FAIL_FAST_IF(!(ShouldAllowAllMonoTTFonts())); + UINT i; + LPCTSTR ptszFace = pelf->elfLogFont.lfFaceName; + PFACENODE pFN; + PFONTENUMDATA pfed = (PFONTENUMDATA)lParam; + + DBGFONTS((" FontEnum \"%ls\" (%d,%d) weight 0x%lx(%d) %x -- %s\n", + ptszFace, + pelf->elfLogFont.lfWidth, pelf->elfLogFont.lfHeight, + pelf->elfLogFont.lfWeight, pelf->elfLogFont.lfWeight, + pelf->elfLogFont.lfCharSet, + pfed->bFindFaces ? "Finding Faces" : "Creating Fonts")); + + // reject non-monospaced fonts + if (!(pelf->elfLogFont.lfPitchAndFamily & FIXED_PITCH)) + { + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + // reject non-modern or italic TT fonts + if ((nFontType == TRUETYPE_FONTTYPE) && + (((pelf->elfLogFont.lfPitchAndFamily & 0xf0) != FF_MODERN) || + pelf->elfLogFont.lfItalic)) + { + DBGFONTS((" REJECT face (TT but not FF_MODERN)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + // reject non-TT fonts that aren't OEM + if ((nFontType != TRUETYPE_FONTTYPE) && !IS_DBCS_OR_OEM_CHARSET(pelf->elfLogFont.lfCharSet)) + { + DBGFONTS((" REJECT face (not TT nor OEM)\n")); + return FE_SKIPFONT; + } + + // reject fonts that are vertical + if (ptszFace[0] == TEXT('@')) + { + DBGFONTS((" REJECT face (not TT and TATEGAKI)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + // reject non-TT fonts that aren't terminal + if (g_fEastAsianSystem && (nFontType != TRUETYPE_FONTTYPE) && (0 != lstrcmp(ptszFace, TERMINAL_FACENAME))) + { + DBGFONTS((" REJECT face (not TT nor Terminal)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + // reject East Asian TT fonts that aren't East Asian charset. + if (g_fEastAsianSystem && (nFontType == TRUETYPE_FONTTYPE) && !IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) { + DBGFONTS((" REJECT face (East Asian charset, but not East Asian TT)\n")); + return FE_SKIPFONT; // should be enumerate next charset. + } + + // reject East Asian TT fonts on non-East Asian systems + if (!g_fEastAsianSystem && (nFontType == TRUETYPE_FONTTYPE) && IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) + { + DBGFONTS((" REJECT face (East Asian TT and not East Asian charset)\n")); + return FE_SKIPFONT; // should be enumerate next charset. + } + + /* + * Add or find the facename + */ + pFN = AddFaceNode(ptszFace); + if (pFN == NULL) { + return FE_ABANDONFONT; + } + + if (pfed->bFindFaces) { + DWORD dwFontType; + + if (nFontType == TRUETYPE_FONTTYPE) { + DBGFONTS(("NEW TT FACE %ls\n", ptszFace)); + dwFontType = EF_TTFONT; + } else if (nFontType == RASTER_FONTTYPE) { + DBGFONTS(("NEW OEM FACE %ls\n",ptszFace)); + dwFontType = EF_OEMFONT; + } else { + dwFontType = 0; + } + + pFN->dwFlag |= dwFontType | EF_NEW; + if (IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) { + pFN->dwFlag |= EF_DBCSFONT; + } + return FE_SKIPFONT; + } + + /* + * Add the font to the table. If this is a true type font, add the + * sizes from the array. Otherwise, just add the size we got. + */ + if (nFontType & TRUETYPE_FONTTYPE) { + for (i = 0; i < pfed->nTTPoints; i++) { + pelf->elfLogFont.lfHeight = pfed->pTTPoints[i]; + pelf->elfLogFont.lfWidth = 0; + pelf->elfLogFont.lfWeight = pntm->tmWeight; + pfed->ulFE |= AddFont(pelf, pntm, nFontType, pfed->hDC, pFN); + if (pfed->ulFE == FE_ABANDONFONT) { + return FE_ABANDONFONT; + } + } + } else { + pfed->ulFE |= AddFont(pelf, pntm, nFontType, pfed->hDC, pFN); + if (pfed->ulFE == FE_ABANDONFONT) { + return FE_ABANDONFONT; + } + } + + return FE_FONTOK; // and continue enumeration +} + +/* + * Returns bit combination + * FE_ABANDONFONT - do not continue enumerating this font + * FE_SKIPFONT - skip this font but keep enumerating + * FE_FONTOK - font was created and added to cache or already there + * + * Is called exactly once by GDI for each font in the system. This + * routine is used to store the FONT_INFO structure. + */ +int +FontEnum( + ENUMLOGFONT *pelf, + NEWTEXTMETRIC *pntm, + int nFontType, + LPARAM lParam) +{ + UINT i; + LPCTSTR ptszFace = pelf->elfLogFont.lfFaceName; + PFACENODE pFN; + PFONTENUMDATA pfed = (PFONTENUMDATA)lParam; + + DBGFONTS((" FontEnum \"%ls\" (%d,%d) weight 0x%lx(%d) %x -- %s\n", + ptszFace, + pelf->elfLogFont.lfWidth, pelf->elfLogFont.lfHeight, + pelf->elfLogFont.lfWeight, pelf->elfLogFont.lfWeight, + pelf->elfLogFont.lfCharSet, + pfed->bFindFaces ? "Finding Faces" : "Creating Fonts")); + + // + // reject variable width and italic fonts, also tt fonts with neg ac + // + + if + ( + !(pelf->elfLogFont.lfPitchAndFamily & FIXED_PITCH) || + (pelf->elfLogFont.lfItalic) || + !(pntm->ntmFlags & NTM_NONNEGATIVE_AC) + ) + { + if (! IsAvailableTTFont(ptszFace)) { + DBGFONTS((" REJECT face (dbcs, variable pitch, italic, or neg a&c)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + } + + /* + * reject TT fonts for whoom family is not modern, that is do not use + * FF_DONTCARE // may be surprised unpleasantly + * FF_DECORATIVE // likely to be symbol fonts + * FF_SCRIPT // cursive, inappropriate for console + * FF_SWISS OR FF_ROMAN // variable pitch + */ + + if ((nFontType == TRUETYPE_FONTTYPE) && + ((pelf->elfLogFont.lfPitchAndFamily & 0xf0) != FF_MODERN)) { + DBGFONTS((" REJECT face (TT but not FF_MODERN)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + /* + * reject non-TT fonts that aren't OEM + */ + if ((nFontType != TRUETYPE_FONTTYPE) && + (!g_fEastAsianSystem || !IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) && + (pelf->elfLogFont.lfCharSet != OEM_CHARSET)) { + DBGFONTS((" REJECT face (not TT nor OEM)\n")); + return FE_SKIPFONT; + } + + /* + * reject non-TT fonts that are virtical font + */ + if ((nFontType != TRUETYPE_FONTTYPE) && + (ptszFace[0] == TEXT('@'))) + { + DBGFONTS((" REJECT face (not TT and TATEGAKI)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + /* + * reject non-TT fonts that aren't Terminal + */ + if (g_fEastAsianSystem && (nFontType != TRUETYPE_FONTTYPE) && + (0 != lstrcmp(ptszFace, TERMINAL_FACENAME))) + { + DBGFONTS((" REJECT face (not TT nor Terminal)\n")); + return pfed->bFindFaces ? FE_SKIPFONT : FE_ABANDONFONT; + } + + /* + * reject East Asian TT fonts that aren't East Asian charset. + */ + if (IsAvailableTTFont(ptszFace) && + !IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet) && + !IsAvailableTTFontCP(ptszFace,0) + ) { + DBGFONTS((" REJECT face (East Asian TT and not East Asian charset)\n")); + return FE_SKIPFONT; // should be enumerate next charset. + } + + /* + * Add or find the facename + */ + pFN = AddFaceNode(ptszFace); + if (pFN == NULL) { + return FE_ABANDONFONT; + } + + if (pfed->bFindFaces) { + DWORD dwFontType; + + if (nFontType == TRUETYPE_FONTTYPE) { + DBGFONTS(("NEW TT FACE %ls\n", ptszFace)); + dwFontType = EF_TTFONT; + } else if (nFontType == RASTER_FONTTYPE) { + DBGFONTS(("NEW OEM FACE %ls\n",ptszFace)); + dwFontType = EF_OEMFONT; + } else { + dwFontType = 0; + } + + pFN->dwFlag |= dwFontType | EF_NEW; + if (IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) { + pFN->dwFlag |= EF_DBCSFONT; + } + return FE_SKIPFONT; + } + + + if (IS_BOLD(pelf->elfLogFont.lfWeight)) { + DBGFONTS2((" A bold font (weight %d)\n", pelf->elfLogFont.lfWeight)); + // return FE_SKIPFONT; + } + + /* + * Add the font to the table. If this is a true type font, add the + * sizes from the array. Otherwise, just add the size we got. + */ + if (nFontType & TRUETYPE_FONTTYPE) { + for (i = 0; i < pfed->nTTPoints; i++) { + pelf->elfLogFont.lfHeight = pfed->pTTPoints[i]; + pelf->elfLogFont.lfWidth = 0; + pelf->elfLogFont.lfWeight = 400; + pfed->ulFE |= AddFont(pelf, pntm, nFontType, pfed->hDC, pFN); + if (pfed->ulFE == FE_ABANDONFONT) { + return FE_ABANDONFONT; + } + } + } else { + pfed->ulFE |= AddFont(pelf, pntm, nFontType, pfed->hDC, pFN); + if (pfed->ulFE == FE_ABANDONFONT) { + return FE_ABANDONFONT; + } + } + + return FE_FONTOK; // and continue enumeration +} + +BOOL +DoFontEnum( + __in_opt HDC hDC, + __in_ecount_opt(LF_FACESIZE) LPTSTR ptszFace, + __in_ecount_opt(nTTPoints) PSHORT pTTPoints, + __in UINT nTTPoints) +{ + BOOL bDeleteDC = FALSE; + FONTENUMDATA fed; + LOGFONT LogFont; + + DBGFONTS(("DoFontEnum \"%ls\"\n", ptszFace)); + if (hDC == NULL) { + hDC = CreateCompatibleDC(NULL); + bDeleteDC = TRUE; + } + + fed.hDC = hDC; + fed.bFindFaces = (ptszFace == NULL); + fed.ulFE = 0; + fed.pTTPoints = pTTPoints; + fed.nTTPoints = nTTPoints; + RtlZeroMemory(&LogFont, sizeof(LOGFONT)); + LogFont.lfCharSet = DEFAULT_CHARSET; + if (ptszFace != nullptr) { + StringCchCopy(LogFont.lfFaceName, LF_FACESIZE, ptszFace); + + if (NumberOfFonts == 0 && // We've yet to enumerate fonts + g_fEastAsianSystem && // And we're currently using a CJK codepage + !IS_ANY_DBCS_CHARSET(CodePageToCharSet(OEMCP)) && // But the system codepage *isn't* CJK + 0 == lstrcmp(ptszFace, TERMINAL_FACENAME)) { // and we're looking at the raster font + + // In this specific scenario, the raster font will only be enumerated if we ask for OEM_CHARSET rather than + // a CJK charset + LogFont.lfCharSet = OEM_CHARSET; + } + } + + /* + * EnumFontFamiliesEx function enumerates one font in every face in every + * character set. + */ + EnumFontFamiliesEx(hDC, &LogFont, (FONTENUMPROC)((ShouldAllowAllMonoTTFonts()) ? FontEnumForV2Console : FontEnum), (LPARAM)&fed, 0); + if (bDeleteDC) { + DeleteDC(hDC); + } + + return (fed.ulFE & FE_FONTOK) != 0; +} + + +VOID +RemoveFace(__in_ecount(LF_FACESIZE) LPCTSTR ptszFace) +{ + DWORD i; + int nToRemove = 0; + + DBGFONTS(("RemoveFace %ls\n", ptszFace)); + // + // Delete & Remove fonts with Face Name == ptszFace + // + for (i = 0; i < NumberOfFonts; i++) + { + if (0 == lstrcmp(FontInfo[i].FaceName, ptszFace)) + { + BOOL bDeleted = DeleteObject(FontInfo[i].hFont); + DBGFONTS(("RemoveFace: hFont %p was %sdeleted\n", + FontInfo[i].hFont, bDeleted ? "" : "NOT ")); + bDeleted; // to fix x86 build complaining + FontInfo[i].hFont = NULL; + nToRemove++; + } + else if (nToRemove > 0) + { + /* + * Shuffle from FontInfo[i] down nToRemove slots. + */ + RtlMoveMemory(&FontInfo[i - nToRemove], + &FontInfo[i], + sizeof(FONT_INFO)*(NumberOfFonts - i)); + NumberOfFonts -= nToRemove; + i -= nToRemove; + nToRemove = 0; + } + } + NumberOfFonts -= nToRemove; +} + +// Given a desired SHORT size, search pTTPoints to determine if size is in the list. +static bool IsSizePresentInList(__in const SHORT sSizeDesired, __in_ecount(nTTPoints) PSHORT pTTPoints, __in UINT nTTPoints) +{ + bool fSizePresent = false; + for (UINT i = 0; i < nTTPoints; i++) + { + if (pTTPoints[i] == sSizeDesired) + { + fSizePresent = true; + break; + } + } + return fSizePresent; +} + +// Given a face name, determine if the size provided is custom (i.e. not on the hardcoded list of sizes). Note that the +// list of sizes we use varies depending on the codepage being used +bool IsFontSizeCustom(__in PCWSTR pszFaceName, __in const SHORT sSize) +{ + bool fUsingCustomFontSize; + if (g_fEastAsianSystem && !IsAvailableTTFontCP(pszFaceName, 0)) + { + fUsingCustomFontSize = !IsSizePresentInList(sSize, TTPointsDbcs, ARRAYSIZE(TTPointsDbcs)); + } + else + { + fUsingCustomFontSize = !IsSizePresentInList(sSize, TTPoints, ARRAYSIZE(TTPoints)); + } + + return fUsingCustomFontSize; +} + +// Determines if the currently-selected font is using a custom size +static bool IsCurrentFontSizeCustom() +{ + return IsFontSizeCustom(gpStateInfo->FaceName, gpStateInfo->FontSize.Y); +} + +// Given a size, iterate through all TT fonts and load them in the provided size (only used for custom (non-hardcoded) +// font sizes) +void CreateSizeForAllTTFonts(__in const SHORT sSize) +{ + HDC hDC = CreateCompatibleDC(NULL); + + // for each font face + for (PFACENODE pFN = gpFaceNames; pFN; pFN = pFN->pNext) + { + if (pFN->dwFlag & EF_TTFONT) + { + // if it's a TT font, load the supplied size + DoFontEnum(hDC, pFN->atch, (PSHORT)&sSize, 1); + } + } +} + +NTSTATUS +EnumerateFonts( + DWORD Flags) +{ + TEXTMETRIC tm; + HDC hDC; + PFACENODE pFN; + DWORD FontIndex; + DWORD dwFontType = 0; + + DBGFONTS(("EnumerateFonts %lx\n", Flags)); + + dwFontType = (EF_TTFONT | EF_OEMFONT | EF_DEFFACE) & Flags; + + if (FontInfo == NULL) { + // + // allocate memory for the font array + // + NumberOfFonts = 0; + + FontInfo = (PFONT_INFO)HeapAlloc(GetProcessHeap(), 0, sizeof(FONT_INFO) * INITIAL_FONTS); + if (FontInfo == NULL) { + return STATUS_NO_MEMORY; + } + + FontInfoLength = INITIAL_FONTS; + } + + hDC = CreateCompatibleDC(NULL); + + if (Flags & EF_DEFFACE) { + SelectObject(hDC, GetStockObject(OEM_FIXED_FONT)); + GetTextMetrics(hDC, &tm); + GetTextFace(hDC, LF_FACESIZE, DefaultFaceName); + + DefaultFontSize.X = (SHORT)(tm.tmMaxCharWidth); + DefaultFontSize.Y = (SHORT)(tm.tmHeight+tm.tmExternalLeading); + DefaultFontFamily = tm.tmPitchAndFamily; + + if (IS_ANY_DBCS_CHARSET(tm.tmCharSet)) { + DefaultFontSize.X /= 2; + } + + DBGFONTS(("Default (OEM) Font %ls (%d,%d) CharSet 0x%02X\n", DefaultFaceName, + DefaultFontSize.X, DefaultFontSize.Y, + tm.tmCharSet)); + + // Make sure we are going to enumerate the OEM face. + pFN = AddFaceNode(DefaultFaceName); + if (pFN != NULL) { + pFN->dwFlag |= EF_DEFFACE | EF_OEMFONT; + } + } + + if (gbEnumerateFaces) { + /* + * Set the EF_OLD bit and clear the EF_NEW bit + * for all previously available faces + */ + for (pFN = gpFaceNames; pFN; pFN = pFN->pNext) { + pFN->dwFlag |= EF_OLD; + pFN->dwFlag &= ~EF_NEW; + } + + // + // Use DoFontEnum to get the names of all the suitable Faces + // All facenames found will be put in gpFaceNames with + // the EF_NEW bit set. + // + DoFontEnum(hDC, NULL, TTPoints, 1); + gbEnumerateFaces = FALSE; + } + + // Use DoFontEnum to get all fonts from the system. Our FontEnum + // proc puts just the ones we want into an array + // + for (pFN = gpFaceNames; pFN; pFN = pFN->pNext) { + DBGFONTS(("\"%ls\" is %s%s%s%s%s%s\n", pFN->atch, + pFN->dwFlag & EF_NEW ? "NEW " : " ", + pFN->dwFlag & EF_OLD ? "OLD " : " ", + pFN->dwFlag & EF_ENUMERATED ? "ENUMERATED " : " ", + pFN->dwFlag & EF_OEMFONT ? "OEMFONT " : " ", + pFN->dwFlag & EF_TTFONT ? "TTFONT " : " ", + pFN->dwFlag & EF_DEFFACE ? "DEFFACE " : " ")); + + if ((pFN->dwFlag & (EF_OLD|EF_NEW)) == EF_OLD) { + // The face is no longer available + RemoveFace(pFN->atch); + pFN->dwFlag &= ~EF_ENUMERATED; + continue; + } + if ((pFN->dwFlag & dwFontType) == 0) { + // not the kind of face we want + continue; + } + if (pFN->dwFlag & EF_ENUMERATED) { + // we already enumerated this face + continue; + } + + if (pFN->dwFlag & EF_TTFONT) { + if (g_fEastAsianSystem && !IsAvailableTTFontCP(pFN->atch,0)) + DoFontEnum(hDC, pFN->atch, TTPointsDbcs, ARRAYSIZE(TTPointsDbcs)); + else + DoFontEnum(hDC, pFN->atch, TTPoints, ARRAYSIZE(TTPoints)); + } else { + DoFontEnum(hDC, pFN->atch, NULL, 0); + } + pFN->dwFlag |= EF_ENUMERATED; + } + + // Now check to see if the currently selected font is using a custom size not in the hardcoded list (TTPoints or + // TTPointsDbcs depending on locale). If so, make sure we populate all of our fonts at that size. + if (IsCurrentFontSizeCustom()) + { + CreateSizeForAllTTFonts(gpStateInfo->FontSize.Y); + } + + DeleteDC(hDC); + + if (g_fEastAsianSystem) { + for (FontIndex = 0; FontIndex < NumberOfFonts; FontIndex++) { + if (FontInfo[FontIndex].Size.X == DefaultFontSize.X && + FontInfo[FontIndex].Size.Y == DefaultFontSize.Y && + IS_DBCS_OR_OEM_CHARSET(FontInfo[FontIndex].tmCharSet) && + FontInfo[FontIndex].Family == DefaultFontFamily) { + break; + } + } + } else { + for (FontIndex = 0; FontIndex < NumberOfFonts; FontIndex++) { + if (FontInfo[FontIndex].Size.X == DefaultFontSize.X && + FontInfo[FontIndex].Size.Y == DefaultFontSize.Y && + FontInfo[FontIndex].Family == DefaultFontFamily) { + break; + } + } + } + + if (FontIndex < NumberOfFonts) { + DefaultFontIndex = FontIndex; + } else { + DefaultFontIndex = 0; + } + + DBGFONTS(("EnumerateFonts : DefaultFontIndex = %ld\n", DefaultFontIndex)); + + return STATUS_SUCCESS; +} diff --git a/src/propsheet/precomp.cpp b/src/propsheet/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/propsheet/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/propsheet/precomp.h b/src/propsheet/precomp.h new file mode 100644 index 000000000..6c44645f4 --- /dev/null +++ b/src/propsheet/precomp.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// -- WARNING -- LOAD BEARING CODE -- +// This define ABSOLUTELY MUST be included (and equal to 1, or more specifically != 0) +// prior to the import of Common Controls. +// Failure to do so will result in a state where property sheet pages load without complete theming, +// suddenly start disappearing and closing themselves (while throwing no error in the debugger) +// or otherwise failing to load the correct version of ComCtl or the string resources you expect. +// For more details, see https://msdn.microsoft.com/en-us/library/windows/desktop/bb773175(v=vs.85).aspx +// DO NOT REMOVE. +#define ISOLATION_AWARE_ENABLED 1 +// -- END WARNING + +#define DEFINE_CONSOLEV2_PROPERTIES +#define INC_OLE2 + + +// This includes a lot of common headers needed by both the host and the propsheet +// including: windows.h, winuser, ntstatus, assert, and the DDK +#include "HostAndPropsheetIncludes.h" + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "globals.h" + +#include "console.h" +#include "menu.h" +#include "dialogs.h" + +#include +#include +#include +#include + +#include "strid.h" +#include "..\propslib\conpropsp.hpp" + +// WIL +#include +#include +#include +#include +#include + +// This is currently bubbling up the source tree to our branch +#ifndef WM_DPICHANGED_BEFOREPARENT +#define WM_DPICHANGED_BEFOREPARENT 0x02E2 +#endif + +// When on a non-CJK machine using the raster font in a CJK codepage (e.g. "chcp 932"), the raster font is enumerated as +// OEM_CHARSET rather than the language-specific charset. Use this macro in conjunction with a check against +// g_fEastAsianSystem or other codepage checks as needed to determine if a font with these charsets should be used. +#define IS_DBCS_OR_OEM_CHARSET(x) (IS_ANY_DBCS_CHARSET(x) || (x) == OEM_CHARSET) diff --git a/src/propsheet/preview.cpp b/src/propsheet/preview.cpp new file mode 100644 index 000000000..18f390a61 --- /dev/null +++ b/src/propsheet/preview.cpp @@ -0,0 +1,481 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + preview.c + +Abstract: + + This module contains the code for console preview window + +Author: + + Therese Stowell (thereses) Feb-3-1992 (swiped from Win3.1) + +Revision History: + +--*/ + +#include "precomp.h" +#pragma hdrstop + + +/* ----- Equates ----- */ +#define PREVIEW_HSCROLL 0x01 +#define PREVIEW_VSCROLL 0x02 + + +/* ----- Prototypes ----- */ + +void AspectPoint( + RECT* rectPreview, + POINT* pt); + +LONG AspectScale( + LONG n1, + LONG n2, + LONG m); + + +/* ----- Globals ----- */ + +POINT NonClientSize; +RECT WindowRect; +DWORD PreviewFlags; + + +VOID +UpdatePreviewRect(VOID) + +/*++ + + Update the global window size and dimensions + +--*/ + +{ + POINT MinSize; + POINT MaxSize; + POINT WindowSize; + PFONT_INFO lpFont; + HMONITOR hMonitor; + MONITORINFO mi = {0}; + + /* + * Get the font pointer + */ + lpFont = &FontInfo[g_currentFontIndex]; + + /* + * Get the window size + */ + MinSize.x = (GetSystemMetrics(SM_CXMIN)-NonClientSize.x) / lpFont->Size.X; + MinSize.y = (GetSystemMetrics(SM_CYMIN)-NonClientSize.y) / lpFont->Size.Y; + MaxSize.x = GetSystemMetrics(SM_CXFULLSCREEN) / lpFont->Size.X; + MaxSize.y = GetSystemMetrics(SM_CYFULLSCREEN) / lpFont->Size.Y; + WindowSize.x = max(MinSize.x, min(MaxSize.x, gpStateInfo->WindowSize.X)); + WindowSize.y = max(MinSize.y, min(MaxSize.y, gpStateInfo->WindowSize.Y)); + + /* + * Get the window rectangle, making sure it's at least twice the + * size of the non-client area. + */ + WindowRect.left = gpStateInfo->WindowPosX; + WindowRect.top = gpStateInfo->WindowPosY; + WindowRect.right = WindowSize.x * lpFont->Size.X + NonClientSize.x; + if (WindowRect.right < NonClientSize.x * 2) { + WindowRect.right = NonClientSize.x * 2; + } + WindowRect.right += WindowRect.left; + WindowRect.bottom = WindowSize.y * lpFont->Size.Y + NonClientSize.y; + if (WindowRect.bottom < NonClientSize.y * 2) { + WindowRect.bottom = NonClientSize.y * 2; + } + WindowRect.bottom += WindowRect.top; + + /* + * Get information about the monitor we're on + */ + hMonitor = MonitorFromRect(&WindowRect, MONITOR_DEFAULTTONEAREST); + mi.cbSize = sizeof(mi); + GetMonitorInfo(hMonitor, &mi); + gcxScreen = mi.rcWork.right - mi.rcWork.left; + gcyScreen = mi.rcWork.bottom - mi.rcWork.top; + + /* + * Convert window rectangle to monitor relative coordinates + */ + WindowRect.right -= WindowRect.left; + WindowRect.left -= mi.rcWork.left; + WindowRect.bottom -= WindowRect.top; + WindowRect.top -= mi.rcWork.top; + + /* + * Update the display flags + */ + if (WindowSize.x < gpStateInfo->ScreenBufferSize.X) { + PreviewFlags |= PREVIEW_HSCROLL; + } else { + PreviewFlags &= ~PREVIEW_HSCROLL; + } + if (WindowSize.y < gpStateInfo->ScreenBufferSize.Y) { + PreviewFlags |= PREVIEW_VSCROLL; + } else { + PreviewFlags &= ~PREVIEW_VSCROLL; + } +} + + +VOID +InvalidatePreviewRect(HWND hWnd) + +/*++ + + Invalidate the area covered by the preview "window" + +--*/ + +{ + RECT rectWin; + RECT rectPreview; + + /* + * Get the size of the preview "screen" + */ + GetClientRect(hWnd, &rectPreview); + + /* + * Get the dimensions of the preview "window" and scale it to the + * preview "screen" + */ + rectWin.left = WindowRect.left; + rectWin.top = WindowRect.top; + rectWin.right = WindowRect.left + WindowRect.right; + rectWin.bottom = WindowRect.top + WindowRect.bottom; + AspectPoint(&rectPreview, (POINT*)&rectWin.left); + AspectPoint(&rectPreview, (POINT*)&rectWin.right); + + /* + * Invalidate the area covered by the preview "window" + */ + InvalidateRect(hWnd, &rectWin, FALSE); +} + + +VOID +PreviewPaint( + PAINTSTRUCT* pPS, + HWND hWnd + ) + +/*++ + + Paints the font preview. This is called inside the paint message + handler for the preview window + +--*/ + +{ + RECT rectWin; + RECT rectPreview; + HBRUSH hbrFrame; + HBRUSH hbrTitle; + HBRUSH hbrOld; + HBRUSH hbrClient; + HBRUSH hbrBorder; + HBRUSH hbrButton; + HBRUSH hbrScroll; + HBRUSH hbrDesktop; + POINT ptButton; + POINT ptScroll; + HDC hDC; + HBITMAP hBitmap; + HBITMAP hBitmapOld; + COLORREF rgbClient; + + /* + * Get the size of the preview "screen" + */ + GetClientRect(hWnd, &rectPreview); + + /* + * Get the dimensions of the preview "window" and scale it to the + * preview "screen" + */ + rectWin = WindowRect; + AspectPoint(&rectPreview, (POINT*)&rectWin.left); + AspectPoint(&rectPreview, (POINT*)&rectWin.right); + + /* + * Compute the dimensions of some other window components + */ + ptButton.x = GetSystemMetrics(SM_CXSIZE); + ptButton.y = GetSystemMetrics(SM_CYSIZE); + AspectPoint(&rectPreview, &ptButton); + ptButton.y *= 2; /* Double the computed size for "looks" */ + ptScroll.x = GetSystemMetrics(SM_CXVSCROLL); + ptScroll.y = GetSystemMetrics(SM_CYHSCROLL); + AspectPoint(&rectPreview, &ptScroll); + + /* + * Create the memory device context + */ + hDC = CreateCompatibleDC(pPS->hdc); + hBitmap = CreateCompatibleBitmap(pPS->hdc, + rectPreview.right, + rectPreview.bottom); + hBitmapOld = (HBITMAP) SelectObject(hDC, hBitmap); + + /* + * Create the brushes + */ + hbrBorder = CreateSolidBrush(GetSysColor(COLOR_ACTIVEBORDER)); + hbrTitle = CreateSolidBrush(GetSysColor(COLOR_ACTIVECAPTION)); + hbrFrame = CreateSolidBrush(GetSysColor(COLOR_WINDOWFRAME)); + hbrButton = CreateSolidBrush(GetSysColor(COLOR_BTNFACE)); + hbrScroll = CreateSolidBrush(GetSysColor(COLOR_SCROLLBAR)); + hbrDesktop = CreateSolidBrush(GetSysColor(COLOR_BACKGROUND)); + rgbClient = GetNearestColor(hDC, ScreenBkColor(gpStateInfo)); + hbrClient = CreateSolidBrush(rgbClient); + + /* + * Erase the clipping area + */ + FillRect(hDC, &(pPS->rcPaint), hbrDesktop); + + /* + * Fill in the whole window with the client brush + */ + hbrOld = (HBRUSH) SelectObject(hDC, hbrClient); + PatBlt(hDC, rectWin.left, rectWin.top, + rectWin.right - 1, rectWin.bottom - 1, PATCOPY); + + /* + * Fill in the caption bar + */ + SelectObject(hDC, hbrTitle); + PatBlt(hDC, rectWin.left + 3, rectWin.top + 3, + rectWin.right - 7, ptButton.y - 2, PATCOPY); + + /* + * Draw the "buttons" + */ + SelectObject(hDC, hbrButton); + PatBlt(hDC, rectWin.left + 3, rectWin.top + 3, + ptButton.x, ptButton.y - 2, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4 - ptButton.x, + rectWin.top + 3, + ptButton.x, ptButton.y - 2, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4 - 2 * ptButton.x - 1, + rectWin.top + 3, + ptButton.x, ptButton.y - 2, PATCOPY); + SelectObject(hDC, hbrFrame); + PatBlt(hDC, rectWin.left + 3 + ptButton.x, rectWin.top + 3, + 1, ptButton.y - 2, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4 - ptButton.x - 1, + rectWin.top + 3, + 1, ptButton.y - 2, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4 - 2 * ptButton.x - 2, + rectWin.top + 3, + 1, ptButton.y - 2, PATCOPY); + + /* + * Draw the scrollbars + */ + SelectObject(hDC, hbrScroll); + if (PreviewFlags & PREVIEW_HSCROLL) { + PatBlt(hDC, rectWin.left + 3, + rectWin.top + rectWin.bottom - 4 - ptScroll.y, + rectWin.right - 7, ptScroll.y, PATCOPY); + } + if (PreviewFlags & PREVIEW_VSCROLL) { + PatBlt(hDC, rectWin.left + rectWin.right - 4 - ptScroll.x, + rectWin.top + 1 + ptButton.y + 1, + ptScroll.x, rectWin.bottom - 6 - ptButton.y, PATCOPY); + if (PreviewFlags & PREVIEW_HSCROLL) { + SelectObject(hDC, hbrFrame); + PatBlt(hDC, rectWin.left + rectWin.right - 5 - ptScroll.x, + rectWin.top + rectWin.bottom - 4 - ptScroll.y, + 1, ptScroll.y, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4 - ptScroll.x, + rectWin.top + rectWin.bottom - 5 - ptScroll.y, + ptScroll.x, 1, PATCOPY); + } + } + + /* + * Draw the interior window frame and caption frame + */ + SelectObject(hDC, hbrFrame); + PatBlt(hDC, rectWin.left + 2, rectWin.top + 2, + 1, rectWin.bottom - 5, PATCOPY); + PatBlt(hDC, rectWin.left + 2, rectWin.top + 2, + rectWin.right - 5, 1, PATCOPY); + PatBlt(hDC, rectWin.left + 2, rectWin.top + rectWin.bottom - 4, + rectWin.right - 5, 1, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 4, rectWin.top + 2, + 1, rectWin.bottom - 5, PATCOPY); + PatBlt(hDC, rectWin.left + 2, rectWin.top + 1 + ptButton.y, + rectWin.right - 5, 1, PATCOPY); + + /* + * Draw the border + */ + SelectObject(hDC, hbrBorder); + PatBlt(hDC, rectWin.left + 1, rectWin.top + 1, + 1, rectWin.bottom - 3, PATCOPY); + PatBlt(hDC, rectWin.left + 1, rectWin.top + 1, + rectWin.right - 3, 1, PATCOPY); + PatBlt(hDC, rectWin.left + 1, rectWin.top + rectWin.bottom - 3, + rectWin.right - 3, 1, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 3, rectWin.top + 1, + 1, rectWin.bottom - 3, PATCOPY); + + /* + * Draw the exterior window frame + */ + SelectObject(hDC, hbrFrame); + PatBlt(hDC, rectWin.left, rectWin.top, + 1, rectWin.bottom - 1, PATCOPY); + PatBlt(hDC, rectWin.left, rectWin.top, + rectWin.right - 1, 1, PATCOPY); + PatBlt(hDC, rectWin.left, rectWin.top + rectWin.bottom - 2, + rectWin.right - 1, 1, PATCOPY); + PatBlt(hDC, rectWin.left + rectWin.right - 2, rectWin.top, + 1, rectWin.bottom - 1, PATCOPY); + + /* + * Copy the memory device context to the screen device context + */ + BitBlt(pPS->hdc, 0, 0, rectPreview.right, rectPreview.bottom, + hDC, 0, 0, SRCCOPY); + + /* + * Clean up everything + */ + SelectObject(hDC, hbrOld); + SelectObject(hDC, hBitmapOld); + DeleteObject(hbrBorder); + DeleteObject(hbrFrame); + DeleteObject(hbrTitle); + DeleteObject(hbrClient); + DeleteObject(hbrButton); + DeleteObject(hbrScroll); + DeleteObject(hbrDesktop); + DeleteObject(hBitmap); + DeleteDC(hDC); +} + + +LRESULT +PreviewWndProc( + HWND hWnd, + UINT wMessage, + WPARAM wParam, + LPARAM lParam + ) + +/* + * PreviewWndProc + * Handles the preview window + */ + +{ + PAINTSTRUCT ps; + LPCREATESTRUCT lpcs; + RECT rcWindow; + int cx; + int cy; + + switch (wMessage) { + case WM_CREATE: + /* + * Figure out space used by non-client area + */ + SetRect(&rcWindow, 0, 0, 50, 50); + AdjustWindowRect(&rcWindow, WS_OVERLAPPEDWINDOW, FALSE); + NonClientSize.x = rcWindow.right - rcWindow.left - 50; + NonClientSize.y = rcWindow.bottom - rcWindow.top - 50; + + /* + * Compute the size of the preview "window" + */ + UpdatePreviewRect(); + + /* + * Scale the window so it has the same aspect ratio as the screen + */ + lpcs = (LPCREATESTRUCT)lParam; + cx = lpcs->cx; + cy = AspectScale(gcyScreen, gcxScreen, cx); + if (cy > lpcs->cy) { + cy = lpcs->cy; + cx = AspectScale(gcxScreen, gcyScreen, cy); + } + MoveWindow(hWnd, lpcs->x, lpcs->y, cx, cy, TRUE); + break; + + case WM_PAINT: + BeginPaint(hWnd, &ps); + PreviewPaint(&ps, hWnd); + EndPaint(hWnd, &ps); + break; + + case CM_PREVIEW_UPDATE: + InvalidatePreviewRect(hWnd); + UpdatePreviewRect(); + + /* + * Make sure the preview "screen" has the correct aspect ratio + */ + GetWindowRect(hWnd, &rcWindow); + cx = rcWindow.right - rcWindow.left; + cy = AspectScale(gcyScreen, gcxScreen, cx); + if (cy != rcWindow.bottom - rcWindow.top) { + SetWindowPos(hWnd, NULL, 0, 0, cx, cy, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER); + } + + InvalidatePreviewRect(hWnd); + break; + + default: + return DefWindowProc(hWnd, wMessage, wParam, lParam); + } + return 0L; +} + + +/* AspectScale + * Performs the following calculation in LONG arithmetic to avoid + * overflow: + * return = n1 * m / n2 + * This can be used to make an aspect ration calculation where n1/n2 + * is the aspect ratio and m is a known value. The return value will + * be the value that corresponds to m with the correct apsect ratio. + */ + +LONG AspectScale( + LONG n1, + LONG n2, + LONG m) +{ + LONG Temp; + + Temp = n1 * m + (n2 >> 1); + return Temp / n2; +} + +/* AspectPoint + * Scales a point to be preview-sized instead of screen-sized. + */ + +void AspectPoint( + RECT* rectPreview, + POINT* pt) +{ + pt->x = AspectScale(rectPreview->right, gcxScreen, pt->x); + pt->y = AspectScale(rectPreview->bottom, gcyScreen, pt->y); +} diff --git a/src/propsheet/product.pbxproj b/src/propsheet/product.pbxproj new file mode 100644 index 000000000..1568d31e4 --- /dev/null +++ b/src/propsheet/product.pbxproj @@ -0,0 +1,9 @@ + + + + + + $(Sku_Langs) + + + \ No newline at end of file diff --git a/src/propsheet/propsheet.vcxproj b/src/propsheet/propsheet.vcxproj new file mode 100644 index 000000000..583eff62b --- /dev/null +++ b/src/propsheet/propsheet.vcxproj @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + Create + ProgramDatabase + + + + + + + + + + + + + + + + + + + + + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + + + + + DynamicLibrary + + + {5D23E8E1-3C64-4CC1-A8F7-6861677F7239} + Win32Proj + propsheet + Propsheet.DLL + console + + + + + true + + + + comctl32.lib;RuntimeObject.lib;Shlwapi.lib;%(AdditionalDependencies) + console.def + type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*' + + + $(IntermediateOutputPath);..\inc;..\host;%(AdditionalIncludeDirectories) + + + $(IntermediateOutputPath);%(AdditionalIncludeDirectories) + + + + + + + + + diff --git a/src/propsheet/propsheet.vcxproj.filters b/src/propsheet/propsheet.vcxproj.filters new file mode 100644 index 000000000..32c98268a --- /dev/null +++ b/src/propsheet/propsheet.vcxproj.filters @@ -0,0 +1,114 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + + + Resource Files + + + + + Source Files + + + diff --git a/src/propsheet/registry.cpp b/src/propsheet/registry.cpp new file mode 100644 index 000000000..4034ec24e --- /dev/null +++ b/src/propsheet/registry.cpp @@ -0,0 +1,959 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + registry.c + +Abstract: + + This file contains functions to read and write values + to the registry. + +Author: + + Jerry Shea (JerrySh) 30-Sep-1994 + +Revision History: + +--*/ + +#include "precomp.h" +#include +#include +#include +#include "../inc/conattrs.hpp" +#pragma hdrstop + +#define CONSOLE_REGISTRY_CURRENTPAGE (L"CurrentPage") + +extern BOOL g_fEditKeys; + +BOOL GetConsoleBoolValue(__in PCWSTR pszValueName, __in BOOL fDefault) +{ + return SHRegGetBoolUSValue(CONSOLE_REGISTRY_STRING, + pszValueName, + FALSE /*fIgnoreHKCU*/, + fDefault); +} + +/*++ + +Routine Description: + + This routine allocates a state info structure and fill it in with + default values. + +Arguments: + + none + +Return Value: + + pStateInfo - pointer to structure to receive information + +--*/ + +VOID InitRegistryValues( + __out PCONSOLE_STATE_INFO pStateInfo) +{ + pStateInfo->ScreenAttributes = 0x07; // white on black + pStateInfo->PopupAttributes = 0xf5; // purple on white + pStateInfo->InsertMode = FALSE; + pStateInfo->QuickEdit = FALSE; + pStateInfo->FullScreen = FALSE; + pStateInfo->ScreenBufferSize.X = 80; + pStateInfo->ScreenBufferSize.Y = 25; + pStateInfo->WindowSize.X = 80; + pStateInfo->WindowSize.Y = 25; + pStateInfo->WindowPosX = 0; + pStateInfo->WindowPosY = 0; + pStateInfo->AutoPosition = TRUE; + pStateInfo->FontSize.X = 0; + pStateInfo->FontSize.Y = 0; + pStateInfo->FontFamily = 0; + pStateInfo->FontWeight = 0; + pStateInfo->FaceName[0] = TEXT('\0'); + pStateInfo->CursorSize = 25; + pStateInfo->HistoryBufferSize = 25; + pStateInfo->NumberOfHistoryBuffers = 4; + pStateInfo->HistoryNoDup = 0; + + if (pStateInfo->fIsV2Console) + { + // if we're servicing a v2 console instance, default to the new color defaults + pStateInfo->ColorTable[ 0] = RGB( 12, 12, 12); // Black + pStateInfo->ColorTable[ 1] = RGB( 0, 55, 218); // Dark Blue + pStateInfo->ColorTable[ 2] = RGB( 19, 161, 14); // Dark Green + pStateInfo->ColorTable[ 3] = RGB( 58, 150, 221); // Dark Cyan + pStateInfo->ColorTable[ 4] = RGB(197, 15, 31); // Dark Red + pStateInfo->ColorTable[ 5] = RGB(136, 23, 152); // Dark Magenta + pStateInfo->ColorTable[ 6] = RGB(193, 156, 0); // Dark Yellow + pStateInfo->ColorTable[ 7] = RGB(204, 204, 204); // Dark White + pStateInfo->ColorTable[ 8] = RGB(118, 118, 118); // Bright Black + pStateInfo->ColorTable[ 9] = RGB( 59, 120, 255); // Bright Blue + pStateInfo->ColorTable[10] = RGB( 22, 198, 12); // Bright Green + pStateInfo->ColorTable[11] = RGB( 97, 214, 214); // Bright Cyan + pStateInfo->ColorTable[12] = RGB(231, 72, 86); // Bright Red + pStateInfo->ColorTable[13] = RGB(180, 0, 158); // Bright Magenta + pStateInfo->ColorTable[14] = RGB(249, 241, 165); // Bright Yellow + pStateInfo->ColorTable[15] = RGB(242, 242, 242); // White + } + else + { + pStateInfo->ColorTable[ 0] = RGB(0, 0, 0 ); + pStateInfo->ColorTable[ 1] = RGB(0, 0, 0x80); + pStateInfo->ColorTable[ 2] = RGB(0, 0x80,0 ); + pStateInfo->ColorTable[ 3] = RGB(0, 0x80,0x80); + pStateInfo->ColorTable[ 4] = RGB(0x80,0, 0 ); + pStateInfo->ColorTable[ 5] = RGB(0x80,0, 0x80); + pStateInfo->ColorTable[ 6] = RGB(0x80,0x80,0 ); + pStateInfo->ColorTable[ 7] = RGB(0xC0,0xC0,0xC0); + pStateInfo->ColorTable[ 8] = RGB(0x80,0x80,0x80); + pStateInfo->ColorTable[ 9] = RGB(0, 0, 0xFF); + pStateInfo->ColorTable[10] = RGB(0, 0xFF,0 ); + pStateInfo->ColorTable[11] = RGB(0, 0xFF,0xFF); + pStateInfo->ColorTable[12] = RGB(0xFF,0, 0 ); + pStateInfo->ColorTable[13] = RGB(0xFF,0, 0xFF); + pStateInfo->ColorTable[14] = RGB(0xFF,0xFF,0 ); + pStateInfo->ColorTable[15] = RGB(0xFF,0xFF,0xFF); + } + + pStateInfo->CodePage = OEMCP; + pStateInfo->hWnd = NULL; + pStateInfo->OriginalTitle = NULL; + pStateInfo->LinkTitle = NULL; + + // v2 console state + pStateInfo->fWrapText = TRUE; + pStateInfo->fFilterOnPaste = TRUE; + pStateInfo->fCtrlKeyShortcutsDisabled = FALSE; + pStateInfo->fLineSelection= TRUE; + pStateInfo->bWindowTransparency = BYTE_MAX; + pStateInfo->CursorType = 0; + pStateInfo->CursorColor = INVALID_COLOR; + pStateInfo->InterceptCopyPaste = FALSE; + pStateInfo->DefaultForeground = INVALID_COLOR; + pStateInfo->DefaultBackground = INVALID_COLOR; + // end v2 console state +} + +/*++ + +Routine Description: + + This routine reads in values from the registry and places them + in the supplied structure. + +Arguments: + + pStateInfo - optional pointer to structure to receive information + +Return Value: + + current page number + +--*/ + +DWORD GetRegistryValues( + __out_opt PCONSOLE_STATE_INFO pStateInfo) +{ + HKEY hCurrentUserKey, hConsoleKey, hTitleKey; + NTSTATUS Status; + DWORD dwValue, dwRet = 0, i; + WCHAR awchBuffer[LF_FACESIZE]; + + // initial values for global v2 settings + g_fForceV2 = GetConsoleBoolValue(CONSOLE_REGISTRY_FORCEV2, TRUE); + g_fEditKeys = GetConsoleBoolValue(CONSOLE_REGISTRY_EXTENDEDEDITKEY, TRUE); + + // + // Open the current user registry key and console key. + // + Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); + + if (!NT_SUCCESS(Status)) { + return 0; + } + + // + // If there is no structure to fill out, just get the current + // page and bail out. + // + + if (pStateInfo == nullptr) { + Status = RegistrySerialization::s_QueryValue(hConsoleKey, + CONSOLE_REGISTRY_CURRENTPAGE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + dwRet = dwValue; + } + + goto CloseKeys; + } + + // + // Open the console title subkey unless we're changing the defaults. + // + if (pStateInfo->Defaults) { + hTitleKey = hConsoleKey; + } else { + Status = RegistrySerialization::s_OpenKey(hConsoleKey, + pStateInfo->OriginalTitle, + &hTitleKey); + if (!NT_SUCCESS(Status)) { + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + return 0; + } + } + + // + // Initial screen fill + // + + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FILLATTR, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->ScreenAttributes = (WORD)dwValue; + } + + // + // Initial popup fill + // + + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_POPUPATTR, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->PopupAttributes = (WORD)dwValue; + } + + // + // Initial color table + // + + for (i = 0; i < 16; i++) { + StringCchPrintf(awchBuffer, + ARRAYSIZE(awchBuffer), + CONSOLE_REGISTRY_COLORTABLE, + i); + Status = RegistrySerialization::s_QueryValue(hTitleKey, + awchBuffer, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->ColorTable[i] = dwValue; + } + } + + // + // Initial insert mode. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_INSERTMODE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->InsertMode = !!dwValue; + } + + // + // Initial quick edit mode + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_QUICKEDIT, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->QuickEdit = !!dwValue; + } + + // + // Initial code page + // + + FAIL_FAST_IF(!(OEMCP != 0)); + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CODEPAGE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + if (IsValidCodePage(dwValue)) { + pStateInfo->CodePage = (UINT) dwValue; + } + } + + // + // Initial screen buffer size. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_BUFFERSIZE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->ScreenBufferSize.X = LOWORD(dwValue); + pStateInfo->ScreenBufferSize.Y = HIWORD(dwValue); + } + + // + // Initial window size. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_WINDOWSIZE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->WindowSize.X = LOWORD(dwValue); + pStateInfo->WindowSize.Y = HIWORD(dwValue); + } + + // + // Initial window position. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_WINDOWPOS, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->WindowPosX = (SHORT)LOWORD(dwValue); + pStateInfo->WindowPosY = (SHORT)HIWORD(dwValue); + pStateInfo->AutoPosition = FALSE; + } + + // + // Initial font size + // + + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FONTSIZE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->FontSize.X = LOWORD(dwValue); + pStateInfo->FontSize.Y = HIWORD(dwValue); + } + + // + // Initial font family. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FONTFAMILY, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->FontFamily = dwValue; + } + + // + // Initial font weight. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FONTWEIGHT, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->FontWeight = dwValue; + } + + // + // Initial font face name. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FACENAME, + sizeof(awchBuffer), + REG_SZ, + (PBYTE)awchBuffer, + NULL); + if (NT_SUCCESS(Status)) { + RtlCopyMemory(pStateInfo->FaceName, awchBuffer, sizeof(awchBuffer)); + } + + // + // Initial cursor size. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CURSORSIZE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->CursorSize = dwValue; + } + + // + // Initial history buffer size. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_HISTORYSIZE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->HistoryBufferSize = dwValue; + } + + // + // Initial number of history buffers. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_HISTORYBUFS, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->NumberOfHistoryBuffers = dwValue; + } + + // + // Initial history duplication mode. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_HISTORYNODUP, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->HistoryNoDup = dwValue; + } + + // + // Initial text wrapping. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_LINEWRAP, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->fWrapText = dwValue; + } + + // + // Initial filter on paste. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_FILTERONPASTE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->fFilterOnPaste = dwValue; + } + + // + // Initial ctrl shortcuts disabled. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CTRLKEYSHORTCUTS_DISABLED, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->fCtrlKeyShortcutsDisabled = dwValue; + } + + // + // Initial line selection. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_LINESELECTION, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->fLineSelection = dwValue; + } + + // + // Initial transparency. + // + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_WINDOWALPHA, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + if (dwValue <= BYTE_MAX) + { + pStateInfo->bWindowTransparency = (BYTE)dwValue; + } + } + + // Initial Cursor Color + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CURSORCOLOR, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->CursorColor = dwValue; + } + // Initial Cursor Shape + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_CURSORTYPE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) + { + pStateInfo->CursorType = dwValue; + } + + // Initial Intercept Copy Paste + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_INTERCEPTCOPYPASTE, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) + { + pStateInfo->InterceptCopyPaste = !!dwValue; + } + + // Initial Foreground Color + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_DEFAULTFOREGROUND, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->DefaultForeground = dwValue; + } + + // Initial Background Color + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_DEFAULTBACKGROUND, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->DefaultBackground = dwValue; + } + + // Initial Background Color + Status = RegistrySerialization::s_QueryValue(hTitleKey, + CONSOLE_REGISTRY_TERMINALSCROLLING, + sizeof(dwValue), + REG_DWORD, + (PBYTE)&dwValue, + NULL); + if (NT_SUCCESS(Status)) { + pStateInfo->TerminalScrolling = dwValue; + } + + // + // Close the registry keys + // + + if (hTitleKey != hConsoleKey) { + RegCloseKey(hTitleKey); + } + +CloseKeys: + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + + return dwRet; +} + +VOID SetGlobalRegistryValues() +{ + // save global v2 settings + const DWORD dwForceV2 = g_fForceV2; + SHSetValue(HKEY_CURRENT_USER, + CONSOLE_REGISTRY_STRING, + CONSOLE_REGISTRY_FORCEV2, + REG_DWORD, + (LPBYTE)&dwForceV2, + sizeof(dwForceV2)); + + const DWORD dwEditKeys = g_fEditKeys; + SHSetValue(HKEY_CURRENT_USER, + CONSOLE_REGISTRY_STRING, + CONSOLE_REGISTRY_EXTENDEDEDITKEY, + REG_DWORD, + (LPBYTE)&dwEditKeys, + sizeof(dwEditKeys)); +} + +/*++ + +Routine Description: + + This routine writes values to the registry from the supplied + structure. + +Arguments: + + pStateInfo - optional pointer to structure containing information + dwPage - current page number + +Return Value: + + none + +--*/ + +VOID SetRegistryValues( + PCONSOLE_STATE_INFO pStateInfo, + DWORD dwPage) +{ + HKEY hCurrentUserKey, hConsoleKey, hTitleKey; + NTSTATUS Status; + DWORD dwValue, i; + WCHAR awchBuffer[LF_FACESIZE]; + + // + // Open the current user registry key and console registry key. + // + Status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUserKey, &hConsoleKey); + + if (!NT_SUCCESS(Status)) { + return; + } + + // + // Save the current page. + // + LOG_IF_FAILED(RegistrySerialization::s_SetValue(hConsoleKey, + CONSOLE_REGISTRY_CURRENTPAGE, + REG_DWORD, + (BYTE*)&dwPage, + sizeof(dwPage))); + + // + // Open the console title subkey unless we're changing the defaults. + // + if (pStateInfo->Defaults) { + hTitleKey = hConsoleKey; + } else { + Status = RegistrySerialization::s_CreateKey(hConsoleKey, + pStateInfo->OriginalTitle, + &hTitleKey); + if (!NT_SUCCESS(Status)) { + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); + return; + } + } + + // + // Save screen and popup colors and color table + // + + dwValue = pStateInfo->ScreenAttributes; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FILLATTR, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->PopupAttributes; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_POPUPATTR, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + for (i = 0; i < 16; i++) { + dwValue = pStateInfo->ColorTable[i]; + if (SUCCEEDED(StringCchPrintf(awchBuffer, ARRAYSIZE(awchBuffer), CONSOLE_REGISTRY_COLORTABLE, i))) + { + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + awchBuffer, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + } + } + + // + // Save insert, quickedit, and fullscreen mode settings + // + + dwValue = pStateInfo->InsertMode; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_INSERTMODE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->QuickEdit; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_QUICKEDIT, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + FAIL_FAST_IF(!(OEMCP != 0)); + if (g_fEastAsianSystem) { + dwValue = (DWORD) pStateInfo->CodePage; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_CODEPAGE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + } + + // + // Save screen buffer size + // + + dwValue = MAKELONG(pStateInfo->ScreenBufferSize.X, + pStateInfo->ScreenBufferSize.Y); + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_BUFFERSIZE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + // + // Save window size + // + + dwValue = MAKELONG(pStateInfo->WindowSize.X, + pStateInfo->WindowSize.Y); + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWSIZE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + // + // Save window position + // + + if (pStateInfo->AutoPosition) { + LOG_IF_FAILED(RegistrySerialization::s_DeleteValue(hTitleKey, CONSOLE_REGISTRY_WINDOWPOS)); + } + else + { + dwValue = MAKELONG(pStateInfo->WindowPosX, + pStateInfo->WindowPosY); + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWPOS, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + } + + // + // Save font size, family, weight, and face name + // + + dwValue = MAKELONG(pStateInfo->FontSize.X, + pStateInfo->FontSize.Y); + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FONTSIZE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->FontFamily; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FONTFAMILY, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->FontWeight; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FONTWEIGHT, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FACENAME, + REG_SZ, + (BYTE*)(pStateInfo->FaceName), + (DWORD)(wcslen(pStateInfo->FaceName) + 1) * sizeof(TCHAR))); + + // + // Save cursor size + // + + dwValue = pStateInfo->CursorSize; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_CURSORSIZE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + // + // Save history buffer size and number + // + + dwValue = pStateInfo->HistoryBufferSize; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_HISTORYSIZE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->NumberOfHistoryBuffers; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_HISTORYBUFS, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->HistoryNoDup; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_HISTORYNODUP, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + // Save per-title V2 console state + dwValue = pStateInfo->fWrapText; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_LINEWRAP, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->fFilterOnPaste; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_FILTERONPASTE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->fCtrlKeyShortcutsDisabled; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_CTRLKEYSHORTCUTS_DISABLED, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->fLineSelection; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_LINESELECTION, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->bWindowTransparency; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_WINDOWALPHA, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + SetGlobalRegistryValues(); + + // Save cursor type and color + dwValue = pStateInfo->CursorType; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_CURSORTYPE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + dwValue = pStateInfo->CursorColor; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_CURSORCOLOR, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + dwValue = pStateInfo->InterceptCopyPaste; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_INTERCEPTCOPYPASTE, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + dwValue = pStateInfo->TerminalScrolling; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_TERMINALSCROLLING, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->DefaultForeground; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_DEFAULTFOREGROUND, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + dwValue = pStateInfo->DefaultBackground; + LOG_IF_FAILED(RegistrySerialization::s_UpdateValue(hConsoleKey, + hTitleKey, + CONSOLE_REGISTRY_DEFAULTBACKGROUND, + REG_DWORD, + (BYTE*)&dwValue, + sizeof(dwValue))); + + // + // Close the registry keys + // + if (hTitleKey != hConsoleKey) { + RegCloseKey(hTitleKey); + } + + RegCloseKey(hConsoleKey); + RegCloseKey(hCurrentUserKey); +} diff --git a/src/propsheet/sources b/src/propsheet/sources new file mode 100644 index 000000000..8b47d1e53 --- /dev/null +++ b/src/propsheet/sources @@ -0,0 +1,109 @@ +!include ..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Property Sheet +# ------------------------------------- + +# This module draws the Properties or Defaults page that allows +# the user to configure their settings in a running console session. + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = console +TARGETTYPE = DYNLINK +TARGETEXT = dll +UMTYPE = windows +TARGET_DESTINATION = retail +DLLENTRY = _DllMainCRTStartup + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DISOLATION_AWARE_ENABLED -DWIN32 -D_WIN32 -DSTRICT_CONST -DNT + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +MSC_WARNING_LEVEL = /W3 /WX + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = precomp.h +PRECOMPILED_PCH = precomp.pch +PRECOMPILED_OBJ = precomp.obj + +SOURCES = \ + dll.cpp \ + util.cpp \ + console.cpp \ + globals.cpp \ + fontdlg.cpp \ + OptionsPage.cpp \ + ColorsPage.cpp \ + ColorControl.cpp \ + ColorsPage.cpp \ + LayoutPage.cpp \ + TerminalPage.cpp \ + init.cpp \ + misc.cpp \ + preview.cpp \ + registry.cpp \ + dbcs.cpp \ + strid.mc \ + PropSheetHandler.cpp \ + console.rc + +INCLUDES = \ + $(INCLUDES); \ + $(ONECOREBASE_PRIVATE_WIL_INC_PATH_L); \ + ..\inc; \ + ..\host; \ + +TARGETLIBS = \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(ONECORE_SDK_LIB_VPATH)\onecoreuap.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecoreuap_internal.lib \ + $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-font-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-dialogbox-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-gui-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-misc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-window-l1.lib\ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-object-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-cursor-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-dc-access-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-syscolors-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-sysparams-l1.lib\ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-shell-shell32-l1.lib \ + $(ONECOREWINDOWS_INTERNAL_LIB_PATH_L)\ext-ms-win-gdi-internal-desktop-l1-1-1.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\propslib\$(O)\conprops.lib \ + +# ------------------------------------- +# Side-by-side Manifesting +# ------------------------------------- + +SXS_APPLICATION_MANIFEST = WindowsShell.Manifest +SXS_MANIFEST_SOURCE = . diff --git a/src/propsheet/strid.mc b/src/propsheet/strid.mc new file mode 100644 index 000000000..902be4c11 --- /dev/null +++ b/src/propsheet/strid.mc @@ -0,0 +1,4 @@ +MessageId=1000 SymbolicName=MSG_FONTSTRING_FORMATTING +Language=English +%1: %2%0 +. diff --git a/src/propsheet/util.cpp b/src/propsheet/util.cpp new file mode 100644 index 000000000..c4fa42194 --- /dev/null +++ b/src/propsheet/util.cpp @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "shlwapi.h" + +// Version detection code for ComCtl32 copied/adapter from +// https://msdn.microsoft.com/en-us/library/windows/desktop/hh298349(v=vs.85).aspx#DllGetVersion + +#define PACKVERSION(major,minor) MAKELONG(minor,major) + +static DWORD GetVersion(_In_ PCWSTR pwszDllName) +{ + DWORD dwVersion = 0; + + // We have to call for ComCtl32.dll without a path name so Fusion SxS will redirect us + // if it thinks we are manifested properly and Fusion is enabled in this process space. + HINSTANCE const hinstDll = LoadLibrary(pwszDllName); + + if (nullptr != hinstDll) + { + DLLGETVERSIONPROC const pDllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(hinstDll, "DllGetVersion"); + + // Because some DLLs might not implement this function, you must test for + // it explicitly. Depending on the particular DLL, the lack of a DllGetVersion + // function can be a useful indicator of the version. + + if (nullptr != pDllGetVersion) + { + DLLVERSIONINFO dvi = {0}; + dvi.cbSize = sizeof(dvi); + + if (SUCCEEDED((*pDllGetVersion)(&dvi))) + { + dwVersion = PACKVERSION(dvi.dwMajorVersion, dvi.dwMinorVersion); + } + } + + FreeLibrary(hinstDll); + } + return dwVersion; +} + +static bool IsComCtlV6Present() +{ + PCWSTR pwszDllName = L"ComCtl32.dll"; + DWORD const dwVer = GetVersion(pwszDllName); + DWORD const dwTarget = PACKVERSION(6, 0); + + if (dwVer >= dwTarget) + { + // This version of ComCtl32.dll is version 6.0 or later. + return true; + } + else + { + // Proceed knowing that version 6.0 or later additions are not available. + // Use an alternate approach for older the DLL version. + return false; + } +} + +BOOL InitializeConsoleState() +{ + RegisterClasses(ghInstance); + OEMCP = GetOEMCP(); + g_fIsComCtlV6Present = IsComCtlV6Present(); + + return NT_SUCCESS(InitializeDbcsMisc()); +} + +void UninitializeConsoleState() +{ + if (g_fHostedInFileProperties && gpStateInfo->LinkTitle != nullptr) + { + // If we're in the file props dialog and have an allocated title, we need to free it. Outside of the file props + // dlg, the caller of ConsolePropertySheet() owns the lifetime. + CoTaskMemFree(gpStateInfo->LinkTitle); + gpStateInfo->LinkTitle = nullptr; + } + + DestroyDbcsMisc(); + UnregisterClasses(ghInstance); +} + +void UpdateApplyButton(const HWND hDlg) +{ + if (g_fHostedInFileProperties) + { + PropSheet_Changed(GetParent(hDlg), hDlg); + } +} diff --git a/src/propsheet/windowsshell.manifest b/src/propsheet/windowsshell.manifest new file mode 100644 index 000000000..52b7e854c --- /dev/null +++ b/src/propsheet/windowsshell.manifest @@ -0,0 +1,33 @@ + + + +Windows Shell + + + + + + + + + + + + + + + true + + + diff --git a/src/propslib/RegistrySerialization.cpp b/src/propslib/RegistrySerialization.cpp new file mode 100644 index 000000000..9be4d052b --- /dev/null +++ b/src/propslib/RegistrySerialization.cpp @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "RegistrySerialization.hpp" + +#pragma hdrstop + +#define SET_FIELD_AND_SIZE(x) FIELD_OFFSET(Settings, x), RTL_FIELD_SIZE(Settings, x) + +#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) + +DWORD RegistrySerialization::ToWin32RegistryType(const _RegPropertyType type) +{ + switch (type) + { + case _RegPropertyType::Boolean: + case _RegPropertyType::Dword: + case _RegPropertyType::Word: + case _RegPropertyType::Byte: + case _RegPropertyType::Coordinate: + return REG_DWORD; + case _RegPropertyType::String: + return REG_SZ; + default: + return REG_NONE; + } +} + +// Registry settings to load (not all of them, some are special) +const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_PropertyMappings[] = +{ + //+---------------------------------+-----------------------------------------------+-----------------------------------------------+ + //| Property type | Property Name | Corresponding Settings field | + { _RegPropertyType::Word, CONSOLE_REGISTRY_POPUPATTR, SET_FIELD_AND_SIZE(_wPopupFillAttribute), }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_INSERTMODE, SET_FIELD_AND_SIZE(_bInsertMode) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_LINESELECTION, SET_FIELD_AND_SIZE(_bLineSelection) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_FILTERONPASTE, SET_FIELD_AND_SIZE(_fFilterOnPaste) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_LINEWRAP, SET_FIELD_AND_SIZE(_bWrapText) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_CTRLKEYSHORTCUTS_DISABLED, SET_FIELD_AND_SIZE(_fCtrlKeyShortcutsDisabled) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_QUICKEDIT, SET_FIELD_AND_SIZE(_bQuickEdit) }, + { _RegPropertyType::Byte, CONSOLE_REGISTRY_WINDOWALPHA, SET_FIELD_AND_SIZE(_bWindowAlpha) }, + { _RegPropertyType::Coordinate, CONSOLE_REGISTRY_FONTSIZE, SET_FIELD_AND_SIZE(_dwFontSize) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_FONTFAMILY, SET_FIELD_AND_SIZE(_uFontFamily) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_FONTWEIGHT, SET_FIELD_AND_SIZE(_uFontWeight) }, + { _RegPropertyType::String, CONSOLE_REGISTRY_FACENAME, SET_FIELD_AND_SIZE(_FaceName) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_CURSORSIZE, SET_FIELD_AND_SIZE(_uCursorSize) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_HISTORYSIZE, SET_FIELD_AND_SIZE(_uHistoryBufferSize) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_HISTORYBUFS, SET_FIELD_AND_SIZE(_uNumberOfHistoryBuffers) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_HISTORYNODUP, SET_FIELD_AND_SIZE(_bHistoryNoDup) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_SCROLLSCALE, SET_FIELD_AND_SIZE(_uScrollScale) }, + { _RegPropertyType::Word, CONSOLE_REGISTRY_FILLATTR, SET_FIELD_AND_SIZE(_wFillAttribute) }, + { _RegPropertyType::Coordinate, CONSOLE_REGISTRY_BUFFERSIZE, SET_FIELD_AND_SIZE(_dwScreenBufferSize) }, + { _RegPropertyType::Coordinate, CONSOLE_REGISTRY_WINDOWSIZE, SET_FIELD_AND_SIZE(_dwWindowSize) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_TRIMZEROHEADINGS, SET_FIELD_AND_SIZE(_fTrimLeadingZeros) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_ENABLE_COLOR_SELECTION, SET_FIELD_AND_SIZE(_fEnableColorSelection) }, + { _RegPropertyType::Coordinate, CONSOLE_REGISTRY_WINDOWPOS, SET_FIELD_AND_SIZE(_dwWindowOrigin) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_CURSORCOLOR, SET_FIELD_AND_SIZE(_CursorColor) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_CURSORTYPE, SET_FIELD_AND_SIZE(_CursorType) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_INTERCEPTCOPYPASTE, SET_FIELD_AND_SIZE(_fInterceptCopyPaste) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_DEFAULTFOREGROUND, SET_FIELD_AND_SIZE(_DefaultForeground) }, + { _RegPropertyType::Dword, CONSOLE_REGISTRY_DEFAULTBACKGROUND, SET_FIELD_AND_SIZE(_DefaultBackground) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_TERMINALSCROLLING, SET_FIELD_AND_SIZE(_TerminalScrolling) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) }, + { _RegPropertyType::Boolean, CONSOLE_REGISTRY_COPYCOLOR, SET_FIELD_AND_SIZE(_fCopyColor) } + +}; +const size_t RegistrySerialization::s_PropertyMappingsSize = ARRAYSIZE(s_PropertyMappings); + +// Global registry settings to load +const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_GlobalPropMappings[] = +{ + //+---------------------------------+-----------------------------------------------+-----------------------------------------------+ + //| Property type | Property Name | Corresponding Settings field | + { _RegPropertyType::Dword, CONSOLE_REGISTRY_VIRTTERM_LEVEL, SET_FIELD_AND_SIZE(_dwVirtTermLevel), } +}; +const size_t RegistrySerialization::s_GlobalPropMappingsSize = ARRAYSIZE(s_GlobalPropMappings); + +// Routine Description: +// - Reads number from the registry and applies it to the given property if the value exists +// Supports: Dword, Word, Byte, Boolean, and Coordinate +// Arguments: +// - hKey - Registry key to read from +// - pPropMap - Contains property information to use in looking up/storing value data +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_LoadRegDword(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) +{ + // find offset into destination structure for this numerical value + PBYTE const pbField = (PBYTE)pSettings + pPropMap->dwFieldOffset; + + // attempt to load number into this field + // If we're not successful, it's ok. Just don't fill it. + DWORD dwValue; + NTSTATUS Status = s_QueryValue(hKey, + pPropMap->pwszValueName, + sizeof(dwValue), + ToWin32RegistryType(pPropMap->propertyType), + (PBYTE)& dwValue, + nullptr); + if (NT_SUCCESS(Status)) + { + switch (pPropMap->propertyType) + { + case _RegPropertyType::Dword: + { + DWORD* const pdField = (DWORD*)pbField; + *pdField = dwValue; + break; + } + case _RegPropertyType::Word: + { + WORD* const pwField = (WORD*)pbField; + *pwField = (WORD)dwValue; + break; + } + case _RegPropertyType::Boolean: + { + *pbField = !!dwValue; + break; + } + case _RegPropertyType::Byte: + { + *pbField = LOBYTE(dwValue); + break; + } + case _RegPropertyType::Coordinate: + { + PCOORD pcoordField = (PCOORD)pbField; + pcoordField->X = LOWORD(dwValue); + pcoordField->Y = HIWORD(dwValue); + break; + } + } + } + + return Status; +} + +// Routine Description: +// - Reads string from the registry and applies it to the given property if the value exists +// Arguments: +// - hKey - Registry key to read from +// - pPropMap - Contains property information to use in looking up/storing value data +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_LoadRegString(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings) +{ + // find offset into destination structure for this numerical value + PBYTE const pbField = (PBYTE)pSettings + pPropMap->dwFieldOffset; + + // number of characters within the field + size_t const cchField = pPropMap->cbFieldSize / sizeof(WCHAR); + + PWCHAR pwchString = new(std::nothrow) WCHAR[cchField]; + NTSTATUS Status = NT_TESTNULL(pwchString); + if (NT_SUCCESS(Status)) + { + Status = s_QueryValue(hKey, + pPropMap->pwszValueName, + (DWORD)(cchField) * sizeof(WCHAR), + ToWin32RegistryType(pPropMap->propertyType), + (PBYTE)pwchString, + nullptr); + if (NT_SUCCESS(Status)) + { + // ensure pwchString is null terminated + pwchString[cchField - 1] = UNICODE_NULL; + + Status = StringCchCopyW((PWSTR)pbField, cchField, pwchString); + } + + delete[] pwchString; + } + + return Status; +} + +#pragma region Helpers + +// Routine Description: +// - Opens the root console key from HKCU +// Arguments: +// - phCurrentUserKey - Returns a handle to the HKCU root +// - phConsoleKey - Returns a handle to the Console subkey +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _Out_ HKEY* phConsoleKey) +{ + // Always set an output value. It will be made valid before the end if everything succeeds. + *phCurrentUserKey = static_cast(INVALID_HANDLE_VALUE); + *phConsoleKey = static_cast(INVALID_HANDLE_VALUE); + + wil::unique_hkey currentUserKey; + wil::unique_hkey consoleKey; + + // Open the current user registry key. + NTSTATUS Status = NTSTATUS_FROM_WIN32(RegOpenCurrentUser(KEY_READ | KEY_WRITE, ¤tUserKey)); + + if (NT_SUCCESS(Status)) + { + // Open the console registry key. + Status = s_OpenKey(currentUserKey.get(), CONSOLE_REGISTRY_STRING, &consoleKey); + + // If we can't open the console registry key, create one and open it. + if (NTSTATUS_FROM_WIN32(ERROR_FILE_NOT_FOUND) == Status) + { + Status = s_CreateKey(currentUserKey.get(), CONSOLE_REGISTRY_STRING, &consoleKey); + } + + // If we're successful, give the keys back. + if (NT_SUCCESS(Status)) + { + *phCurrentUserKey = currentUserKey.release(); + *phConsoleKey = consoleKey.release(); + } + } + + return Status; +} + +// Routine Description: +// - Opens a subkey of the given key +// Arguments: +// - hKey - Handle to a registry key +// - pwszSubKey - String name of sub key +// - phResult - Filled with handle to the sub key if available. Check return status. +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) +{ + return NTSTATUS_FROM_WIN32(RegOpenKeyW(hKey, pwszSubKey, phResult)); +} + +// Routine Description: +// - Deletes the value under a given key +// Arguments: +// - hKey - Handle to a registry key +// - pwszValueName - String name of value to delete under that key +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_DeleteValue(const HKEY hKey, _In_ PCWSTR const pwszValueName) +{ + return NTSTATUS_FROM_WIN32(RegDeleteKeyValueW(hKey, nullptr, pwszValueName)); +} + +// Routine Description: +// - Creates a subkey of the given key +// This function creates keys with read/write access. +// Arguments: +// - hKey - Handle to a registry key +// - pwszSubKey - String name of sub key +// - phResult - Filled with handle to the sub key if created/opened successfully. Check return status. +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_CreateKey(const HKEY hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult) +{ + return NTSTATUS_FROM_WIN32(RegCreateKeyW(hKey, pwszSubKey, phResult)); +} + +// Routine Description: +// - Sets a value for the given key +// Arguments: +// - hKey - Handle to a registry key +// - pwszValueName - Name of the value to set +// - dwType - Type of the value being set (see: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724884(v=vs.85).aspx) +// - pbData - Pointer to byte stream of data to set into this value +// - cbDataLength - The length in bytes of the data provided +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_SetValue(const HKEY hKey, + _In_ PCWSTR const pValueName, + const DWORD dwType, + _In_reads_bytes_(cbDataLength) BYTE* const pbData, + const DWORD cbDataLength) +{ + return NTSTATUS_FROM_WIN32(RegSetKeyValueW(hKey, + nullptr, + pValueName, + dwType, + pbData, + cbDataLength)); +} + +// Routine Description: +// - Queries a value for the given key +// Arguments: +// - hKey - Handle to a registry key +// - pwszValueName - Name of the value to query +// - cbValueLength - Length of the provided data buffer. +// - regType - the type of the registry key. +// - pbData - Pointer to byte stream of data to fill with the registry value data. +// - pcbDataLength - Number of bytes filled in the given data buffer +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_QueryValue(const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD cbValueLength, + const DWORD regType, + _Out_writes_bytes_(cbValueLength) BYTE* const pbData, + _Out_opt_ _Out_range_(0, cbValueLength) DWORD* const pcbDataLength) +{ + DWORD cbData = cbValueLength; + + DWORD actualRegType = 0; + LONG const Result = RegQueryValueExW(hKey, + pwszValueName, + nullptr, + &actualRegType, + pbData, + &cbData); + if (ERROR_FILE_NOT_FOUND != Result && + actualRegType != regType) + { + return STATUS_OBJECT_TYPE_MISMATCH; + } + + if (nullptr != pcbDataLength) + { + *pcbDataLength = cbData; + } + + return NTSTATUS_FROM_WIN32(Result); +} + +// Routine Description: +// - Enumerates the values for the given key +// Arguments: +// - hKey - Handle to a registry key +// - dwIndex - Index of value within this key to return +// - cbValueLength - Length of the provided value name buffer. +// - pwszValueName - Value name buffer to be filled with null terminated string name of value. +// - cbDataLength - Length of the provided value data buffer. +// - pbData - Value data buffer to be filled based on data type. Will be null terminated for string types. (REG_SZ, REG_MULTI_SZ, REG_EXPAND_SZ) +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_EnumValue(const HKEY hKey, + const DWORD dwIndex, + const DWORD cbValueLength, + _Out_writes_bytes_(cbValueLength) PWSTR const pwszValueName, + const DWORD cbDataLength, + _Out_writes_bytes_(cbDataLength) BYTE* const pbData) +{ + DWORD cchValueName = cbValueLength / sizeof(WCHAR); + DWORD cbData = cbDataLength; + +#pragma prefast(suppress: 26015, "prefast doesn't realize that cbData == cbDataLength and cchValueName == cbValueLength/2") + return NTSTATUS_FROM_WIN32(RegEnumValueW(hKey, + dwIndex, + pwszValueName, + &cchValueName, + nullptr, + nullptr, + pbData, + &cbData)); +} + +// Routine Description: +// - Updates the value in a given key +// - NOTE: For the console registry, if we're filling a console subkey and the default matches, the subkey copy will be deleted. +// We only store settings in subkeys if they differ from the defaults. +// Arguments: +// - hConsoleKey - Handle to the default console key +// - hKey - Handle to the console subkey for this particular console +// - pwszValueName - Name of the value within the default and subkeys to check/update. +// - dwType - Type of the value being set (see: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724884(v=vs.85).aspx) +// - pbData - Value data buffer to be stored into the registry. +// - cbDataLength - Length of the provided value data buffer. +// Return Value: +// - STATUS_SUCCESSFUL or appropriate NTSTATUS reply for registry operations. +[[nodiscard]] +NTSTATUS RegistrySerialization::s_UpdateValue(const HKEY hConsoleKey, + const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD dwType, + _In_reads_bytes_(cbDataLength) BYTE* pbData, + const DWORD cbDataLength) +{ + NTSTATUS Status = STATUS_UNSUCCESSFUL; // This value won't be used, added to avoid compiler warnings. + BYTE* Data = new(std::nothrow) BYTE[cbDataLength]; + if (Data != nullptr) + { + // If this is not the main console key but the value is the same, + // delete it. Otherwise, set it. + bool fDeleteKey = false; + if (hConsoleKey != hKey) + { + Status = s_QueryValue(hConsoleKey, pwszValueName, sizeof(Data), dwType, Data, nullptr); + if (NT_SUCCESS(Status)) + { + fDeleteKey = (memcmp(pbData, Data, cbDataLength) == 0); + } + } + + if (fDeleteKey) + { + Status = s_DeleteValue(hKey, pwszValueName); + } + else + { + Status = s_SetValue(hKey, pwszValueName, dwType, pbData, cbDataLength); + } + delete[] Data; + } + + return Status; +} + +[[nodiscard]] +NTSTATUS RegistrySerialization::s_OpenCurrentUserConsoleTitleKey(_In_ PCWSTR const title, + _Out_ HKEY* phCurrentUserKey, + _Out_ HKEY* phConsoleKey, + _Out_ HKEY* phTitleKey) +{ + NTSTATUS Status = NTSTATUS_FROM_WIN32(RegOpenKeyW(HKEY_CURRENT_USER, + nullptr, + phCurrentUserKey)); + if (NT_SUCCESS(Status)) + { + Status = RegistrySerialization::s_CreateKey(*phCurrentUserKey, + CONSOLE_REGISTRY_STRING, + phConsoleKey); + if (NT_SUCCESS(Status)) + { + Status = RegistrySerialization::s_CreateKey(*phConsoleKey, title, phTitleKey); + if (!NT_SUCCESS(Status)) + { + RegCloseKey(*phConsoleKey); + RegCloseKey(*phCurrentUserKey); + } + //else all keys were created/opened successfully, and we'll return success + } + else + { + RegCloseKey(*phCurrentUserKey); + } + } + return Status; +} + + + +#pragma endregion diff --git a/src/propslib/RegistrySerialization.hpp b/src/propslib/RegistrySerialization.hpp new file mode 100644 index 000000000..57221e748 --- /dev/null +++ b/src/propslib/RegistrySerialization.hpp @@ -0,0 +1,122 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- RegistrySerialization.hpp + +Abstract: +- This module is used for reading/writing registry operations + +Author(s): +- Michael Niksa (MiNiksa) 23-Jul-2014 +- Paul Campbell (PaulCam) 23-Jul-2014 +- Mike Griese (MiGrie) 04-Aug-2015 + +Revision History: +- From components of srvinit.c +- From miniksa, paulcam's Registry.cpp +--*/ + +#pragma once + +class Settings; + +class RegistrySerialization +{ +public: + + // The following registry methods remain public for DBCS and EUDC lookups. + [[nodiscard]] + static NTSTATUS s_OpenKey(_In_opt_ HKEY const hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult); + + [[nodiscard]] + static NTSTATUS s_QueryValue(const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD cbValueLength, + const DWORD regType, + _Out_writes_bytes_(cbValueLength) BYTE* const pbData, + _Out_opt_ _Out_range_(0, cbValueLength) DWORD* const pcbDataLength); + + [[nodiscard]] + static NTSTATUS s_EnumValue(const HKEY hKey, + const DWORD dwIndex, + const DWORD cbValueLength, + _Out_writes_bytes_(cbValueLength) PWSTR const pwszValueName, + const DWORD cbDataLength, + _Out_writes_bytes_(cbDataLength) BYTE* const pbData); + + [[nodiscard]] + static NTSTATUS s_OpenConsoleKey(_Out_ HKEY* phCurrentUserKey, _Out_ HKEY* phConsoleKey); + + [[nodiscard]] + static NTSTATUS s_CreateKey(const HKEY hKey, _In_ PCWSTR const pwszSubKey, _Out_ HKEY* const phResult); + + [[nodiscard]] + static NTSTATUS s_DeleteValue(const HKEY hKey, _In_ PCWSTR const pwszValueName); + + [[nodiscard]] + static NTSTATUS s_SetValue(const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD dwType, + _In_reads_bytes_(cbDataLength) BYTE* const pbData, + const DWORD cbDataLength); + + [[nodiscard]] + static NTSTATUS s_UpdateValue(const HKEY hConsoleKey, + const HKEY hKey, + _In_ PCWSTR const pwszValueName, + const DWORD dwType, + _In_reads_bytes_(dwDataLength) BYTE* pbData, + const DWORD dwDataLength); + + [[nodiscard]] + static NTSTATUS s_OpenCurrentUserConsoleTitleKey(_In_ PCWSTR const title, + _Out_ HKEY* phCurrentUserKey, + _Out_ HKEY* phConsoleKey, + _Out_ HKEY* phTitleKey ); + + enum _RegPropertyType + { + Boolean, + Dword, + Word, + Byte, + Coordinate, + String, + }; + + static DWORD ToWin32RegistryType(const _RegPropertyType type); + + typedef struct _RegPropertyMap + { + _RegPropertyType const propertyType; + PCWSTR pwszValueName; + DWORD const dwFieldOffset; + size_t const cbFieldSize; + _RegPropertyMap( + _RegPropertyType const propertyType, + PCWSTR pwszValueName, + DWORD const dwFieldOffset, + size_t const cbFieldSize + ) : + propertyType(propertyType), + pwszValueName(pwszValueName), + dwFieldOffset(dwFieldOffset), + cbFieldSize(cbFieldSize){}; + + _RegPropertyMap & operator=( const _RegPropertyMap & ) { return *this; } + } RegPropertyMap; + + static const RegPropertyMap s_PropertyMappings[]; + static const size_t RegistrySerialization::s_PropertyMappingsSize; + + static const RegPropertyMap s_GlobalPropMappings[]; + static const size_t RegistrySerialization::s_GlobalPropMappingsSize; + + [[nodiscard]] + static NTSTATUS s_LoadRegDword(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings); + [[nodiscard]] + static NTSTATUS s_LoadRegString(const HKEY hKey, const _RegPropertyMap* const pPropMap, _In_ Settings* const pSettings); + +}; diff --git a/src/propslib/ShortcutSerialization.cpp b/src/propslib/ShortcutSerialization.cpp new file mode 100644 index 000000000..b0731b8e6 --- /dev/null +++ b/src/propslib/ShortcutSerialization.cpp @@ -0,0 +1,507 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include +#include + +#include "ShortcutSerialization.hpp" + +#pragma hdrstop + +void ShortcutSerialization::s_InitPropVarFromBool(_In_ BOOL fVal, _Out_ PROPVARIANT *ppropvar) +{ + ppropvar->vt = VT_BOOL; + ppropvar->boolVal = fVal ? VARIANT_TRUE : VARIANT_FALSE; +} + +void ShortcutSerialization::s_InitPropVarFromByte(_In_ BYTE bVal, _Out_ PROPVARIANT *ppropvar) +{ + ppropvar->vt = VT_I2; + ppropvar->iVal = bVal; +} + +void ShortcutSerialization::s_InitPropVarFromDword(_In_ DWORD dwVal, _Out_ PROPVARIANT *ppropvar) +{ + // A DWORD is a 4-byte unsigned int value, so use the ui4 member. + // DO NOT use VT_UINT, that doesn't work with PROPVARIANTs. + // see: https://docs.microsoft.com/en-us/windows/desktop/api/wtypes/ne-wtypes-varenum + // also MSFT:18312914 + ppropvar->vt = VT_UI4; + ppropvar->ulVal = dwVal; +} + +void ShortcutSerialization::s_SetLinkPropertyBoolValue(_In_ IPropertyStore *pps, _In_ REFPROPERTYKEY refPropKey,const BOOL fVal) +{ + PROPVARIANT propvarBool; + s_InitPropVarFromBool(fVal, &propvarBool); + pps->SetValue(refPropKey, propvarBool); + PropVariantClear(&propvarBool); +} + +void ShortcutSerialization::s_SetLinkPropertyByteValue(_In_ IPropertyStore *pps, _In_ REFPROPERTYKEY refPropKey,const BYTE bVal) +{ + PROPVARIANT propvarByte; + s_InitPropVarFromByte(bVal, &propvarByte); + pps->SetValue(refPropKey, propvarByte); + PropVariantClear(&propvarByte); +} + +void ShortcutSerialization::s_SetLinkPropertyDwordValue(_Inout_ IPropertyStore *pps, + _In_ REFPROPERTYKEY refPropKey, + const DWORD dwVal) +{ + PROPVARIANT propvarDword; + s_InitPropVarFromDword(dwVal, &propvarDword); + pps->SetValue(refPropKey, propvarDword); + PropVariantClear(&propvarDword); +} + +[[nodiscard]] +HRESULT ShortcutSerialization::s_GetPropertyBoolValue(_In_ IPropertyStore * const pPropStore, _In_ REFPROPERTYKEY refPropKey, _Out_ BOOL * const pfValue) +{ + PROPVARIANT propvar; + HRESULT hr = pPropStore->GetValue(refPropKey, &propvar); + // Only retrieve the value if we actually found one. If the link didn't have + // the property (eg a new property was added_, then ignore it. + if (SUCCEEDED(hr) && propvar.vt != VT_EMPTY) + { + hr = PropVariantToBoolean(propvar, pfValue); + } + + return hr; +} + +[[nodiscard]] +HRESULT ShortcutSerialization::s_GetPropertyByteValue(_In_ IPropertyStore * const pPropStore, _In_ REFPROPERTYKEY refPropKey, _Out_ BYTE * const pbValue) +{ + PROPVARIANT propvar; + HRESULT hr = pPropStore->GetValue(refPropKey, &propvar); + // Only retrieve the value if we actually found one. If the link didn't have + // the property (eg a new property was added_, then ignore it. + if (SUCCEEDED(hr) && propvar.vt != VT_EMPTY) + { + SHORT sValue; + hr = PropVariantToInt16(propvar, &sValue); + if (SUCCEEDED(hr)) + { + hr = (sValue >= 0 && sValue <= BYTE_MAX) ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) + { + *pbValue = (BYTE)sValue; + } + } + } + + return hr; +} + +[[nodiscard]] +HRESULT ShortcutSerialization::s_GetPropertyDwordValue(_Inout_ IPropertyStore * const pPropStore, _In_ REFPROPERTYKEY refPropKey, _Out_ DWORD * const pdwValue) +{ + PROPVARIANT propvar; + HRESULT hr = pPropStore->GetValue(refPropKey, &propvar); + // Only retrieve the value if we actually found one. If the link didn't have + // the property (eg a new property was added_, then ignore it. + if (SUCCEEDED(hr) && propvar.vt != VT_EMPTY) + { + DWORD dwValue; + hr = PropVariantToUInt32(propvar, &dwValue); + if (SUCCEEDED(hr)) + { + *pdwValue = dwValue; + } + } + + return hr; +} + +[[nodiscard]] +HRESULT ShortcutSerialization::s_PopulateV1Properties(_In_ IShellLink * const pslConsole, _In_ PCONSOLE_STATE_INFO pStateInfo) +{ + IShellLinkDataList *pConsoleLnkDataList; + HRESULT hr = pslConsole->QueryInterface(IID_PPV_ARGS(&pConsoleLnkDataList)); + if (SUCCEEDED(hr)) + { + // get/apply standard console properties + NT_CONSOLE_PROPS *pNtConsoleProps = nullptr; + hr = pConsoleLnkDataList->CopyDataBlock(NT_CONSOLE_PROPS_SIG, reinterpret_cast(&pNtConsoleProps)); + if (SUCCEEDED(hr)) + { + pStateInfo->ScreenAttributes = pNtConsoleProps->wFillAttribute; + pStateInfo->PopupAttributes = pNtConsoleProps->wPopupFillAttribute; + pStateInfo->ScreenBufferSize = pNtConsoleProps->dwScreenBufferSize; + pStateInfo->WindowSize = pNtConsoleProps->dwWindowSize; + pStateInfo->WindowPosX = pNtConsoleProps->dwWindowOrigin.X; + pStateInfo->WindowPosY = pNtConsoleProps->dwWindowOrigin.Y; + pStateInfo->FontSize = pNtConsoleProps->dwFontSize; + pStateInfo->FontFamily = pNtConsoleProps->uFontFamily; + pStateInfo->FontWeight = pNtConsoleProps->uFontWeight; + StringCchCopyW(pStateInfo->FaceName, ARRAYSIZE(pStateInfo->FaceName), pNtConsoleProps->FaceName); + pStateInfo->CursorSize = pNtConsoleProps->uCursorSize; + pStateInfo->FullScreen = pNtConsoleProps->bFullScreen; + pStateInfo->QuickEdit = pNtConsoleProps->bQuickEdit; + pStateInfo->InsertMode = pNtConsoleProps->bInsertMode; + pStateInfo->AutoPosition = pNtConsoleProps->bAutoPosition; + pStateInfo->HistoryBufferSize = pNtConsoleProps->uHistoryBufferSize; + pStateInfo->NumberOfHistoryBuffers = pNtConsoleProps->uNumberOfHistoryBuffers; + pStateInfo->HistoryNoDup = pNtConsoleProps->bHistoryNoDup; + CopyMemory(pStateInfo->ColorTable, pNtConsoleProps->ColorTable, sizeof(pStateInfo->ColorTable)); + + LocalFree(pNtConsoleProps); + } + + // get/apply international console properties + if (SUCCEEDED(hr)) + { + NT_FE_CONSOLE_PROPS *pNtFEConsoleProps; + if (SUCCEEDED(pConsoleLnkDataList->CopyDataBlock(NT_FE_CONSOLE_PROPS_SIG, reinterpret_cast(&pNtFEConsoleProps)))) + { + pNtFEConsoleProps->uCodePage = pStateInfo->CodePage; + LocalFree(pNtFEConsoleProps); + } + } + + pConsoleLnkDataList->Release(); + } + + return hr; +} + +[[nodiscard]] +HRESULT ShortcutSerialization::s_PopulateV2Properties(_In_ IShellLink * const pslConsole, _In_ PCONSOLE_STATE_INFO pStateInfo) +{ + IPropertyStore *pPropStoreLnk; + HRESULT hr = pslConsole->QueryInterface(IID_PPV_ARGS(&pPropStoreLnk)); + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_WrapText, &pStateInfo->fWrapText); + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_FilterOnPaste, &pStateInfo->fFilterOnPaste); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_CtrlKeyShortcutsDisabled, &pStateInfo->fCtrlKeyShortcutsDisabled); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_LineSelection, &pStateInfo->fLineSelection); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyByteValue(pPropStoreLnk, PKEY_Console_WindowTransparency, &pStateInfo->bWindowTransparency); + } + if (SUCCEEDED(hr)) + { + DWORD placeholder = 0; + hr = s_GetPropertyDwordValue(pPropStoreLnk, PKEY_Console_CursorType, &placeholder); + if (SUCCEEDED(hr)) + { + pStateInfo->CursorType = (unsigned int) placeholder; + } + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyDwordValue(pPropStoreLnk, PKEY_Console_CursorColor, &pStateInfo->CursorColor); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_InterceptCopyPaste, &pStateInfo->InterceptCopyPaste); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyDwordValue(pPropStoreLnk, PKEY_Console_DefaultForeground, &pStateInfo->DefaultForeground); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyDwordValue(pPropStoreLnk, PKEY_Console_DefaultBackground, &pStateInfo->DefaultBackground); + } + if (SUCCEEDED(hr)) + { + hr = s_GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_TerminalScrolling, &pStateInfo->TerminalScrolling); + } + + pPropStoreLnk->Release(); + } + + return hr; +} + +// Given a shortcut filename, determine what title we should use. Under normal circumstances, we rely on the shell to +// provide the correct title. However, if that fails, we'll just use the shortcut filename minus the extension. +void ShortcutSerialization::s_GetLinkTitle(_In_ PCWSTR pwszShortcutFilename, _Out_writes_(cchShortcutTitle) PWSTR pwszShortcutTitle, const size_t cchShortcutTitle) +{ + NTSTATUS Status = (cchShortcutTitle > 0) ? STATUS_SUCCESS : STATUS_INVALID_PARAMETER_2; + if (NT_SUCCESS(Status)) + { + pwszShortcutTitle[0] = L'\0'; + + WCHAR szTemp[MAX_PATH]; + Status = StringCchCopyW(szTemp, ARRAYSIZE(szTemp), pwszShortcutFilename); + if (NT_SUCCESS(Status)) + { + // Now load the localized title for the shortcut + IShellItem *psi; + HRESULT hrShellItem = SHCreateItemFromParsingName(pwszShortcutFilename, nullptr, IID_PPV_ARGS(&psi)); + if (SUCCEEDED(hrShellItem)) + { + PWSTR pwszShortcutDisplayName; + hrShellItem = psi->GetDisplayName(SIGDN_NORMALDISPLAY, &pwszShortcutDisplayName); + if (SUCCEEDED(hrShellItem)) + { + Status = StringCchCopyW(pwszShortcutTitle, cchShortcutTitle, pwszShortcutDisplayName); + CoTaskMemFree(pwszShortcutDisplayName); + } + + psi->Release(); + } + } + + if (!NT_SUCCESS(Status)) + { + // default to an extension-free version of the filename passed in + Status = StringCchCopyW(pwszShortcutTitle, cchShortcutTitle, pwszShortcutFilename); + if (NT_SUCCESS(Status)) + { + // don't care if we can't remove the extension + (void)PathCchRemoveExtension(pwszShortcutTitle, cchShortcutTitle); + } + } + } +} + +// Given a shortcut filename, retrieve IShellLink and IPersistFile itf ptrs, and ensure that the link is loaded. +[[nodiscard]] +HRESULT ShortcutSerialization::s_GetLoadedShellLinkForShortcut(_In_ PCWSTR pwszShortcutFileName, const DWORD dwMode, _COM_Outptr_ IShellLink **ppsl, _COM_Outptr_ IPersistFile **ppPf) +{ + *ppsl = nullptr; + *ppPf = nullptr; + IShellLink * psl; + HRESULT hr = SHCoCreateInstance(NULL, &CLSID_ShellLink, NULL, IID_PPV_ARGS(&psl)); + if (SUCCEEDED(hr)) + { + IPersistFile * pPf; + hr = psl->QueryInterface(IID_PPV_ARGS(&pPf)); + if (SUCCEEDED(hr)) + { + hr = pPf->Load(pwszShortcutFileName, dwMode); + if (SUCCEEDED(hr)) + { + hr = psl->QueryInterface(IID_PPV_ARGS(ppsl)); + if (SUCCEEDED(hr)) + { + hr = pPf->QueryInterface(IID_PPV_ARGS(ppPf)); + if (FAILED(hr)) + { + (*ppsl)->Release(); + *ppsl = nullptr; + } + } + } + pPf->Release(); + } + psl->Release(); + } + + return hr; +} + +// Retrieves console-only properties from the shortcut file specified in pStateInfo. Used by the console properties sheet. +[[nodiscard]] +NTSTATUS ShortcutSerialization::s_GetLinkConsoleProperties(_Inout_ PCONSOLE_STATE_INFO pStateInfo) +{ + IShellLink * psl; + IPersistFile * ppf; + HRESULT hr = s_GetLoadedShellLinkForShortcut(pStateInfo->LinkTitle, STGM_READ, &psl, &ppf); + if (SUCCEEDED(hr)) + { + hr = s_PopulateV1Properties(psl, pStateInfo); + if (SUCCEEDED(hr)) + { + hr = s_PopulateV2Properties(psl, pStateInfo); + } + + ppf->Release(); + psl->Release(); + } + return (SUCCEEDED(hr)) ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL; +} + +// Retrieves all shortcut properties from the file specified in pStateInfo. Used by conhostv2.dll +[[nodiscard]] +NTSTATUS ShortcutSerialization::s_GetLinkValues(_Inout_ PCONSOLE_STATE_INFO pStateInfo, + _Out_ BOOL * const pfReadConsoleProperties, + _Out_writes_opt_(cchShortcutTitle) PWSTR pwszShortcutTitle, + const size_t cchShortcutTitle, + _Out_writes_opt_(cchIconLocation) PWSTR pwszIconLocation, + const size_t cchIconLocation, + _Out_opt_ int * const piIcon, + _Out_opt_ int * const piShowCmd, + _Out_opt_ WORD * const pwHotKey) +{ + *pfReadConsoleProperties = false; + + if (pwszShortcutTitle && cchShortcutTitle > 0) + { + pwszShortcutTitle[0] = L'\0'; + } + + if (pwszIconLocation && cchIconLocation > 0) + { + pwszIconLocation[0] = L'\0'; + } + + IShellLink * psl; + IPersistFile * ppf; + HRESULT hr = s_GetLoadedShellLinkForShortcut(pStateInfo->LinkTitle, STGM_READ, &psl, &ppf); + if (SUCCEEDED(hr)) + { + // first, load non-console-specific shortcut properties, if requested + if (pwszShortcutTitle) + { + // note: the "LinkTitle" member actually holds the filename of the shortcut, it's just poorly named. + s_GetLinkTitle(pStateInfo->LinkTitle, pwszShortcutTitle, cchShortcutTitle); + } + + if (pwszIconLocation && piIcon) + { + hr = psl->GetIconLocation(pwszIconLocation, static_cast(cchIconLocation), piIcon); + } + + if (SUCCEEDED(hr) && piShowCmd) + { + hr = psl->GetShowCmd(piShowCmd); + } + + if (SUCCEEDED(hr) && pwHotKey) + { + hr = psl->GetHotkey(pwHotKey); + } + + if (SUCCEEDED(hr)) + { + // now load console-specific shortcut properties. note that we don't want to propagate errors out + // here, since we've historically had two outcomes from this function -- we read generic shortcut + // properties (above), and then more specific ones. if the specific ones fail, we still treat it + // like a success so that we can continue to load. + HRESULT hrProps = s_PopulateV1Properties(psl, pStateInfo); + if (SUCCEEDED(hrProps)) + { + *pfReadConsoleProperties = true; + LOG_IF_FAILED(s_PopulateV2Properties(psl, pStateInfo)); + } + } + ppf->Release(); + psl->Release(); + } + + return (SUCCEEDED(hr)) ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL; +} + + +/** +Writes the console properties out to the link it was opened from. +Arguments: + pStateInfo - pointer to structure containing information +Return Value: + A status code if something failed or S_OK +*/ +[[nodiscard]] +NTSTATUS ShortcutSerialization::s_SetLinkValues(_In_ PCONSOLE_STATE_INFO pStateInfo, const BOOL fEastAsianSystem, const BOOL fForceV2) +{ + IShellLinkW * psl; + IPersistFile * ppf; + HRESULT hr = s_GetLoadedShellLinkForShortcut(pStateInfo->LinkTitle, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, &psl, &ppf); + if (SUCCEEDED(hr)) + { + IShellLinkDataList * psldl; + hr = psl->QueryInterface(IID_PPV_ARGS(&psldl)); + if (SUCCEEDED(hr)) + { + // Now the link is loaded, generate new console settings section to replace the one in the link. + NT_CONSOLE_PROPS props; + ((LPDBLIST)&props)->cbSize = sizeof(props); + ((LPDBLIST)&props)->dwSignature = NT_CONSOLE_PROPS_SIG; + props.wFillAttribute = pStateInfo->ScreenAttributes; + props.wPopupFillAttribute = pStateInfo->PopupAttributes; + props.dwScreenBufferSize = pStateInfo->ScreenBufferSize; + props.dwWindowSize = pStateInfo->WindowSize; + props.dwWindowOrigin.X = (SHORT)pStateInfo->WindowPosX; + props.dwWindowOrigin.Y = (SHORT)pStateInfo->WindowPosY; + props.nFont = 0; + props.nInputBufferSize = 0; + props.dwFontSize = pStateInfo->FontSize; + props.uFontFamily = pStateInfo->FontFamily; + props.uFontWeight = pStateInfo->FontWeight; + CopyMemory(props.FaceName, pStateInfo->FaceName, sizeof(props.FaceName)); + props.uCursorSize = pStateInfo->CursorSize; + props.bFullScreen = pStateInfo->FullScreen; + props.bQuickEdit = pStateInfo->QuickEdit; + props.bInsertMode = pStateInfo->InsertMode; + props.bAutoPosition = pStateInfo->AutoPosition; + props.uHistoryBufferSize = pStateInfo->HistoryBufferSize; + props.uNumberOfHistoryBuffers = pStateInfo->NumberOfHistoryBuffers; + props.bHistoryNoDup = pStateInfo->HistoryNoDup; + CopyMemory(props.ColorTable, pStateInfo->ColorTable, sizeof(props.ColorTable)); + + // Store the changes back into the link... + hr = psldl->RemoveDataBlock(NT_CONSOLE_PROPS_SIG); + if (SUCCEEDED(hr)) + { + hr = psldl->AddDataBlock((LPVOID)&props); + } + + if (SUCCEEDED(hr) && fEastAsianSystem) + { + NT_FE_CONSOLE_PROPS fe_props; + ((LPDBLIST)&fe_props)->cbSize = sizeof(fe_props); + ((LPDBLIST)&fe_props)->dwSignature = NT_FE_CONSOLE_PROPS_SIG; + fe_props.uCodePage = pStateInfo->CodePage; + + hr = psldl->RemoveDataBlock(NT_FE_CONSOLE_PROPS_SIG); + if (SUCCEEDED(hr)) + { + hr = psldl->AddDataBlock((LPVOID)&fe_props); + } + } + + if (SUCCEEDED(hr)) + { + IPropertyStore * pps; + hr = psl->QueryInterface(IID_IPropertyStore, reinterpret_cast(&pps)); + if (SUCCEEDED(hr)) + { + s_SetLinkPropertyBoolValue(pps, PKEY_Console_ForceV2, fForceV2); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_WrapText, pStateInfo->fWrapText); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_FilterOnPaste, pStateInfo->fFilterOnPaste); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_CtrlKeyShortcutsDisabled, pStateInfo->fCtrlKeyShortcutsDisabled); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_LineSelection, pStateInfo->fLineSelection); + s_SetLinkPropertyByteValue(pps, PKEY_Console_WindowTransparency, pStateInfo->bWindowTransparency); + s_SetLinkPropertyDwordValue(pps, PKEY_Console_CursorType, pStateInfo->CursorType); + s_SetLinkPropertyDwordValue(pps, PKEY_Console_CursorColor, pStateInfo->CursorColor); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_InterceptCopyPaste, pStateInfo->InterceptCopyPaste); + s_SetLinkPropertyDwordValue(pps, PKEY_Console_DefaultForeground, pStateInfo->DefaultForeground); + s_SetLinkPropertyDwordValue(pps, PKEY_Console_DefaultBackground, pStateInfo->DefaultBackground); + s_SetLinkPropertyBoolValue(pps, PKEY_Console_TerminalScrolling, pStateInfo->TerminalScrolling); + hr = pps->Commit(); + pps->Release(); + } + } + + psldl->Release(); + } + + if (SUCCEEDED(hr)) + { + // Only persist changes if we've successfully made them. + hr = ppf->Save(NULL, TRUE); + } + + ppf->Release(); + psl->Release(); + } + + return (SUCCEEDED(hr)) ? STATUS_SUCCESS : STATUS_UNSUCCESSFUL; +} diff --git a/src/propslib/ShortcutSerialization.hpp b/src/propslib/ShortcutSerialization.hpp new file mode 100644 index 000000000..fe0501b52 --- /dev/null +++ b/src/propslib/ShortcutSerialization.hpp @@ -0,0 +1,77 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ShortcutSerialization.hpp + +Abstract: +- This module is used for writing console properties to the link associated + with a particular console title. + +Author(s): +- Michael Niksa (MiNiksa) 23-Jul-2014 +- Paul Campbell (PaulCam) 23-Jul-2014 +- Mike Griese (MiGrie) 04-Aug-2015 + +Revision History: +- From components of srvinit.c +- From miniksa, paulcam's Registry.cpp +--*/ + +#pragma once + +class ShortcutSerialization +{ +public: + [[nodiscard]] + static NTSTATUS s_SetLinkValues(_In_ PCONSOLE_STATE_INFO pStateInfo, const BOOL fEastAsianSystem, const BOOL fForceV2); + [[nodiscard]] + static NTSTATUS s_GetLinkConsoleProperties(_Inout_ PCONSOLE_STATE_INFO pStateInfo); + [[nodiscard]] + static NTSTATUS s_GetLinkValues(_Inout_ PCONSOLE_STATE_INFO pStateInfo, + _Out_ BOOL * const pfReadConsoleProperties, + _Out_writes_opt_(cchShortcutTitle) PWSTR pwszShortcutTitle, + const size_t cchShortcutTitle, + _Out_writes_opt_(cchIconLocation) PWSTR pwszIconLocation, + const size_t cchIconLocation, + _Out_opt_ int * const piIcon, + _Out_opt_ int * const piShowCmd, + _Out_opt_ WORD * const pwHotKey); + +private: + + + static void s_InitPropVarFromBool(_In_ BOOL fVal, _Out_ PROPVARIANT *ppropvar); + static void s_InitPropVarFromByte(_In_ BYTE bVal, _Out_ PROPVARIANT *ppropvar); + static void s_InitPropVarFromDword(_In_ DWORD dwVal, _Out_ PROPVARIANT *ppropvar); + + static void s_SetLinkPropertyBoolValue(_In_ IPropertyStore *pps, _In_ REFPROPERTYKEY refPropKey,const BOOL fVal); + static void s_SetLinkPropertyByteValue(_In_ IPropertyStore *pps, _In_ REFPROPERTYKEY refPropKey,const BYTE bVal); + static void s_SetLinkPropertyDwordValue(_In_ IPropertyStore *pps, _In_ REFPROPERTYKEY refPropKey,const DWORD dwVal); + + [[nodiscard]] + static HRESULT s_GetPropertyBoolValue(_In_ IPropertyStore * const pPropStore, + _In_ REFPROPERTYKEY refPropKey, + _Out_ BOOL * const pfValue); + [[nodiscard]] + static HRESULT s_GetPropertyByteValue(_In_ IPropertyStore * const pPropStore, + _In_ REFPROPERTYKEY refPropKey, + _Out_ BYTE * const pbValue); + [[nodiscard]] + static HRESULT s_GetPropertyDwordValue(_In_ IPropertyStore * const pPropStore, + _In_ REFPROPERTYKEY refPropKey, + _Out_ DWORD * const pdwValue); + + [[nodiscard]] + static HRESULT s_PopulateV1Properties(_In_ IShellLink * const pslConsole, _In_ PCONSOLE_STATE_INFO pStateInfo); + [[nodiscard]] + static HRESULT s_PopulateV2Properties(_In_ IShellLink * const pslConsole, _In_ PCONSOLE_STATE_INFO pStateInfo); + + static void s_GetLinkTitle(_In_ PCWSTR pwszShortcutFilename, _Out_writes_(cchShortcutTitle) PWSTR pwszShortcutTitle, const size_t cchShortcutTitle); + [[nodiscard]] + static HRESULT s_GetLoadedShellLinkForShortcut(_In_ PCWSTR pwszShortcutFileName, + const DWORD dwMode, + _COM_Outptr_ IShellLink **ppsl, + _COM_Outptr_ IPersistFile **ppPf); +}; diff --git a/src/propslib/TrueTypeFontList.cpp b/src/propslib/TrueTypeFontList.cpp new file mode 100644 index 000000000..53873afee --- /dev/null +++ b/src/propslib/TrueTypeFontList.cpp @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "TrueTypeFontList.hpp" + +#include "RegistrySerialization.hpp" + +#pragma hdrstop + +#define DEFAULT_NON_DBCS_FONTFACE L"Consolas" + +SINGLE_LIST_ENTRY TrueTypeFontList::s_ttFontList; + +WORD +ConvertStringToDec( + __in LPTSTR lpch) +{ + TCHAR ch; + WORD val = 0; + + while ((ch = *lpch) != TEXT('\0')) + { + if (TEXT('0') <= ch && ch <= TEXT('9')) + val = (val * 10) + (ch - TEXT('0')); + else + break; + + lpch++; + } + + return val; +} + +[[nodiscard]] +NTSTATUS TrueTypeFontList::s_Initialize() +{ + HKEY hkRegistry; + WCHAR awchValue[512]; + WCHAR awchData[512]; + DWORD dwIndex; + LPWSTR pwsz; + + // Prevent memory leak. Delete if it's already allocated before refilling it. + LOG_IF_FAILED(s_Destroy()); + + NTSTATUS Status = RegistrySerialization::s_OpenKey(HKEY_LOCAL_MACHINE, + MACHINE_REGISTRY_CONSOLE_TTFONT_WIN32_PATH, + &hkRegistry); + if (NT_SUCCESS(Status)) { + LPTTFONTLIST pTTFontList; + + for (dwIndex = 0; ; dwIndex++) { + Status = RegistrySerialization::s_EnumValue(hkRegistry, + dwIndex, + sizeof(awchValue), + (LPWSTR)awchValue, + sizeof(awchData), + (PBYTE)awchData); + + if (Status == ERROR_NO_MORE_ITEMS) { + Status = STATUS_SUCCESS; + break; + } + + if (!NT_SUCCESS(Status)) { + break; + } + + pTTFontList = (TTFONTLIST*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(TTFONTLIST)); + if (pTTFontList == nullptr) { + break; + } + + pTTFontList->List.Next = nullptr; + pTTFontList->CodePage = ConvertStringToDec(awchValue); + pwsz = awchData; + if (*pwsz == BOLD_MARK) { + pTTFontList->fDisableBold = TRUE; + pwsz++; + } + else { + pTTFontList->fDisableBold = FALSE; + } + + StringCchCopyW(pTTFontList->FaceName1, + ARRAYSIZE(pTTFontList->FaceName1), + pwsz); + + // wcslen is only valid on non-null pointers. + if (pwsz != nullptr) + { + pwsz += wcslen(pwsz) + 1; + + // Validate that pwsz must be pointing to a position in awchData array after the movement. + if (pwsz >= awchData && pwsz < (awchData + ARRAYSIZE(awchData))) + { + if (*pwsz == BOLD_MARK) { + pTTFontList->fDisableBold = TRUE; + pwsz++; + } + StringCchCopyW(pTTFontList->FaceName2, + ARRAYSIZE(pTTFontList->FaceName2), + pwsz); + } + } + + PushEntryList(&s_ttFontList, &(pTTFontList->List)); + } + + RegCloseKey(hkRegistry); + } + + return STATUS_SUCCESS; +} + +[[nodiscard]] +NTSTATUS TrueTypeFontList::s_Destroy() +{ + while (s_ttFontList.Next != nullptr) { + LPTTFONTLIST pTTFontList = (LPTTFONTLIST)PopEntryList(&s_ttFontList); + + if (pTTFontList != nullptr) { + HeapFree(GetProcessHeap(), 0, pTTFontList); + } + } + + s_ttFontList.Next = nullptr; + + return STATUS_SUCCESS; +} + +LPTTFONTLIST TrueTypeFontList::s_SearchByName(_In_opt_ LPCWSTR pwszFace, + _In_ BOOL fCodePage, + _In_ UINT CodePage) +{ + PSINGLE_LIST_ENTRY pTemp = s_ttFontList.Next; + + if (pwszFace) { + while (pTemp != nullptr) { + LPTTFONTLIST pTTFontList = (LPTTFONTLIST)pTemp; + + if (wcscmp(pwszFace, pTTFontList->FaceName1) == 0 || + wcscmp(pwszFace, pTTFontList->FaceName2) == 0) { + if (fCodePage) + if (pTTFontList->CodePage == CodePage) + return pTTFontList; + else + return nullptr; + else + return pTTFontList; + } + + pTemp = pTemp->Next; + } + } + + return nullptr; +} + +[[nodiscard]] +NTSTATUS TrueTypeFontList::s_SearchByCodePage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pwszFaceName, + const size_t cchFaceName) +{ + NTSTATUS status = STATUS_SUCCESS; + BOOL fFontFound = FALSE; + + // Look through our list entries to see if we can find a corresponding truetype font for this codepage + for (PSINGLE_LIST_ENTRY pListEntry = s_ttFontList.Next; pListEntry != nullptr && !fFontFound; pListEntry = pListEntry->Next) + { + LPTTFONTLIST pTTFontEntry = (LPTTFONTLIST)pListEntry; + if (pTTFontEntry->CodePage == uiCodePage) + { + // found a match, use this font's primary facename + status = StringCchCopyW(pwszFaceName, cchFaceName, pTTFontEntry->FaceName1); + fFontFound = TRUE; + } + } + + if (!fFontFound) + { + status = StringCchCopyW(pwszFaceName, cchFaceName, DEFAULT_NON_DBCS_FONTFACE); + } + + return status; +} diff --git a/src/propslib/TrueTypeFontList.hpp b/src/propslib/TrueTypeFontList.hpp new file mode 100644 index 000000000..533a344cd --- /dev/null +++ b/src/propslib/TrueTypeFontList.hpp @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- TrueTypeFontList.hpp + +Abstract: +- This module is used for managing the list of preferred TrueType fonts in the registry + +Author(s): +- Michael Niksa (MiNiksa) 14-Mar-2016 + +--*/ + +#pragma once + +class TrueTypeFontList +{ +public: + static SINGLE_LIST_ENTRY s_ttFontList; + + [[nodiscard]] + static NTSTATUS s_Initialize(); + [[nodiscard]] + static NTSTATUS s_Destroy(); + + static LPTTFONTLIST s_SearchByName(_In_opt_ LPCWSTR pwszFace, + _In_ BOOL fCodePage, + _In_ UINT CodePage); + + [[nodiscard]] + static NTSTATUS s_SearchByCodePage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pwszFaceName, + const size_t cchFaceName); +}; diff --git a/src/propslib/conpropsp.hpp b/src/propslib/conpropsp.hpp new file mode 100644 index 000000000..0d15fca69 --- /dev/null +++ b/src/propslib/conpropsp.hpp @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conpropsp.h + +Abstract: +- This module is used for reading/writing registry operations + AND +- This module is used for writing console properties to the link associated + with a particular console title. + +Author(s): +- Michael Niksa (MiNiksa) 23-Jul-2014 +- Paul Campbell (PaulCam) 23-Jul-2014 +- Mike Griese (MiGrie) 04-Aug-2015 + +Revision History: +- From components of srvinit.c +- From miniksa, paulcam's Registry.cpp +--*/ + +#pragma once + +#include "RegistrySerialization.hpp" +#include "ShortcutSerialization.hpp" +#include "TrueTypeFontList.hpp" diff --git a/src/propslib/precomp.cpp b/src/propslib/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/propslib/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/propslib/precomp.h b/src/propslib/precomp.h new file mode 100644 index 000000000..faa32bc91 --- /dev/null +++ b/src/propslib/precomp.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define DEFINE_CONSOLEV2_PROPERTIES +#define INC_OLE2 + +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS + +// From ntdef.h, but that can't be included or it'll fight over PROBE_ALIGNMENT and other such arch specific defs +typedef _Return_type_success_(return >= 0) LONG NTSTATUS; +/*lint -save -e624 */ // Don't complain about different typedefs. +typedef NTSTATUS *PNTSTATUS; +/*lint -restore */ // Resume checking for different typedefs. +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +#define INLINE_NTSTATUS_FROM_WIN32 1 // Must use inline NTSTATUS or it will call the wrapped function twice. +#pragma warning(push) +#pragma warning(disable:4430) // Must disable 4430 "default int" warning for C++ because ntstatus.h is inflexible SDK definition. +#include +#pragma warning(pop) + +#ifdef EXTERNAL_BUILD +#include +#else +#include +#endif + +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#include "..\host\settings.hpp" +#include + +#include "conpropsp.hpp" + +#pragma region Definitions from DDK (wdm.h) +FORCEINLINE +PSINGLE_LIST_ENTRY +PopEntryList( + _Inout_ PSINGLE_LIST_ENTRY ListHead +) +{ + + PSINGLE_LIST_ENTRY FirstEntry; + + FirstEntry = ListHead->Next; + if (FirstEntry != NULL) { + ListHead->Next = FirstEntry->Next; + } + + return FirstEntry; +} + + +FORCEINLINE +VOID +PushEntryList( + _Inout_ PSINGLE_LIST_ENTRY ListHead, + _Inout_ __drv_aliasesMem PSINGLE_LIST_ENTRY Entry +) + +{ + + Entry->Next = ListHead->Next; + ListHead->Next = Entry; + return; +} +#pragma endregion diff --git a/src/propslib/propslib.vcxproj b/src/propslib/propslib.vcxproj new file mode 100644 index 000000000..ed40731db --- /dev/null +++ b/src/propslib/propslib.vcxproj @@ -0,0 +1,29 @@ + + + + + + + + + Create + + + + + + + + + + + {345FD5A4-B32B-4F29-BD1C-B033BD2C35CC} + Win32Proj + propslib + PropertiesLibrary + ConProps + + + + + \ No newline at end of file diff --git a/src/propslib/propslib.vcxproj.filters b/src/propslib/propslib.vcxproj.filters new file mode 100644 index 000000000..e81f439a5 --- /dev/null +++ b/src/propslib/propslib.vcxproj.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/propslib/sources b/src/propslib/sources new file mode 100644 index 000000000..72a3e010e --- /dev/null +++ b/src/propslib/sources @@ -0,0 +1,48 @@ +!include ..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Properties Library +# ------------------------------------- + +# This module defines a layer to access user configurable properties +# or "preferences" that will be stored in a user's registry or embedded within +# the LNK shortcut file that they used to start the console. +# It also loads some system configuration information from the registry. + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConProps +TARGETTYPE = LIBRARY + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DNT -DWIN32 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = precomp.h + +SOURCES = \ + ShortcutSerialization.cpp \ + RegistrySerialization.cpp \ + TrueTypeFontList.cpp \ + +INCLUDES = \ + ..\inc; \ diff --git a/src/renderer/base/Cluster.cpp b/src/renderer/base/Cluster.cpp new file mode 100644 index 000000000..b639faf2b --- /dev/null +++ b/src/renderer/base/Cluster.cpp @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "../inc/Cluster.hpp" +#include "../../inc/unicode.hpp" +#include "../types/inc/convert.hpp" + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Instantiates a new cluster structure +// Arguments: +// - text - This is a view of the text that forms this cluster (one or more wchar_t*s) +// - columns - This is the number of columns in the grid that the cluster should consume when drawn +Cluster::Cluster(const std::wstring_view text, const size_t columns) : + _text(text), + _columns(columns) +{ +} + +// Routine Description: +// - Provides the embedded text as a single character +// - This might replace the string with the replacement character if it doesn't fit as one wchar_t +// Arguments: +// - +// Return Value: +// - The only wchar_t in the string or the Unicode replacement character as appropriate. +const wchar_t Cluster::GetTextAsSingle() const noexcept +{ + try + { + return Utf16ToUcs2(_text); + } + CATCH_LOG(); + return UNICODE_REPLACEMENT; +} + +// Routine Description: +// - Provides the string of wchar_ts for this cluster. +// Arguments: +// - +// Return Value: +// - String view of wchar_ts. +const std::wstring_view& Cluster::GetText() const noexcept +{ + return _text; +} + +// Routine Description: +// - Gets the number of columns in the grid that this character should consume +// visually when rendered onto a line. +// Arguments: +// - +// Return Value: +// - Number of columns to use when drawing (not a pixel count). +const size_t Cluster::GetColumns() const noexcept +{ + return _columns; +} diff --git a/src/renderer/base/FontInfoBase.cpp b/src/renderer/base/FontInfoBase.cpp new file mode 100644 index 000000000..a3337b075 --- /dev/null +++ b/src/renderer/base/FontInfoBase.cpp @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include + +#include "..\inc\FontInfoBase.hpp" + + +bool operator==(const FontInfoBase& a, const FontInfoBase& b) +{ + return (wcscmp(a._wszFaceName, b._wszFaceName) == 0 && + a._lWeight == b._lWeight && + a._bFamily == b._bFamily && + a._uiCodePage == b._uiCodePage && + a._fDefaultRasterSetFromEngine == b._fDefaultRasterSetFromEngine); +} + +FontInfoBase::FontInfoBase(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont, + const UINT uiCodePage) : + _bFamily(bFamily), + _lWeight(lWeight), + _fDefaultRasterSetFromEngine(fSetDefaultRasterFont), + _uiCodePage(uiCodePage) +{ + if (nullptr != pwszFaceName) + { + wcscpy_s(_wszFaceName, ARRAYSIZE(_wszFaceName), pwszFaceName); + } + + ValidateFont(); +} + +FontInfoBase::FontInfoBase(const FontInfoBase &fibFont) : + FontInfoBase(fibFont.GetFaceName(), + fibFont.GetFamily(), + fibFont.GetWeight(), + fibFont.WasDefaultRasterSetFromEngine(), + fibFont.GetCodePage()) +{ +} + +FontInfoBase::~FontInfoBase() +{ +} + +BYTE FontInfoBase::GetFamily() const +{ + return _bFamily; +} + +// When the default raster font is forced set from the engine, this is how we differentiate it from a simple apply. +// Default raster font is internally represented as a blank face name and zeros for weight, family, and size. This is +// the hint for the engine to use whatever comes back from GetStockObject(OEM_FIXED_FONT) (at least in the GDI world). +bool FontInfoBase::WasDefaultRasterSetFromEngine() const +{ + return _fDefaultRasterSetFromEngine; +} + +LONG FontInfoBase::GetWeight() const +{ + return _lWeight; +} + +PCWCHAR FontInfoBase::GetFaceName() const +{ + return (PCWCHAR)_wszFaceName; +} + +UINT FontInfoBase::GetCodePage() const +{ + return _uiCodePage; +} + +// NOTE: this method is intended to only be used from the engine itself to respond what font it has chosen. +void FontInfoBase::SetFromEngine(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont) +{ + wcscpy_s(_wszFaceName, ARRAYSIZE(_wszFaceName), pwszFaceName); + _bFamily = bFamily; + _lWeight = lWeight; + _fDefaultRasterSetFromEngine = fSetDefaultRasterFont; +} + +// Internally, default raster font is represented by empty facename, and zeros for weight, family, and size. Since +// FontInfoBase doesn't have sizing information, this helper checks everything else. +bool FontInfoBase::IsDefaultRasterFontNoSize() const +{ + return (_lWeight == 0 && _bFamily == 0 && wcsnlen_s(_wszFaceName, ARRAYSIZE(_wszFaceName)) == 0); +} + +void FontInfoBase::ValidateFont() +{ + // If we were given a blank name, it meant raster fonts, which to us is always Terminal. + if (!IsDefaultRasterFontNoSize() && s_pFontDefaultList != nullptr) + { + // If we have a list of default fonts and our current font is the placeholder for the defaults, substitute here. + if (0 == wcsncmp(_wszFaceName, DEFAULT_TT_FONT_FACENAME, ARRAYSIZE(DEFAULT_TT_FONT_FACENAME))) + { + WCHAR pwszDefaultFontFace[LF_FACESIZE]; + if (SUCCEEDED(s_pFontDefaultList->RetrieveDefaultFontNameForCodepage(GetCodePage(), + pwszDefaultFontFace, + ARRAYSIZE(pwszDefaultFontFace)))) + { + wcscpy_s(_wszFaceName, ARRAYSIZE(_wszFaceName), pwszDefaultFontFace); + + // If we're assigning a default true type font name, make sure the family is also set to TrueType + // to help GDI select the appropriate font when we actually create it. + _bFamily = TMPF_TRUETYPE; + } + } + } +} + +bool FontInfoBase::IsTrueTypeFont() const +{ + return (_bFamily & TMPF_TRUETYPE) != 0; +} + +void FontInfoBase::s_SetFontDefaultList(_In_ Microsoft::Console::Render::IFontDefaultList* const pFontDefaultList) +{ + s_pFontDefaultList = pFontDefaultList; +} diff --git a/src/renderer/base/FontInfoDesired.cpp b/src/renderer/base/FontInfoDesired.cpp new file mode 100644 index 000000000..641783153 --- /dev/null +++ b/src/renderer/base/FontInfoDesired.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "..\inc\FontInfoDesired.hpp" + +bool operator==(const FontInfoDesired& a, const FontInfoDesired& b) +{ + return (static_cast(a) == static_cast(b) && + a._coordSizeDesired == b._coordSizeDesired); +} + +COORD FontInfoDesired::GetEngineSize() const +{ + COORD coordSize = _coordSizeDesired; + if (IsTrueTypeFont()) + { + coordSize.X = 0; // Don't tell the engine about the width for a TrueType font. It makes a mess. + } + + return coordSize; +} + +FontInfoDesired::FontInfoDesired(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const COORD coordSizeDesired, + const UINT uiCodePage) : + FontInfoBase(pwszFaceName, bFamily, lWeight, false, uiCodePage), + _coordSizeDesired(coordSizeDesired) +{ +} + +FontInfoDesired::FontInfoDesired(const FontInfo& fiFont) : + FontInfoBase(fiFont), + _coordSizeDesired(fiFont.GetUnscaledSize()) +{ +} + +// This helper determines if this object represents the default raster font. This can either be because internally we're +// using the empty facename and zeros for size, weight, and family, or it can be because we were given explicit +// dimensions from the engine that were the result of loading the default raster font. See GdiEngine::_GetProposedFont(). +bool FontInfoDesired::IsDefaultRasterFont() const +{ + // Either the raster was set from the engine... + // OR the face name is empty with a size of 0x0 or 8x12. + return WasDefaultRasterSetFromEngine() || (wcsnlen_s(GetFaceName(), LF_FACESIZE) == 0 && + ((_coordSizeDesired.X == 0 && _coordSizeDesired.Y == 0) || + (_coordSizeDesired.X == 8 && _coordSizeDesired.Y == 12))); +} diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp new file mode 100644 index 000000000..68c86c562 --- /dev/null +++ b/src/renderer/base/RenderEngineBase.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" +#include "../inc/RenderEngineBase.hpp" +#pragma hdrstop +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; + +RenderEngineBase::RenderEngineBase() : + _titleChanged(false), + _lastFrameTitle(L"") +{ + +} + +HRESULT RenderEngineBase::InvalidateTitle(const std::wstring& proposedTitle) noexcept +{ + if (proposedTitle != _lastFrameTitle) + { + _titleChanged = true; + } + + return S_OK; +} + +HRESULT RenderEngineBase::UpdateTitle(const std::wstring& newTitle) noexcept +{ + HRESULT hr = S_FALSE; + if (newTitle != _lastFrameTitle) + { + RETURN_IF_FAILED(_DoUpdateTitle(newTitle)); + _lastFrameTitle = newTitle; + _titleChanged = false; + hr = S_OK; + } + return hr; +} diff --git a/src/renderer/base/dirs b/src/renderer/base/dirs new file mode 100644 index 000000000..134104358 --- /dev/null +++ b/src/renderer/base/dirs @@ -0,0 +1,3 @@ +DIRS= \ + lib + diff --git a/src/renderer/base/fontinfo.cpp b/src/renderer/base/fontinfo.cpp new file mode 100644 index 000000000..ddfe2e98a --- /dev/null +++ b/src/renderer/base/fontinfo.cpp @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "..\inc\FontInfo.hpp" + +bool operator==(const FontInfo& a, const FontInfo& b) +{ + return (static_cast(a) == static_cast(b) && + a._coordSize == b._coordSize && + a._coordSizeUnscaled == b._coordSizeUnscaled); +} + +FontInfo::FontInfo(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const COORD coordSize, + const UINT uiCodePage, + const bool fSetDefaultRasterFont /*= false*/) : + FontInfoBase(pwszFaceName, bFamily, lWeight, fSetDefaultRasterFont, uiCodePage), + _coordSize(coordSize), + _coordSizeUnscaled(coordSize) +{ + ValidateFont(); +} + +FontInfo::FontInfo(const FontInfo& fiFont) : + FontInfoBase(fiFont), + _coordSize(fiFont.GetSize()), + _coordSizeUnscaled(fiFont.GetUnscaledSize()) +{ + +} + +COORD FontInfo::GetUnscaledSize() const +{ + return _coordSizeUnscaled; +} + +COORD FontInfo::GetSize() const +{ + return _coordSize; +} + + +void FontInfo::SetFromEngine(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont, + const COORD coordSize, + const COORD coordSizeUnscaled) +{ + FontInfoBase::SetFromEngine(pwszFaceName, + bFamily, + lWeight, + fSetDefaultRasterFont); + + _coordSize = coordSize; + _coordSizeUnscaled = coordSizeUnscaled; + + _ValidateCoordSize(); +} + +void FontInfo::ValidateFont() +{ + _ValidateCoordSize(); +} + +void FontInfo::_ValidateCoordSize() +{ + // a (0,0) font is okay for the default raster font, as we will eventually set the dimensions based on the font GDI + // passes back to us. + if (!IsDefaultRasterFontNoSize()) + { + // Initialize X to 1 so we don't divide by 0 + if (_coordSize.X == 0) + { + _coordSize.X = 1; + } + + // If we have no font size, we want to use 8x12 by default + if (_coordSize.Y == 0) + { + _coordSize.X = 8; + _coordSize.Y = 12; + + _coordSizeUnscaled = _coordSize; + } + } +} + +#pragma warning(push) +#pragma warning(suppress:4356) +Microsoft::Console::Render::IFontDefaultList* FontInfo::s_pFontDefaultList; +#pragma warning(pop) + +void FontInfo::s_SetFontDefaultList(_In_ Microsoft::Console::Render::IFontDefaultList* const pFontDefaultList) +{ + FontInfoBase::s_SetFontDefaultList(pFontDefaultList); +} diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj new file mode 100644 index 000000000..7ba34072f --- /dev/null +++ b/src/renderer/base/lib/base.vcxproj @@ -0,0 +1,40 @@ + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} + Win32Proj + base + RendererBase + ConRenderBase + + + + + \ No newline at end of file diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters new file mode 100644 index 000000000..00f0bb1e6 --- /dev/null +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -0,0 +1,87 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {45f2815e-fdb4-4f38-94a3-794de34b7911} + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files\inc + + + Header Files\inc + + + Header Files\inc + + + Header Files\inc + + + Header Files\inc + + + Header Files\inc + + + Header Files\inc + + + Header Files + + + Header Files\inc + + + + + + \ No newline at end of file diff --git a/src/renderer/base/lib/sources b/src/renderer/base/lib/sources new file mode 100644 index 000000000..e752adb9b --- /dev/null +++ b/src/renderer/base/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderBase +TARGETTYPE = LIBRARY diff --git a/src/renderer/base/precomp.cpp b/src/renderer/base/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/renderer/base/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/renderer/base/precomp.h b/src/renderer/base/precomp.h new file mode 100644 index 000000000..5069f6214 --- /dev/null +++ b/src/renderer/base/precomp.h @@ -0,0 +1,33 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#pragma once + +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#include + +#include "..\..\types\inc\viewport.hpp" +#include "..\..\inc\operators.hpp" + +#ifndef _NTSTATUS_DEFINED +#define _NTSTATUS_DEFINED +typedef _Return_type_success_(return >= 0) long NTSTATUS; +#endif + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp new file mode 100644 index 000000000..1fcf695b7 --- /dev/null +++ b/src/renderer/base/renderer.cpp @@ -0,0 +1,932 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "renderer.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Creates a new renderer controller for a console. +// Arguments: +// - pData - The interface to console data structures required for rendering +// - pEngine - The output engine for targeting each rendering frame +// Return Value: +// - An instance of a Renderer. +// NOTE: CAN THROW IF MEMORY ALLOCATION FAILS. +Renderer::Renderer(IRenderData* pData, + _In_reads_(cEngines) IRenderEngine** const rgpEngines, + const size_t cEngines, + std::unique_ptr thread) : + _pData(pData), + _pThread{ std::move(thread) }, + _destructing{ false } +{ + + _srViewportPrevious = { 0 }; + + for (size_t i = 0; i < cEngines; i++) + { + IRenderEngine* engine = rgpEngines[i]; + // NOTE: THIS CAN THROW IF MEMORY ALLOCATION FAILS. + AddRenderEngine(engine); + } +} + +// Routine Description: +// - Destroys an instance of a renderer +// Arguments: +// - +// Return Value: +// - +Renderer::~Renderer() +{ + _destructing = true; +} + +// Routine Description: +// - Walks through the console data structures to compose a new frame based on the data that has changed since last call and outputs it to the connected rendering engine. +// Arguments: +// - +// Return Value: +// - HRESULT S_OK, GDI error, Safe Math error, or state/argument errors. +[[nodiscard]] +HRESULT Renderer::PaintFrame() +{ + if (_destructing) + { + return S_FALSE; + } + + for (IRenderEngine* const pEngine : _rgpEngines) + { + LOG_IF_FAILED(_PaintFrameForEngine(pEngine)); + } + + return S_OK; +} + + +[[nodiscard]] +HRESULT Renderer::_PaintFrameForEngine(_In_ IRenderEngine* const pEngine) +{ + FAIL_FAST_IF_NULL(pEngine); // This is a programming error. Fail fast. + + _pData->LockConsole(); + auto unlock = wil::scope_exit([&]() + { + _pData->UnlockConsole(); + }); + + // Last chance check if anything scrolled without an explicit invalidate notification since the last frame. + _CheckViewportAndScroll(); + + // Try to start painting a frame + HRESULT const hr = pEngine->StartPaint(); + RETURN_IF_FAILED(hr); + + // Return early if there's nothing to paint. + // The renderer itself tracks if there's something to do with the title, the + // engine won't know that. + if (S_FALSE == hr) + { + return S_OK; + } + + auto endPaint = wil::scope_exit([&]() + { + LOG_IF_FAILED(pEngine->EndPaint()); + }); + + // A. Prep Colors + RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), true)); + + // B. Perform Scroll Operations + RETURN_IF_FAILED(_PerformScrolling(pEngine)); + + // 1. Paint Background + RETURN_IF_FAILED(_PaintBackground(pEngine)); + + // 2. Paint Rows of Text + _PaintBufferOutput(pEngine); + + // 3. Paint overlays that reside above the text buffer + _PaintOverlays(pEngine); + + // 4. Paint Selection + _PaintSelection(pEngine); + + // 5. Paint Cursor + _PaintCursor(pEngine); + + // 6. Paint window title + RETURN_IF_FAILED(_PaintTitle(pEngine)); + + // Force scope exit end paint to finish up collecting information and possibly painting + endPaint.reset(); + + // Force scope exit unlock to let go of global lock so other threads can run + unlock.reset(); + + // Trigger out-of-lock presentation for renderers that can support it + RETURN_IF_FAILED(pEngine->Present()); + + // As we leave the scope, EndPaint will be called (declared above) + return S_OK; +} + +void Renderer::_NotifyPaintFrame() +{ + // The thread will provide throttling for us. + _pThread->NotifyPaint(); +} + +// Routine Description: +// - Called when the system has requested we redraw a portion of the console. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerSystemRedraw(const RECT* const prcDirtyClient) +{ + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->InvalidateSystem(prcDirtyClient)); + }); + + _NotifyPaintFrame(); +} + +// Routine Description: +// - Called when a particular region within the console buffer has changed. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerRedraw(const Viewport& region) +{ + Viewport view = _pData->GetViewport(); + SMALL_RECT srUpdateRegion = region.ToExclusive(); + + if (view.TrimToViewport(&srUpdateRegion)) + { + view.ConvertToOrigin(&srUpdateRegion); + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->Invalidate(&srUpdateRegion)); + }); + + _NotifyPaintFrame(); + } +} + +// Routine Description: +// - Called when a particular coordinate within the console buffer has changed. +// Arguments: +// - pcoord: The buffer-space coordinate that has changed. +// Return Value: +// - +void Renderer::TriggerRedraw(const COORD* const pcoord) +{ + TriggerRedraw(Viewport::FromCoord(*pcoord)); // this will notify to paint if we need it. +} + +// Routine Description: +// - Called when the cursor has moved in the buffer. Allows for RenderEngines to +// differentiate between cursor movements and other invalidates. +// Visual Renderers (ex GDI) sohuld invalidate the position, while the VT +// engine ignores this. See MSFT:14711161. +// Arguments: +// - pcoord: The buffer-space position of the cursor. +// Return Value: +// - +void Renderer::TriggerRedrawCursor(const COORD* const pcoord) +{ + Viewport view = _pData->GetViewport(); + COORD updateCoord = *pcoord; + + if (view.IsInBounds(updateCoord)) + { + view.ConvertToOrigin(&updateCoord); + for (IRenderEngine* pEngine : _rgpEngines) + { + LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord)); + + // Double-wide cursors need to invalidate the right half as well. + if (_pData->IsCursorDoubleWidth()) + { + updateCoord.X++; + LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord)); + } + } + + _NotifyPaintFrame(); + } +} + +// Routine Description: +// - Called when something that changes the output state has occurred and the entire frame is now potentially invalid. +// - NOTE: Use sparingly. Try to reduce the refresh region where possible. Only use when a global state change has occurred. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerRedrawAll() +{ + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->InvalidateAll()); + }); + + _NotifyPaintFrame(); +} + +// Method Description: +// - Called when the host is about to die, to give the renderer one last chance +// to paint before the host exits. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerTeardown() +{ + // We need to shut down the paint thread on teardown. + _pThread->WaitForPaintCompletionAndDisable(INFINITE); + + // Then walk through and do one final paint on the caller's thread. + for (IRenderEngine* const pEngine : _rgpEngines) + { + bool fEngineRequestsRepaint = false; + HRESULT hr = pEngine->PrepareForTeardown(&fEngineRequestsRepaint); + LOG_IF_FAILED(hr); + + if (SUCCEEDED(hr) && fEngineRequestsRepaint) + { + LOG_IF_FAILED(_PaintFrameForEngine(pEngine)); + } + } +} + +// Routine Description: +// - Called when the selected area in the console has changed. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerSelection() +{ + try + { + // Get selection rectangles + const auto rects = _GetSelectionRects(); + + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection)); + LOG_IF_FAILED(pEngine->InvalidateSelection(rects)); + }); + + _previousSelection = rects; + + _NotifyPaintFrame(); + } + CATCH_LOG(); +} + +// Routine Description: +// - Called when we want to check if the viewport has moved and scroll accordingly if so. +// Arguments: +// - +// Return Value: +// - True if something changed and we scrolled. False otherwise. +bool Renderer::_CheckViewportAndScroll() +{ + SMALL_RECT const srOldViewport = _srViewportPrevious; + SMALL_RECT const srNewViewport = _pData->GetViewport().ToInclusive(); + + COORD coordDelta; + coordDelta.X = srOldViewport.Left - srNewViewport.Left; + coordDelta.Y = srOldViewport.Top - srNewViewport.Top; + + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->UpdateViewport(srNewViewport)); + LOG_IF_FAILED(pEngine->InvalidateScroll(&coordDelta)); + }); + _srViewportPrevious = srNewViewport; + + return coordDelta.X != 0 || coordDelta.Y != 0; +} + +// Routine Description: +// - Called when a scroll operation has occurred by manipulating the viewport. +// - This is a special case as calling out scrolls explicitly drastically improves performance. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerScroll() +{ + if (_CheckViewportAndScroll()) + { + _NotifyPaintFrame(); + } +} + +// Routine Description: +// - Called when a scroll operation wishes to explicitly adjust the frame by the given coordinate distance. +// - This is a special case as calling out scrolls explicitly drastically improves performance. +// - This should only be used when the viewport is not modified. It lets us know we can "scroll anyway" to save perf, +// because the backing circular buffer rotated out from behind the viewport. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerScroll(const COORD* const pcoordDelta) +{ + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta)); + }); + + _NotifyPaintFrame(); +} + +// Routine Description: +// - Called when the text buffer is about to circle it's backing buffer. +// A renderer might want to get painted before that happens. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerCircling() +{ + for (IRenderEngine* const pEngine : _rgpEngines) + { + bool fEngineRequestsRepaint = false; + HRESULT hr = pEngine->InvalidateCircling(&fEngineRequestsRepaint); + LOG_IF_FAILED(hr); + + if (SUCCEEDED(hr) && fEngineRequestsRepaint) + { + LOG_IF_FAILED(_PaintFrameForEngine(pEngine)); + } + } +} + +// Routine Description: +// - Called when the title of the console window has changed. Indicates that we +// should update the title on the next frame. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerTitleChange() +{ + const std::wstring newTitle = _pData->GetConsoleTitle(); + for (IRenderEngine* const pEngine : _rgpEngines) + { + LOG_IF_FAILED(pEngine->InvalidateTitle(newTitle)); + } + _NotifyPaintFrame(); +} + +// Routine Description: +// - Update the title for a particular engine. +// Arguments: +// - pEngine: the engine to update the title for. +// Return Value: +// - the HRESULT of the underlying engine's UpdateTitle call. +HRESULT Renderer::_PaintTitle(IRenderEngine* const pEngine) +{ + const std::wstring newTitle = _pData->GetConsoleTitle(); + return pEngine->UpdateTitle(newTitle); +} + +// Routine Description: +// - Called when a change in font or DPI has been detected. +// Arguments: +// - iDpi - New DPI value +// - FontInfoDesired - A description of the font we would like to have. +// - FontInfo - Data that will be fixed up/filled on return with the chosen font data. +// Return Value: +// - +void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) +{ + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + LOG_IF_FAILED(pEngine->UpdateDpi(iDpi)); + LOG_IF_FAILED(pEngine->UpdateFont(FontInfoDesired, FontInfo)); + }); + + _NotifyPaintFrame(); +} + +// Routine Description: +// - Get the information on what font we would be using if we decided to create a font with the given parameters +// - This is for use with speculative calculations. +// Arguments: +// - iDpi - The DPI of the target display +// - pFontInfoDesired - A description of the font we would like to have. +// - pFontInfo - Data that will be fixed up/filled on return with the chosen font data. +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT Renderer::GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) +{ + // If there's no head, return E_FAIL. The caller should decide how to + // handle this. + // Currently, the only caller is the WindowProc:WM_GETDPISCALEDSIZE handler. + // It will assume that the proposed font is 1x1, regardless of DPI. + if (_rgpEngines.size() < 1) + { + return E_FAIL; + } + + // There will only every really be two engines - the real head and the VT + // renderer. We won't know which is which, so iterate over them. + // Only return the result of the successful one if it's not S_FALSE (which is the VT renderer) + // TODO: 14560740 - The Window might be able to get at this info in a more sane manner + FAIL_FAST_IF(!(_rgpEngines.size() <= 2)); + for (IRenderEngine* const pEngine : _rgpEngines) + { + const HRESULT hr = LOG_IF_FAILED(pEngine->GetProposedFont(FontInfoDesired, FontInfo, iDpi)); + // We're looking for specifically S_OK, S_FALSE is not good enough. + if (hr == S_OK) + { + return hr; + } + }; + + return E_FAIL; +} + +// Routine Description: +// - Tests against the current rendering engine to see if this particular character would be considered +// full-width (inscribed in a square, twice as wide as a standard Western character, typically used for CJK +// languages) or half-width. +// - Typically used to determine how many positions in the backing buffer a particular character should fill. +// NOTE: This only handles 1 or 2 wide (in monospace terms) characters. +// Arguments: +// - glyph - the utf16 encoded codepoint to test +// Return Value: +// - True if the codepoint is full-width (two wide), false if it is half-width (one wide). +bool Renderer::IsGlyphWideByFont(const std::wstring_view glyph) +{ + bool fIsFullWidth = false; + + // There will only every really be two engines - the real head and the VT + // renderer. We won't know which is which, so iterate over them. + // Only return the result of the successful one if it's not S_FALSE (which is the VT renderer) + // TODO: 14560740 - The Window might be able to get at this info in a more sane manner + FAIL_FAST_IF(!(_rgpEngines.size() <= 2)); + for (IRenderEngine* const pEngine : _rgpEngines) + { + const HRESULT hr = LOG_IF_FAILED(pEngine->IsGlyphWideByFont(glyph, &fIsFullWidth)); + // We're looking for specifically S_OK, S_FALSE is not good enough. + if (hr == S_OK) + { + return fIsFullWidth; + } + } + + return fIsFullWidth; +} + +// Routine Description: +// - Sets an event in the render thread that allows it to proceed, thus enabling painting. +// Arguments: +// - +// Return Value: +// - +void Renderer::EnablePainting() +{ + _pThread->EnablePainting(); +} + +// Routine Description: +// - Waits for the current paint operation to complete, if any, up to the specified timeout. +// - Resets an event in the render thread that precludes it from advancing, thus disabling rendering. +// - If no paint operation is currently underway, returns immediately. +// Arguments: +// - dwTimeoutMs - Milliseconds to wait for the current paint operation to complete, if any (can be INFINITE). +// Return Value: +// - +void Renderer::WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) +{ + _pThread->WaitForPaintCompletionAndDisable(dwTimeoutMs); +} + +// Routine Description: +// - Paint helper to fill in the background color of the invalid area within the frame. +// Arguments: +// - +// Return Value: +// - +[[nodiscard]] +HRESULT Renderer::_PaintBackground(_In_ IRenderEngine* const pEngine) +{ + return pEngine->PaintBackground(); +} + +// Routine Description: +// - Paint helper to copy the primary console buffer text onto the screen. +// - This portion primarily handles figuring the current viewport, comparing it/trimming it versus the invalid portion of the frame, and queuing up, row by row, which pieces of text need to be further processed. +// - See also: Helper functions that seperate out each complexity of text rendering. +// Arguments: +// - +// Return Value: +// - +void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) +{ + // This is the subsection of the entire screen buffer that is currently being presented. + // It can move left/right or top/bottom depending on how the viewport is scrolled + // relative to the entire buffer. + const auto view = _pData->GetViewport(); + + // This is effectively the number of cells on the visible screen that need to be redrawn. + // The origin is always 0, 0 because it represents the screen itself, not the underlying buffer. + auto dirty = Viewport::FromInclusive(pEngine->GetDirtyRectInChars()); + + // Shift the origin of the dirty region to match the underlying buffer so we can + // compare the two regions directly for intersection. + dirty = Viewport::Offset(dirty, view.Origin()); + + // The intersection between what is dirty on the screen (in need of repaint) + // and what is supposed to be visible on the screen (the viewport) is what + // we need to walk through line-by-line and repaint onto the screen. + const auto redraw = Viewport::Intersect(dirty, view); + + // Shortcut: don't bother redrawing if the width is 0. + if (redraw.Width() > 0) + { + // Retrieve the text buffer so we can read information out of it. + const auto& buffer = _pData->GetTextBuffer(); + + // Now walk through each row of text that we need to redraw. + for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++) + { + // Calculate the boundaries of a single line. This is from the left to right edge of the dirty + // area in width and exactly 1 tall. + const auto bufferLine = Viewport::FromDimensions({ redraw.Left(), row }, { redraw.Width(), 1 }); + + // Find where on the screen we should place this line information. This requires us to re-map + // the buffer-based origin of the line back onto the screen-based origin of the line + // For example, the screen might say we need to paint 1,1 because it is dirty but the viewport is actually looking + // at 13,26 relative to the buffer. + // This means that we need 14,27 out of the backing buffer to fill in the 1,1 cell of the screen. + const auto screenLine = Viewport::Offset(bufferLine, -view.Origin()); + + // Retrieve the cell information iterator limited to just this line we want to redraw. + auto it = buffer.GetCellDataAt(bufferLine.Origin(), bufferLine); + + // Ask the helper to paint through this specific line. + _PaintBufferOutputHelper(pEngine, it, screenLine.Origin()); + } + } +} + +void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, + TextBufferCellIterator it, + const COORD target) +{ + // If we have valid data, let's figure out how to draw it. + if (it) + { + // TODO: MSFT: 20961091 - This is a perf issue. Instead of rebuilding this and allocing memory to hold the reinterpretation, + // we should have an iterator/view adapter for the rendering. + // That would probably also eliminate the RenderData needing to give us the entire TextBuffer as well... + // Retrieve the iterator for one line of information. + std::vector clusters; + size_t cols = 0; + + // Retrieve the first color. + auto color = it->TextAttr(); + + // And hold the point where we should start drawing. + auto screenPoint = target; + + // This outer loop will continue until we reach the end of the text we are trying to draw. + while (it) + { + // Hold onto the current run color right here for the length of the outer loop. + // We'll be changing the persistent one as we run through the inner loops to detect + // when a run changes, but we will still need to know this color at the bottom + // when we go to draw gridlines for the length of the run. + const auto currentRunColor = color; + + // Update the drawing brushes with our color. + THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false)); + + // Advance the point by however many columns we've just outputted and reset the accumulator. + screenPoint.X += gsl::narrow(cols); + cols = 0; + + // Ensure that our cluster vector is clear. + clusters.clear(); + + // This inner loop will accumulate clusters until the color changes. + // When the color changes, it will save the new color off and break. + do + { + if (color != it->TextAttr()) + { + color = it->TextAttr(); + break; + } + + // Walk through the text data and turn it into rendering clusters. + clusters.emplace_back(it->Chars(), it->Columns()); + + // Advance the cluster and column counts. + const auto columnCount = clusters.back().GetColumns(); + it += columnCount > 0 ? columnCount : 1; // prevent infinite loop for no visible columns + cols += columnCount; + + } while (it); + + // Do the painting. + // TODO: Calculate when trim left should be TRUE + THROW_IF_FAILED(pEngine->PaintBufferLine({ clusters.data(), clusters.size() }, screenPoint, false)); + + // If we're allowed to do grid drawing, draw that now too (since it will be coupled with the color data) + if (_pData->IsGridLineDrawingAllowed()) + { + // We're only allowed to draw the grid lines under certain circumstances. + _PaintBufferOutputGridLineHelper(pEngine, currentRunColor, cols, screenPoint); + } + } + } +} + +// Method Description: +// - Generates a IRenderEngine::GridLines structure from the values in the +// provided textAttribute +// Arguments: +// - textAttribute: the TextAttribute to generate GridLines from. +// Return Value: +// - a GridLines containing all the gridline info from the TextAtribute +IRenderEngine::GridLines Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept +{ + // Convert console grid line representations into rendering engine enum representations. + IRenderEngine::GridLines lines = IRenderEngine::GridLines::None; + + if (textAttribute.IsTopHorizontalDisplayed()) + { + lines |= IRenderEngine::GridLines::Top; + } + + if (textAttribute.IsBottomHorizontalDisplayed()) + { + lines |= IRenderEngine::GridLines::Bottom; + } + + if (textAttribute.IsLeftVerticalDisplayed()) + { + lines |= IRenderEngine::GridLines::Left; + } + + if (textAttribute.IsRightVerticalDisplayed()) + { + lines |= IRenderEngine::GridLines::Right; + } + return lines; +} + +// Routine Description: +// - Paint helper for primary buffer output function. +// - This particular helper sets up the various box drawing lines that can be inscribed around any character in the buffer (left, right, top, underline). +// - See also: All related helpers and buffer output functions. +// Arguments: +// - textAttribute - The line/box drawing attributes to use for this particular run. +// - cchLine - The length of both pwsLine and pbKAttrsLine. +// - coordTarget - The X/Y coordinate position in the buffer which we're attempting to start rendering from. +// Return Value: +// - +void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, + const TextAttribute textAttribute, + const size_t cchLine, + const COORD coordTarget) +{ + const COLORREF rgb = _pData->GetForegroundColor(textAttribute); + + // Convert console grid line representations into rendering engine enum representations. + IRenderEngine::GridLines lines = Renderer::s_GetGridlines(textAttribute); + + // Draw the lines + LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget)); +} + +// Routine Description: +// - Paint helper to draw the cursor within the buffer. +// Arguments: +// - +// Return Value: +// - +void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine) +{ + if (_pData->IsCursorVisible()) + { + // Get cursor position in buffer + COORD coordCursor = _pData->GetCursorPosition(); + // Adjust cursor to viewport + Viewport view = _pData->GetViewport(); + view.ConvertToOrigin(&coordCursor); + + COLORREF cursorColor = _pData->GetCursorColor(); + bool useColor = cursorColor != INVALID_COLOR; + + // Build up the cursor parameters including position, color, and drawing options + IRenderEngine::CursorOptions options; + options.coordCursor = coordCursor; + options.ulCursorHeightPercent = _pData->GetCursorHeight(); + options.cursorPixelWidth = _pData->GetCursorPixelWidth(); + options.fIsDoubleWidth = _pData->IsCursorDoubleWidth(); + options.cursorType = _pData->GetCursorStyle(); + options.fUseColor = useColor; + options.cursorColor = cursorColor; + options.isOn = _pData->IsCursorOn(); + + // Draw it within the viewport + LOG_IF_FAILED(pEngine->PaintCursor(options)); + + } +} + +// Routine Description: +// - Paint helper to draw text that overlays the main buffer to provide user interactivity regions +// - This supports IME composition. +// Arguments: +// - engine - The render engine that we're targeting. +// - overlay - The overlay to draw. +// Return Value: +// - +void Renderer::_PaintOverlay(IRenderEngine& engine, + const RenderOverlay& overlay) +{ + try + { + // First get the screen buffer's viewport. + Viewport view = _pData->GetViewport(); + + // Now get the overlay's viewport and adjust it to where it is supposed to be relative to the window. + + SMALL_RECT srCaView = overlay.region.ToInclusive(); + srCaView.Top += overlay.origin.Y; + srCaView.Bottom += overlay.origin.Y; + srCaView.Left += overlay.origin.X; + srCaView.Right += overlay.origin.X; + + // Set it up in a Viewport helper structure and trim it the IME viewport to be within the full console viewport. + Viewport viewConv = Viewport::FromInclusive(srCaView); + + SMALL_RECT srDirty = engine.GetDirtyRectInChars(); + + // Dirty is an inclusive rectangle, but oddly enough the IME was an exclusive one, so correct it. + srDirty.Bottom++; + srDirty.Right++; + + if (viewConv.TrimToViewport(&srDirty)) + { + Viewport viewDirty = Viewport::FromInclusive(srDirty); + + for (SHORT iRow = viewDirty.Top(); iRow < viewDirty.BottomInclusive(); iRow++) + { + const COORD target{ viewDirty.Left(), iRow }; + const auto source = target - overlay.origin; + + auto it = overlay.buffer.GetCellLineDataAt(source); + + _PaintBufferOutputHelper(&engine, it, target); + } + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Paint helper to draw the composition string portion of the IME. +// - This specifically is the string that appears at the cursor on the input line showing what the user is currently typing. +// - See also: Generic Paint IME helper method. +// Arguments: +// - +// Return Value: +// - +void Renderer::_PaintOverlays(_In_ IRenderEngine* const pEngine) +{ + try + { + const auto overlays = _pData->GetOverlays(); + + for (const auto& overlay : overlays) + { + _PaintOverlay(*pEngine, overlay); + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Paint helper to draw the selected area of the window. +// Arguments: +// - +// Return Value: +// - +void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) +{ + try + { + SMALL_RECT srDirty = pEngine->GetDirtyRectInChars(); + Viewport dirtyView = Viewport::FromInclusive(srDirty); + + // Get selection rectangles + const auto rectangles = _GetSelectionRects(); + for (auto rect : rectangles) + { + if (dirtyView.TrimToViewport(&rect)) + { + LOG_IF_FAILED(pEngine->PaintSelection(rect)); + } + } + } + CATCH_LOG(); +} + +// Routine Description: +// - Helper to convert the text attributes to actual RGB colors and update the rendering pen/brush within the rendering engine before the next draw operation. +// Arguments: +// - pEngine - Which engine is being updated +// - textAttributes - The 16 color foreground/background combination to set +// - isSettingDefaultBrushes - Alerts that the default brushes are being set which will +// impact whether or not to include the hung window/erase window brushes in this operation +// and can affect other draw state that wants to know the default color scheme. +// (Usually only happens when the default is changed, not when each individual color is swapped in a multi-color run.) +// Return Value: +// - +[[nodiscard]] +HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute textAttributes, const bool isSettingDefaultBrushes) +{ + const COLORREF rgbForeground = _pData->GetForegroundColor(textAttributes); + const COLORREF rgbBackground = _pData->GetBackgroundColor(textAttributes); + const WORD legacyAttributes = textAttributes.GetLegacyAttributes(); + const bool isBold = textAttributes.IsBold(); + + // The last color need's to be each engine's responsibility. If it's local to this function, + // then on the next engine we might not update the color. + RETURN_IF_FAILED(pEngine->UpdateDrawingBrushes(rgbForeground, rgbBackground, legacyAttributes, isBold, isSettingDefaultBrushes)); + + return S_OK; +} + +// Routine Description: +// - Helper called before a majority of paint operations to scroll most of the previous frame into the appropriate +// position before we paint the remaining invalid area. +// - Used to save drawing time/improve performance +// Arguments: +// - +// Return Value: +// - +[[nodiscard]] +HRESULT Renderer::_PerformScrolling(_In_ IRenderEngine* const pEngine) +{ + return pEngine->ScrollFrame(); +} + +// Routine Description: +// - Helper to determine the selected region of the buffer. +// Return Value: +// - A vector of rectangles representing the regions to select, line by line. +std::vector Renderer::_GetSelectionRects() const +{ + auto rects = _pData->GetSelectionRects(); + // Adjust rectangles to viewport + Viewport view = _pData->GetViewport(); + + std::vector result; + + for (auto& rect : rects) + { + auto sr = view.ConvertToOrigin(rect).ToInclusive(); + + // hopefully temporary, we should be receiving the right selection sizes without correction. + sr.Right++; + sr.Bottom++; + + result.emplace_back(sr); + } + + return result; +} + +// Method Description: +// - Adds another Render engine to this renderer. Future rendering calls will +// also be sent to the new renderer. +// Arguments: +// - pEngine: The new render engine to be added +// Return Value: +// - +// Throws if we ran out of memory or there was some other error appending the +// engine to our collection. +void Renderer::AddRenderEngine(_In_ IRenderEngine* const pEngine) +{ + THROW_IF_NULL_ALLOC(pEngine); + _rgpEngines.push_back(pEngine); +} diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp new file mode 100644 index 000000000..e5c586809 --- /dev/null +++ b/src/renderer/base/renderer.hpp @@ -0,0 +1,138 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Renderer.hpp + +Abstract: +- This is the definition of our renderer. +- It provides interfaces for the console application to notify when various portions of the console state have changed and need to be redrawn. +- It requires a data interface to fetch relevant console structures required for drawing and a drawing engine target for output. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "../inc/IRenderer.hpp" +#include "../inc/IRenderEngine.hpp" +#include "../inc/IRenderData.hpp" + +#include "thread.hpp" + +#include "../../buffer/out/textBuffer.hpp" +#include "../../buffer/out/CharRow.hpp" + +namespace Microsoft::Console::Render +{ + class Renderer sealed : public IRenderer + { + public: + Renderer(IRenderData* pData, + _In_reads_(cEngines) IRenderEngine** const pEngine, + const size_t cEngines, + std::unique_ptr thread); + + [[nodiscard]] + static HRESULT s_CreateInstance(IRenderData* pData, + _In_reads_(cEngines) IRenderEngine** const rgpEngines, + const size_t cEngines, + _Outptr_result_nullonfailure_ Renderer** const ppRenderer); + + [[nodiscard]] + static HRESULT s_CreateInstance(IRenderData* pData, + _Outptr_result_nullonfailure_ Renderer** const ppRenderer); + + virtual ~Renderer() override; + + [[nodiscard]] + HRESULT PaintFrame(); + + void TriggerSystemRedraw(const RECT* const prcDirtyClient) override; + void TriggerRedraw(const Microsoft::Console::Types::Viewport& region) override; + void TriggerRedraw(const COORD* const pcoord) override; + void TriggerRedrawCursor(const COORD* const pcoord) override; + void TriggerRedrawAll() override; + void TriggerTeardown() override; + + void TriggerSelection() override; + void TriggerScroll() override; + void TriggerScroll(const COORD* const pcoordDelta) override; + + void TriggerCircling() override; + void TriggerTitleChange() override; + + void TriggerFontChange(const int iDpi, + const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) override; + + [[nodiscard]] + HRESULT GetProposedFont(const int iDpi, + const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) override; + + bool IsGlyphWideByFont(const std::wstring_view glyph) override; + + void EnablePainting() override; + void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) override; + + void AddRenderEngine(_In_ IRenderEngine* const pEngine) override; + + private: + std::deque _rgpEngines; + + IRenderData* _pData; // Non-ownership pointer + + std::unique_ptr _pThread; + bool _destructing = false; + + void _NotifyPaintFrame(); + + [[nodiscard]] + HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine); + + bool _CheckViewportAndScroll(); + + [[nodiscard]] + HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine); + + void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); + + void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, + TextBufferCellIterator it, + const COORD target); + + static IRenderEngine::GridLines s_GetGridlines(const TextAttribute& textAttribute) noexcept; + + void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, + const TextAttribute textAttribute, + const size_t cchLine, + const COORD coordTarget); + + void _PaintSelection(_In_ IRenderEngine* const pEngine); + void _PaintCursor(_In_ IRenderEngine* const pEngine); + + void _PaintOverlays(_In_ IRenderEngine* const pEngine); + void _PaintOverlay(IRenderEngine& engine, const RenderOverlay& overlay); + + [[nodiscard]] + HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool isSettingDefaultBrushes); + + [[nodiscard]] + HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); + + SMALL_RECT _srViewportPrevious; + + std::vector _GetSelectionRects() const; + std::vector _previousSelection; + + [[nodiscard]] + HRESULT _PaintTitle(IRenderEngine* const pEngine); + + // Helper functions to diagnose issues with painting and layout. + // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. + bool _fDebug = false; + }; +} diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc new file mode 100644 index 000000000..f705a2117 --- /dev/null +++ b/src/renderer/base/sources.inc @@ -0,0 +1,38 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Renderer Base +# ------------------------------------- + +# This module provides the base layer for all rendering activities. +# It will fetch data from the main console host server and prepare it +# in a rendering-engine-agnostic fashion so the console code +# can interface with displays on varying platforms. + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\Cluster.cpp \ + ..\FontInfo.cpp \ + ..\FontInfoBase.cpp \ + ..\FontInfoDesired.cpp \ + ..\RenderEngineBase.cpp \ + ..\renderer.cpp \ + ..\thread.cpp \ + +INCLUDES = \ + ..; \ + ..\..\..\inc; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ diff --git a/src/renderer/base/thread.cpp b/src/renderer/base/thread.cpp new file mode 100644 index 000000000..44cbb6be0 --- /dev/null +++ b/src/renderer/base/thread.cpp @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "thread.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Render; + +RenderThread::RenderThread() : + _pRenderer(nullptr), + _hThread(INVALID_HANDLE_VALUE), + _hEvent(INVALID_HANDLE_VALUE), + _hPaintCompletedEvent(INVALID_HANDLE_VALUE), + _fKeepRunning(true), + _hPaintEnabledEvent(INVALID_HANDLE_VALUE) +{ + +} + +RenderThread::~RenderThread() +{ + if (_hThread != INVALID_HANDLE_VALUE) + { + _fKeepRunning = false; // stop loop after final run + SignalObjectAndWait(_hEvent, _hThread, INFINITE, FALSE); // signal final paint and wait for thread to finish. + + CloseHandle(_hThread); + _hThread = INVALID_HANDLE_VALUE; + } + + if (_hEvent != INVALID_HANDLE_VALUE) + { + CloseHandle(_hEvent); + _hEvent = INVALID_HANDLE_VALUE; + } + + if (_hPaintEnabledEvent != INVALID_HANDLE_VALUE) + { + CloseHandle(_hPaintEnabledEvent); + _hEvent = INVALID_HANDLE_VALUE; + } + + if (_hPaintCompletedEvent != INVALID_HANDLE_VALUE) + { + CloseHandle(_hPaintCompletedEvent); + _hEvent = INVALID_HANDLE_VALUE; + } +} + +// Method Description: +// - Create all of the Events we'll need, and the actual thread we'll be doing +// work on. +// Arguments: +// - pRendererParent: the IRenderer that owns this thread, and which we should +// trigger frames for. +// Return Value: +// - S_OK if we succeeded, else an HRESULT corresponding to a failure to create +// an Event or Thread. +[[nodiscard]] +HRESULT RenderThread::Initialize(IRenderer* const pRendererParent) noexcept +{ + _pRenderer = pRendererParent; + + HRESULT hr = S_OK; + // Create event before thread as thread will start immediately. + if (SUCCEEDED(hr)) + { + HANDLE hEvent = CreateEventW(nullptr, // non-inheritable security attributes + FALSE, // auto reset event + FALSE, // initially unsignaled + nullptr // no name + ); + + if (hEvent == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else + { + _hEvent = hEvent; + } + } + + if (SUCCEEDED(hr)) + { + HANDLE hPaintEnabledEvent = CreateEventW(nullptr, + TRUE, // manual reset event + FALSE, // initially signaled + nullptr); + + if (hPaintEnabledEvent == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else + { + _hPaintEnabledEvent = hPaintEnabledEvent; + } + } + + if (SUCCEEDED(hr)) + { + HANDLE hPaintCompletedEvent = CreateEventW(nullptr, + TRUE, // manual reset event + TRUE, // initially signaled + nullptr); + + if (hPaintCompletedEvent == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else + { + _hPaintCompletedEvent = hPaintCompletedEvent; + } + } + + if (SUCCEEDED(hr)) + { + HANDLE hThread = CreateThread(nullptr, // non-inheritable security attributes + 0, // use default stack size + s_ThreadProc, + this, + 0, // create immediately + nullptr // we don't need the thread ID + ); + + if (hThread == nullptr) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else + { + _hThread = hThread; + } + } + + return hr; +} + +DWORD WINAPI RenderThread::s_ThreadProc(_In_ LPVOID lpParameter) +{ + RenderThread* const pContext = static_cast(lpParameter); + + if (pContext != nullptr) + { + return pContext->_ThreadProc(); + } + else + { + return (DWORD)E_INVALIDARG; + } +} + +DWORD WINAPI RenderThread::_ThreadProc() +{ + while (_fKeepRunning) + { + WaitForSingleObject(_hPaintEnabledEvent, INFINITE); + WaitForSingleObject(_hEvent, INFINITE); + + ResetEvent(_hPaintCompletedEvent); + + LOG_IF_FAILED(_pRenderer->PaintFrame()); + + SetEvent(_hPaintCompletedEvent); + + // extra check before we sleep since it's a "long" activity, relatively speaking. + if (_fKeepRunning) + { + Sleep(s_FrameLimitMilliseconds); + } + } + + return S_OK; +} + +void RenderThread::NotifyPaint() +{ + SetEvent(_hEvent); +} + +void RenderThread::EnablePainting() +{ + SetEvent(_hPaintEnabledEvent); +} + +void RenderThread::WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) +{ + // When rendering takes place via DirectX, and a console application + // currently owns the screen, and a new console application is launched (or + // the user switches to another console application), the new application + // cannot take over the screen until the active one relinquishes it. This + // blocking mechanism goes as follows: + // + // 1. The console input thread of the new console application connects to + // ConIoSrv; + // 2. While servicing the new connection request, ConIoSrv sends an event to + // the active application letting it know that it has lost focus; + // 3.1 ConIoSrv waits for a reply from the client application; + // 3.2 Meanwhile, the active application receives the focus event and calls + // the method this methed, waiting for the current paint operation to + // finish. + // + // This means that the new application is waiting on the connection request + // reply from ConIoSrv, ConIoSrv is waiting on the active application to + // acknowledge the lost focus event to reply to the new application, and the + // console input thread in the active application is waiting on the renderer + // thread to finish its current paint operation. + // + // Question: what should happen if the wait on the paint operation times + // out? + // + // There are three options: + // + // 1. On timeout, the active console application could reply with an error + // message and terminate itself, effectively relinquishing control of the + // display; + // + // 2. ConIoSrv itself could time out on waiting for a reply, and forcibly + // terminate the active console application; + // + // 3. Let the wait time out and let the user deal with it. Because the wait + // occurs on a single iteration of the renderer thread, it seemed to me that + // the likelihood of failure is extremely small, especially since the client + // console application that the active conhost instance is servicing has no + // say over what happens in the renderer thread, only by proxy. Thus, the + // chance of failure (timeout) is minimal and since the OneCoreUAP console + // is not a massively used piece of software, it didn’t seem that it would + // be a good use of time to build the requisite infrastructure to deal with + // a timeout here, at least not for now. In case of a timeout DirectX will + // catch the mistake of a new application attempting to acquire the display + // while another one still owns it and will flag it as a DWM bug. Right now, + // the active application will wait one second for the paint operation to + // finish. + // + // TODO: MSFT: 11833883 - Determine action when wait on paint operation via + // DirectX on OneCoreUAP times out while switching console + // applications. + + ResetEvent(_hPaintEnabledEvent); + WaitForSingleObject(_hPaintCompletedEvent, dwTimeoutMs); +} diff --git a/src/renderer/base/thread.hpp b/src/renderer/base/thread.hpp new file mode 100644 index 000000000..831473303 --- /dev/null +++ b/src/renderer/base/thread.hpp @@ -0,0 +1,52 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Thread.hpp + +Abstract: +- This is the definition of our rendering thread designed to throttle and compartmentalize drawing operations. + +Author(s): +- Michael Niksa (MiNiksa) Feb 2016 +--*/ + +#pragma once + +#include "..\inc\IRenderer.hpp" +#include "..\inc\IRenderThread.hpp" + +namespace Microsoft::Console::Render +{ + class RenderThread final : public IRenderThread + { + public: + RenderThread(); + virtual ~RenderThread() override; + + [[nodiscard]] + HRESULT Initialize(_In_ IRenderer* const pRendererParent) noexcept; + + void NotifyPaint() override; + + void EnablePainting() override; + void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) override; + + private: + static DWORD WINAPI s_ThreadProc(_In_ LPVOID lpParameter); + DWORD WINAPI _ThreadProc(); + + static DWORD const s_FrameLimitMilliseconds = 8; + + HANDLE _hThread; + HANDLE _hEvent; + + HANDLE _hPaintEnabledEvent; + HANDLE _hPaintCompletedEvent; + + IRenderer* _pRenderer; // Non-ownership pointer + + bool _fKeepRunning; + }; +} diff --git a/src/renderer/dirs b/src/renderer/dirs new file mode 100644 index 000000000..c6e4c0eb9 --- /dev/null +++ b/src/renderer/dirs @@ -0,0 +1,6 @@ +DIRS= \ + base \ + dx \ + gdi \ + wddmcon \ + vt \ diff --git a/src/renderer/dx/CustomTextLayout.cpp b/src/renderer/dx/CustomTextLayout.cpp new file mode 100644 index 000000000..3280a98b4 --- /dev/null +++ b/src/renderer/dx/CustomTextLayout.cpp @@ -0,0 +1,945 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "CustomTextLayout.h" + +#include +#include + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Creates a CustomTextLayout object for calculating which glyphs should be placed and where +// Arguments: +// - factory - DirectWrite factory reference in case we need other DirectWrite objects for our layout +// - analyzer - DirectWrite text analyzer from the factory that has been cached at a level above this layout (expensive to create) +// - format - The DirectWrite format object representing the size and other text properties to be applied (by default) to a layout +// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary) +// - clusters - From the backing buffer, the text to be displayed clustered by the columns it should consume. +// - width - The count of pixels available per column (the expected pixel width of every column) +CustomTextLayout::CustomTextLayout(IDWriteFactory2* const factory, + IDWriteTextAnalyzer1* const analyzer, + IDWriteTextFormat2* const format, + IDWriteFontFace5* const font, + std::basic_string_view const clusters, + size_t const width) : + _factory{ factory }, + _analyzer{ analyzer }, + _format{ format }, + _font{ font }, + _localeName{}, + _numberSubstitution{}, + _readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT }, + _runs{}, + _breakpoints{}, + _runIndex{ 0 }, + _width{ width } +{ + // Fetch the locale name out once now from the format + _localeName.resize(format->GetLocaleNameLength() + 1); // +1 for null + THROW_IF_FAILED(format->GetLocaleName(_localeName.data(), gsl::narrow(_localeName.size()))); + + for (const auto& cluster : clusters) + { + const auto cols = gsl::narrow(cluster.GetColumns()); + _textClusterColumns.push_back(cols); + _text += cluster.GetText(); + } +} + +// Routine Description: +// - Implements a drawing interface similarly to the default IDWriteTextLayout which will +// take the string from construction, analyze it for complexity, shape up the glyphs, +// and then draw the final product to the given renderer at the point and pass along +// the context information. +// - This specific class does the layout calculations and complexity analysis, not the +// final drawing. That's the renderer's job (passed in.) +// Arguments: +// - clientDrawingContext - Optional pointer to information that the renderer might need +// while attempting to graphically place the text onto the screen +// - renderer - The interface to be used for actually putting text onto the screen +// - originX - X pixel point of top left corner on final surface for drawing +// - originY - Y pixel point of top left corner on final surface for drawing +// Return Value: +// - S_OK or suitable DirectX/DirectWrite/Direct2D result code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::Draw(_In_opt_ void* clientDrawingContext, + _In_ IDWriteTextRenderer* renderer, + FLOAT originX, + FLOAT originY) +{ + RETURN_IF_FAILED(_AnalyzeRuns()); + RETURN_IF_FAILED(_ShapeGlyphRuns()); + RETURN_IF_FAILED(_DrawGlyphRuns(clientDrawingContext, renderer, { originX, originY })); + + return S_OK; +} + +// Routine Description: +// - Uses the internal text information and the analyzers/font information from construction +// to determine the complexity of the text inside this layout, compute the subsections (or runs) +// that contain similar property information, and stores that information internally. +// - We determine line breakpoints, bidirectional information, the script properties, +// number substitution, and font fallback properties in this function. +// Arguments: +// - - Uses internal state +// Return Value: +// - S_OK or suitable DirectWrite or STL error code +[[nodiscard]] +HRESULT CustomTextLayout::_AnalyzeRuns() noexcept +{ + try + { + // We're going to need the text length in UINT32 format for the DWrite calls. + // Convert it once up front. + const auto textLength = gsl::narrow(_text.size()); + + // Initially start out with one result that covers the entire range. + // This result will be subdivided by the analysis processes. + _runs.resize(1); + auto& initialRun = _runs.front(); + initialRun.nextRunIndex = 0; + initialRun.textStart = 0; + initialRun.textLength = textLength; + initialRun.bidiLevel = (_readingDirection == DWRITE_READING_DIRECTION_RIGHT_TO_LEFT); + + // Allocate enough room to have one breakpoint per code unit. + _breakpoints.resize(_text.size()); + + // Call each of the analyzers in sequence, recording their results. + RETURN_IF_FAILED(_analyzer->AnalyzeLineBreakpoints(this, 0, textLength, this)); + RETURN_IF_FAILED(_analyzer->AnalyzeBidi(this, 0, textLength, this)); + RETURN_IF_FAILED(_analyzer->AnalyzeScript(this, 0, textLength, this)); + RETURN_IF_FAILED(_analyzer->AnalyzeNumberSubstitution(this, 0, textLength, this)); + + // Perform our custom font fallback analyzer that mimics the pattern of the real analyzers. + RETURN_IF_FAILED(_AnalyzeFontFallback(this, 0, textLength)); + + // Resequence the resulting runs in order before returning to caller. + size_t totalRuns = _runs.size(); + std::vector runs; + runs.resize(totalRuns); + + UINT32 nextRunIndex = 0; + for (size_t i = 0; i < totalRuns; ++i) + { + runs.at(i) = _runs.at(nextRunIndex); + nextRunIndex = _runs.at(nextRunIndex).nextRunIndex; + } + + _runs.swap(runs); + } + CATCH_RETURN(); + return S_OK; +} + +// Routine Description: +// - Uses the internal run analysis information (from the analyze step) to map and shape out +// the glyphs from the fonts. This is effectively a loop of _ShapeGlyphRun. See it for details. +// Arguments: +// - - Uses internal state +// Return Value: +// - S_OK or suitable DirectWrite or STL error code +[[nodiscard]] +HRESULT CustomTextLayout::_ShapeGlyphRuns() noexcept +{ + try + { + // Shapes all the glyph runs in the layout. + const auto textLength = gsl::narrow(_text.size()); + + // Estimate the maximum number of glyph indices needed to hold a string. + UINT32 estimatedGlyphCount = _EstimateGlyphCount(textLength); + + _glyphIndices.resize(estimatedGlyphCount); + _glyphOffsets.resize(estimatedGlyphCount); + _glyphAdvances.resize(estimatedGlyphCount); + _glyphClusters.resize(textLength); + + UINT32 glyphStart = 0; + + // Shape each run separately. This is needed whenever script, locale, + // or reading direction changes. + for (UINT32 runIndex = 0; runIndex < _runs.size(); ++runIndex) + { + LOG_IF_FAILED(_ShapeGlyphRun(runIndex, glyphStart)); + } + + _glyphIndices.resize(glyphStart); + _glyphOffsets.resize(glyphStart); + _glyphAdvances.resize(glyphStart); + } + CATCH_RETURN(); + return S_OK; +} + +// Routine Description: +// - Calculates the following information for any one particular run of text: +// 1. Indices (finding the ID number in each font for each glyph) +// 2. Offsets (the left/right or top/bottom spacing from the baseline origin for each glyph) +// 3. Advances (the width allowed for every glyph) +// 4. Clusters (the bunches of glyphs that represent a particular combined character) +// - A run is defined by the analysis step as a substring of the original text that has similar properties +// such that it can be processed together as a unit. +// Arguments: +// - runIndex - The ID number of the internal runs array to use while shaping +// - glyphStart - On input, which portion of the internal indices/offsets/etc. arrays to use +// to write the shaping information. +// - On output, the position that should be used by the next call as its start position +// Return Value: +// - S_OK or suitable DirectWrite or STL error code +[[nodiscard]] +HRESULT CustomTextLayout::_ShapeGlyphRun(const UINT32 runIndex, UINT32& glyphStart) noexcept +{ + try + { + // Shapes a single run of text into glyphs. + // Alternately, you could iteratively interleave shaping and line + // breaking to reduce the number glyphs held onto at once. It's simpler + // for this demostration to just do shaping and line breaking as two + // separate processes, but realize that this does have the consequence that + // certain advanced fonts containing line specific features (like Gabriola) + // will shape as if the line is not broken. + + Run& run = _runs.at(runIndex); + UINT32 textStart = run.textStart; + UINT32 textLength = run.textLength; + UINT32 maxGlyphCount = static_cast(_glyphIndices.size() - glyphStart); + UINT32 actualGlyphCount = 0; + + run.glyphStart = glyphStart; + run.glyphCount = 0; + + if (textLength == 0) + { + return S_FALSE; // Nothing to do.. + } + + // Get the font for this run + ::Microsoft::WRL::ComPtr face; + if (run.font) + { + RETURN_IF_FAILED(run.font->CreateFontFace(&face)); + } + else + { + face = _font; + } + + // Allocate space for shaping to fill with glyphs and other information, + // with about as many glyphs as there are text characters. We'll actually + // need more glyphs than codepoints if they are decomposed into separate + // glyphs, or fewer glyphs than codepoints if multiple are substituted + // into a single glyph. In any case, the shaping process will need some + // room to apply those rules to even make that determintation. + + if (textLength > maxGlyphCount) + { + maxGlyphCount = _EstimateGlyphCount(textLength); + const UINT32 totalGlyphsArrayCount = glyphStart + maxGlyphCount; + _glyphIndices.resize(totalGlyphsArrayCount); + } + + std::vector textProps(textLength); + std::vector glyphProps(maxGlyphCount); + + // Get the glyphs from the text, retrying if needed. + + int tries = 0; + + HRESULT hr = S_OK; + do + { + hr = _analyzer->GetGlyphs( + &_text[textStart], + textLength, + face.Get(), + run.isSideways, // isSideways, + WI_IsFlagSet(run.bidiLevel, 1), // isRightToLeft + &run.script, + _localeName.data(), + (run.isNumberSubstituted) ? _numberSubstitution.Get() : nullptr, + nullptr, // features + nullptr, // featureLengths + 0, // featureCount + maxGlyphCount, // maxGlyphCount + &_glyphClusters[textStart], + &textProps[0], + &_glyphIndices[glyphStart], + &glyphProps[0], + &actualGlyphCount + ); + tries++; + + if (hr == E_NOT_SUFFICIENT_BUFFER) + { + // Try again using a larger buffer. + maxGlyphCount = _EstimateGlyphCount(maxGlyphCount); + UINT32 totalGlyphsArrayCount = glyphStart + maxGlyphCount; + + glyphProps.resize(maxGlyphCount); + _glyphIndices.resize(totalGlyphsArrayCount); + } + else + { + break; + } + } while (tries < 2); // We'll give it two chances. + + RETURN_IF_FAILED(hr); + + // Get the placement of the all the glyphs. + + _glyphAdvances.resize(std::max(static_cast(glyphStart + actualGlyphCount), _glyphAdvances.size())); + _glyphOffsets.resize(std::max(static_cast(glyphStart + actualGlyphCount), _glyphOffsets.size())); + + const auto fontSizeFormat = _format->GetFontSize(); + const auto fontSize = fontSizeFormat * run.fontScale; + + hr = _analyzer->GetGlyphPlacements( + &_text[textStart], + &_glyphClusters[textStart], + &textProps[0], + textLength, + &_glyphIndices[glyphStart], + &glyphProps[0], + actualGlyphCount, + face.Get(), + fontSize, + run.isSideways, + (run.bidiLevel & 1), // isRightToLeft + &run.script, + _localeName.data(), + NULL, // features + NULL, // featureRangeLengths + 0, // featureRanges + &_glyphAdvances[glyphStart], + &_glyphOffsets[glyphStart] + ); + + RETURN_IF_FAILED(hr); + + // If we need to detect font fallback, we can do it this way: + // if (!_font->Equals(face.Get())) + + // We're going to walk through and check for advances that don't match the space that we expect to give out. + { + IDWriteFontFace1* face1; + RETURN_IF_FAILED(face.Get()->QueryInterface(&face1)); + + DWRITE_FONT_METRICS1 metrics; + face1->GetMetrics(&metrics); + + // Walk through advances and space out characters that are too small to consume their box. + for (auto i = glyphStart; i < (glyphStart + actualGlyphCount); i++) + { + // Advance is how wide in pixels the glyph is + auto& advance = _glyphAdvances[i]; + + // Offsets is how far to move the origin (in pixels) from where it is + auto& offset = _glyphOffsets[i]; + + // Get how many columns we expected the glyph to have and mutiply into pixels. + const auto columns = _textClusterColumns[i]; + const auto advanceExpected = static_cast(columns * _width); + + // If what we expect is bigger than what we have... pad it out. + if (advanceExpected > advance) + { + // Get the amount of space we have leftover. + const auto diff = advanceExpected - advance; + + // Move the X offset (pixels to the right from the left edge) by half the excess space + // so half of it will be left of the glyph and the other half on the right. + offset.advanceOffset += diff / 2; + + // Set the advance to the perfect width we want. + advance = advanceExpected; + } + // If what we expect is smaller than what we have... rescale the font size to get a smaller glyph to fit. + else if (advanceExpected < advance) + { + // We need to retrieve the design information for this specific glyph so we can figure out the appropriate + // height proportional to the width that we desire. + INT32 advanceInDesignUnits; + RETURN_IF_FAILED(face1->GetDesignGlyphAdvances(1, &_glyphIndices[i], &advanceInDesignUnits)); + + DWRITE_GLYPH_METRICS glyphMetrics; + RETURN_IF_FAILED(face1->GetDesignGlyphMetrics(&_glyphIndices[i], 1, &glyphMetrics)); + + + // When things are drawn, we want the font size (as specified in the base font in the original format) + // to be scaled by some factor. + // i.e. if the original font size was 16, we might want to draw this glyph with a 15.2 size font so + // the width (and height) of the glyph will shrink to fit the monospace cell box. + + // This pattern is copied from the DxRenderer's algorithm for figuring out the font height for a specific width + // and was advised by the DirectWrite team. + const float widthAdvance = static_cast(advanceInDesignUnits) / metrics.designUnitsPerEm; + const auto fontSizeWant = advanceExpected / widthAdvance; + run.fontScale = fontSizeWant / fontSizeFormat; + + // Set the advance to the perfect width that we want. + advance = advanceExpected; + } + } + } + + // Certain fonts, like Batang, contain glyphs for hidden control + // and formatting characters. So we'll want to explicitly force their + // advance to zero. + // I'm leaving this here for future reference, but I don't think we want invisible glyphs for this renderer. + //if (run.script.shapes & DWRITE_SCRIPT_SHAPES_NO_VISUAL) + //{ + // std::fill(_glyphAdvances.begin() + glyphStart, + // _glyphAdvances.begin() + glyphStart + actualGlyphCount, + // 0.0f + // ); + //} + + // Set the final glyph count of this run and advance the starting glyph. + run.glyphCount = actualGlyphCount; + glyphStart += actualGlyphCount; + } + CATCH_RETURN(); + return S_OK; +} + +// Routine Description: +// - Takes the analyzed and shaped textual information from the layout process and +// forwards it into the given renderer in a run-by-run fashion. +// Arguments: +// - clientDrawingContext - Optional pointer to information that the renderer might need +// while attempting to graphically place the text onto the screen +// - renderer - The interface to be used for actually putting text onto the screen +// - origin - pixel point of top left corner on final surface for drawing +// Return Value: +// - S_OK or suitable DirectX/DirectWrite/Direct2D result code. +[[nodiscard]] +HRESULT CustomTextLayout::_DrawGlyphRuns(_In_opt_ void* clientDrawingContext, + IDWriteTextRenderer* renderer, + const D2D_POINT_2F origin) noexcept +{ + try + { + // We're going to start from the origin given and walk to the right for each + // sub-run that was calculated by the layout analysis. + auto mutableOrigin = origin; + + // Draw each run separately. + for (UINT32 runIndex = 0; runIndex < _runs.size(); ++runIndex) + { + // Get the run + Run& run = _runs.at(runIndex); + + // Get the font face from the font metadata provided by the fallback analysis + ::Microsoft::WRL::ComPtr face; + if (run.font) + { + RETURN_IF_FAILED(run.font->CreateFontFace(&face)); + } + else + { + face = _font; + } + + + // Prepare the glyph run and description objects by converting our + // internal storage representation into something that matches DWrite's structures. + DWRITE_GLYPH_RUN glyphRun = { 0 }; + glyphRun.bidiLevel = run.bidiLevel; + glyphRun.fontEmSize = _format->GetFontSize() * run.fontScale; + glyphRun.fontFace = face.Get(); + glyphRun.glyphAdvances = _glyphAdvances.data() + run.glyphStart; + glyphRun.glyphCount = run.glyphCount; + glyphRun.glyphIndices = _glyphIndices.data() + run.glyphStart; + glyphRun.glyphOffsets = _glyphOffsets.data() + run.glyphStart; + glyphRun.isSideways = false; + + DWRITE_GLYPH_RUN_DESCRIPTION glyphRunDescription = { 0 }; + glyphRunDescription.clusterMap = _glyphClusters.data(); + glyphRunDescription.localeName = _localeName.data(); + glyphRunDescription.string = _text.data(); + glyphRunDescription.stringLength = run.textLength; + glyphRunDescription.textPosition = run.textStart; + + // Try to draw it + RETURN_IF_FAILED(renderer->DrawGlyphRun(clientDrawingContext, + mutableOrigin.x, + mutableOrigin.y, + DWRITE_MEASURING_MODE_NATURAL, + &glyphRun, + &glyphRunDescription, + nullptr)); + + // Shift origin to the right for the next run based on the amount of space consumed. + mutableOrigin.x = std::accumulate(_glyphAdvances.begin() + run.glyphStart, + _glyphAdvances.begin() + run.glyphStart + run.glyphCount, + mutableOrigin.x); + } + } + CATCH_RETURN(); + return S_OK; +} + +// Routine Description: +// - Estimates the maximum number of glyph indices needed to hold a string of +// a given length. This is the formula given in the Uniscribe SDK and should +// cover most cases. Degenerate cases will require a reallocation. +// Arguments: +// - textLength - the number of wchar_ts in the original string +// Return Value: +// - An estimate of how many glyph spaces may be required in the shaping arrays +// to hold the data from a string of the given length. +[[nodiscard]] +UINT32 CustomTextLayout::_EstimateGlyphCount(const UINT32 textLength) noexcept +{ + // This formula is from https://docs.microsoft.com/en-us/windows/desktop/api/dwrite/nf-dwrite-idwritetextanalyzer-getglyphs + // and is the recommended formula for estimating buffer size for glyph count. + return 3 * textLength / 2 + 16; +} + +#pragma region IDWriteTextAnalysisSource methods +// Routine Description: +// - Implementation of IDWriteTextAnalysisSource::GetTextAtPosition +// - This method will retrieve a substring of the text in this layout +// to be used in an analysis step. +// Arguments: +// - textPosition - The index of the first character of the text to retrieve. +// - textString - The pointer to the first character of text at the index requested. +// - textLength - The characters available at/after the textString pointer (string length). +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::GetTextAtPosition(UINT32 textPosition, + _Outptr_result_buffer_(*textLength) WCHAR const** textString, + _Out_ UINT32* textLength) +{ + *textString = nullptr; + *textLength = 0; + + if (textPosition < _text.size()) + { + *textString = _text.data() + textPosition; + *textLength = gsl::narrow(_text.size()) - textPosition; + } + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSource::GetTextBeforePosition +// - This method will retrieve a substring of the text in this layout +// to be used in an analysis step. +// Arguments: +// - textPosition - The index one after the last character of the text to retrieve. +// - textString - The pointer to the first character of text at the index requested. +// - textLength - The characters available at/after the textString pointer (string length). +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::GetTextBeforePosition(UINT32 textPosition, + _Outptr_result_buffer_(*textLength) WCHAR const** textString, + _Out_ UINT32* textLength) +{ + *textString = nullptr; + *textLength = 0; + + if (textPosition > 0 && textPosition <= _text.size()) + { + *textString = _text.data(); + *textLength = textPosition; + } + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSource::GetParagraphReadingDirection +// - This returns the implied reading direction for this block of text (LTR/RTL/etc.) +// Arguments: +// - +// Return Value: +// - The reading direction held for this layout from construction +[[nodiscard]] +DWRITE_READING_DIRECTION STDMETHODCALLTYPE CustomTextLayout::GetParagraphReadingDirection() +{ + return _readingDirection; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSource::GetLocaleName +// - Retrieves the locale name to apply to this text. Sometimes analysis and chosen glyphs vary on locale. +// Arguments: +// - textPosition - The index of the first character in the held string for which layout information is needed +// - textLength - How many characters of the string from the index that the returned locale applies to +// - localeName - Zero terminated string of the locale name. +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::GetLocaleName(UINT32 textPosition, + _Out_ UINT32* textLength, + _Outptr_result_z_ WCHAR const** localeName) +{ + *localeName = _localeName.data(); + *textLength = gsl::narrow(_text.size()) - textPosition; + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSource::GetNumberSubstitution +// - Retrieves the number substitution object name to apply to this text. +// Arguments: +// - textPosition - The index of the first character in the held string for which layout information is needed +// - textLength - How many characters of the string from the index that the returned locale applies to +// - numberSubstitution - Object to use for substituting numbers inside the determined range +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::GetNumberSubstitution(UINT32 textPosition, + _Out_ UINT32* textLength, + _COM_Outptr_ IDWriteNumberSubstitution** numberSubstitution) +{ + *numberSubstitution = nullptr; + *textLength = gsl::narrow(_text.size()) - textPosition; + + return S_OK; +} +#pragma endregion + +#pragma region IDWriteTextAnalysisSink methods +// Routine Description: +// - Implementation of IDWriteTextAnalysisSink::SetScriptAnalysis +// - Accepts the result of the script analysis computation performed by an IDWriteTextAnalyzer and +// stores it internally for later shaping and drawing purposes. +// Arguments: +// - textPosition - The index of the first character in the string that the result applies to +// - textLength - How many characters of the string from the index that the result applies to +// - scriptAnalysis - The analysis information for all glyphs starting at position for length. +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::SetScriptAnalysis(UINT32 textPosition, + UINT32 textLength, + _In_ DWRITE_SCRIPT_ANALYSIS const* scriptAnalysis) +{ + try + { + _SetCurrentRun(textPosition); + _SplitCurrentRun(textPosition); + while (textLength > 0) + { + auto& run = _FetchNextRun(textLength); + run.script = *scriptAnalysis; + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSink::SetLineBreakpoints +// - Accepts the result of the line breakpoint computation performed by an IDWriteTextAnalyzer and +// stores it internally for later shaping and drawing purposes. +// Arguments: +// - textPosition - The index of the first character in the string that the result applies to +// - textLength - How many characters of the string from the index that the result applies to +// - scriptAnalysis - The analysis information for all glyphs starting at position for length. +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::SetLineBreakpoints(UINT32 textPosition, + UINT32 textLength, + _In_reads_(textLength) DWRITE_LINE_BREAKPOINT const* lineBreakpoints) +{ + try + { + if (textLength > 0) + { + RETURN_HR_IF_NULL(E_INVALIDARG, lineBreakpoints); + std::copy_n(lineBreakpoints, textLength, _breakpoints.begin() + textPosition); + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSink::SetBidiLevel +// - Accepts the result of the bidirectional analysis computation performed by an IDWriteTextAnalyzer and +// stores it internally for later shaping and drawing purposes. +// Arguments: +// - textPosition - The index of the first character in the string that the result applies to +// - textLength - How many characters of the string from the index that the result applies to +// - explicitLevel - The analysis information for all glyphs starting at position for length. +// - resolvedLevel - The analysis information for all glyphs starting at position for length. +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::SetBidiLevel(UINT32 textPosition, + UINT32 textLength, + UINT8 /*explicitLevel*/, + UINT8 resolvedLevel) +{ + try + { + _SetCurrentRun(textPosition); + _SplitCurrentRun(textPosition); + while (textLength > 0) + { + auto& run = _FetchNextRun(textLength); + run.bidiLevel = resolvedLevel; + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextAnalysisSink::SetNumberSubstitution +// - Accepts the result of the number substitution analysis computation performed by an IDWriteTextAnalyzer and +// stores it internally for later shaping and drawing purposes. +// Arguments: +// - textPosition - The index of the first character in the string that the result applies to +// - textLength - How many characters of the string from the index that the result applies to +// - numberSubstitution - The analysis information for all glyphs starting at position for length. +// Return Value: +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::SetNumberSubstitution(UINT32 textPosition, + UINT32 textLength, + _In_ IDWriteNumberSubstitution* numberSubstitution) +{ + try + { + _SetCurrentRun(textPosition); + _SplitCurrentRun(textPosition); + while (textLength > 0) + { + auto& run = _FetchNextRun(textLength); + run.isNumberSubstituted = (numberSubstitution != nullptr); + } + } + CATCH_RETURN(); + + return S_OK; +} +#pragma endregion + +#pragma region internal methods for mimicing text analyzer pattern but for font fallback +// Routine Description: +// - Mimics an IDWriteTextAnalyser but for font fallback calculations. +// Arguments: +// - source - a text analysis source to retrieve substrings of the text to be analyzed +// - textPosition - the index to start the substring operation +// - textLength - the length of the substring operation +// Result: +// - S_OK, STL/GSL errors, or a suitable DirectWrite failure code on font fallback analysis. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::_AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, + UINT32 textPosition, + UINT32 textLength) +{ + try + { + // Get the font fallback first + ::Microsoft::WRL::ComPtr format1; + RETURN_IF_FAILED(_format.As(&format1)); + RETURN_HR_IF_NULL(E_NOINTERFACE, format1); + + ::Microsoft::WRL::ComPtr fallback; + RETURN_IF_FAILED(format1->GetFontFallback(&fallback)); + + ::Microsoft::WRL::ComPtr collection; + RETURN_IF_FAILED(format1->GetFontCollection(&collection)); + + std::wstring familyName; + familyName.resize(format1->GetFontFamilyNameLength() + 1); + RETURN_IF_FAILED(format1->GetFontFamilyName(familyName.data(), gsl::narrow(familyName.size()))); + + const auto weight = format1->GetFontWeight(); + const auto style = format1->GetFontStyle(); + const auto stretch = format1->GetFontStretch(); + + if (!fallback) + { + ::Microsoft::WRL::ComPtr factory2; + RETURN_IF_FAILED(_factory.As(&factory2)); + factory2->GetSystemFontFallback(&fallback); + } + + // Walk through and analyze the entire string + while (textLength > 0) + { + UINT32 mappedLength = 0; + IDWriteFont* mappedFont = nullptr; + FLOAT scale = 0.0f; + + fallback->MapCharacters(source, + textPosition, + textLength, + collection.Get(), + familyName.data(), + weight, + style, + stretch, + &mappedLength, + &mappedFont, + &scale); + + RETURN_IF_FAILED(_SetMappedFont(textPosition, mappedLength, mappedFont, scale)); + + textPosition += mappedLength; + textLength -= mappedLength; + } + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Mimics an IDWriteTextAnalysisSink but for font fallback calculations with our +// Analyzer mimic method above. +// Arguments: +// - textPosition - the index to start the substring operation +// - textLength - the length of the substring operation +// - font - the font that applies to the substring range +// - scale - the scale of the font to apply +// - S_OK or appropriate STL/GSL failure code. +[[nodiscard]] +HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition, + UINT32 textLength, + IDWriteFont* const font, + FLOAT const scale) +{ + try + { + _SetCurrentRun(textPosition); + _SplitCurrentRun(textPosition); + while (textLength > 0) + { + auto& run = _FetchNextRun(textLength); + run.font = font; + run.fontScale = scale; + } + } + CATCH_RETURN(); + + return S_OK; +} + +#pragma endregion + +#pragma region internal Run manipulation functions for storing information from sink callbacks +// Routine Description: +// - Used by the sink setters, this returns a reference to the next run. +// Position and length are adjusted to now point after the current run +// being returned. +// Arguments: +// - textLength - The amount of characters for which the next analysis result will apply. +// - The starting index is implicit based on the currently chosen run. +// Return Value: +// - reference to the run needed to store analysis data +[[nodiscard]] +CustomTextLayout::LinkedRun& CustomTextLayout::_FetchNextRun(UINT32& textLength) +{ + + + const auto originalRunIndex = _runIndex; + + auto& run = _runs.at(originalRunIndex); + UINT32 runTextLength = run.textLength; + + // Split the tail if needed (the length remaining is less than the + // current run's size). + if (textLength < runTextLength) + { + runTextLength = textLength; // Limit to what's actually left. + UINT32 runTextStart = run.textStart; + + _SplitCurrentRun(runTextStart + runTextLength); + } + else + { + // Just advance the current run. + _runIndex = run.nextRunIndex; + } + + textLength -= runTextLength; + + // Return a reference to the run that was just current. + // Careful, we have to look it up again as _SplitCurrentRun can resize the array and reshuffle all the reference locations + return _runs.at(originalRunIndex); +} + +// Routine Description: +// - Move the current run to the given position. +// Since the analyzers generally return results in a forward manner, +// this will usually just return early. If not, find the +// corresponding run for the text position. +// Arguments: +// - textPosition - The index into the original string for which we want to select the corresponding run +// Return Value: +// - - Updates internal state +void CustomTextLayout::_SetCurrentRun(const UINT32 textPosition) +{ + + + if (_runIndex < _runs.size() + && _runs[_runIndex].ContainsTextPosition(textPosition)) + { + return; + } + + _runIndex = static_cast( + std::find(_runs.begin(), _runs.end(), textPosition) + - _runs.begin() + ); +} + +// Routine Description: +// - Splits the current run and adjusts the run values accordingly. +// Arguments: +// - splitPosition - The index into the run where we want to split it into two +// Return Value: +// - - Updates internal state, the back half will be selected after running +void CustomTextLayout::_SplitCurrentRun(const UINT32 splitPosition) +{ + + UINT32 runTextStart = _runs.at(_runIndex).textStart; + + if (splitPosition <= runTextStart) + return; // no change + + // Grow runs by one. + size_t totalRuns = _runs.size(); + try + { + _runs.resize(totalRuns + 1); + } + catch (...) + { + return; // Can't increase size. Return same run. + } + + // Copy the old run to the end. + LinkedRun& frontHalf = _runs[_runIndex]; + LinkedRun& backHalf = _runs.back(); + backHalf = frontHalf; + + // Adjust runs' text positions and lengths. + UINT32 splitPoint = splitPosition - runTextStart; + backHalf.textStart += splitPoint; + backHalf.textLength -= splitPoint; + frontHalf.textLength = splitPoint; + frontHalf.nextRunIndex = static_cast(totalRuns); + _runIndex = static_cast(totalRuns); +} +#pragma endregion diff --git a/src/renderer/dx/CustomTextLayout.h b/src/renderer/dx/CustomTextLayout.h new file mode 100644 index 000000000..61595c24b --- /dev/null +++ b/src/renderer/dx/CustomTextLayout.h @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include + +#include +#include +#include + +#include "../inc/Cluster.hpp" + +namespace Microsoft::Console::Render +{ + class CustomTextLayout : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | + ::Microsoft::WRL::InhibitFtmBase>, + IDWriteTextAnalysisSource, + IDWriteTextAnalysisSink> + { + public: + // Based on the Windows 7 SDK sample at https://github.com/pauldotknopf/WindowsSDK7-Samples/tree/master/multimedia/DirectWrite/CustomLayout + + CustomTextLayout(IDWriteFactory2* const factory, + IDWriteTextAnalyzer1* const analyzer, + IDWriteTextFormat2* const format, + IDWriteFontFace5* const font, + const std::basic_string_view<::Microsoft::Console::Render::Cluster> clusters, + size_t const width); + + // IDWriteTextLayout methods (but we don't actually want to implement them all, so just this one matching the existing interface) + [[nodiscard]] + HRESULT STDMETHODCALLTYPE Draw(_In_opt_ void* clientDrawingContext, + _In_ IDWriteTextRenderer* renderer, + FLOAT originX, + FLOAT originY); + + // IDWriteTextAnalysisSource methods + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetTextAtPosition(UINT32 textPosition, + _Outptr_result_buffer_(*textLength) WCHAR const** textString, + _Out_ UINT32* textLength) override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetTextBeforePosition(UINT32 textPosition, + _Outptr_result_buffer_(*textLength) WCHAR const** textString, + _Out_ UINT32* textLength) override; + [[nodiscard]] + virtual DWRITE_READING_DIRECTION STDMETHODCALLTYPE GetParagraphReadingDirection() override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetLocaleName(UINT32 textPosition, + _Out_ UINT32* textLength, + _Outptr_result_z_ WCHAR const** localeName) override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetNumberSubstitution(UINT32 textPosition, + _Out_ UINT32* textLength, + _COM_Outptr_ IDWriteNumberSubstitution** numberSubstitution) override; + + // IDWriteTextAnalysisSink methods + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE SetScriptAnalysis(UINT32 textPosition, + UINT32 textLength, + _In_ DWRITE_SCRIPT_ANALYSIS const* scriptAnalysis) override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE SetLineBreakpoints(UINT32 textPosition, + UINT32 textLength, + _In_reads_(textLength) DWRITE_LINE_BREAKPOINT const* lineBreakpoints) override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE SetBidiLevel(UINT32 textPosition, + UINT32 textLength, + UINT8 explicitLevel, + UINT8 resolvedLevel) override; + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE SetNumberSubstitution(UINT32 textPosition, + UINT32 textLength, + _In_ IDWriteNumberSubstitution* numberSubstitution) override; + protected: + // A single contiguous run of characters containing the same analysis results. + struct Run + { + Run() + : textStart(), + textLength(), + glyphStart(), + glyphCount(), + bidiLevel(), + script(), + isNumberSubstituted(), + isSideways(), + font{ nullptr }, + fontScale{ 1.0 } + { } + + UINT32 textStart; // starting text position of this run + UINT32 textLength; // number of contiguous code units covered + UINT32 glyphStart; // starting glyph in the glyphs array + UINT32 glyphCount; // number of glyphs associated with this run of text + DWRITE_SCRIPT_ANALYSIS script; + UINT8 bidiLevel; + bool isNumberSubstituted; + bool isSideways; + ::Microsoft::WRL::ComPtr font; + FLOAT fontScale; + + inline bool ContainsTextPosition(UINT32 desiredTextPosition) const + { + return desiredTextPosition >= textStart + && desiredTextPosition < textStart + textLength; + } + + inline bool operator==(UINT32 desiredTextPosition) const + { + // Search by text position using std::find + return ContainsTextPosition(desiredTextPosition); + } + }; + + // Single text analysis run, which points to the next run. + struct LinkedRun : Run + { + LinkedRun() + : nextRunIndex(0) + { } + + UINT32 nextRunIndex; // index of next run + }; + + [[nodiscard]] + LinkedRun& _FetchNextRun(UINT32& textLength); + void _SetCurrentRun(const UINT32 textPosition); + void _SplitCurrentRun(const UINT32 splitPosition); + + [[nodiscard]] + HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength); + [[nodiscard]] + HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale); + + [[nodiscard]] + HRESULT _AnalyzeRuns() noexcept; + [[nodiscard]] + HRESULT _ShapeGlyphRuns() noexcept; + [[nodiscard]] + HRESULT _ShapeGlyphRun(const UINT32 runIndex, UINT32& glyphStart) noexcept; + [[nodiscard]] + HRESULT _DrawGlyphRuns(_In_opt_ void* clientDrawingContext, + IDWriteTextRenderer* renderer, + const D2D_POINT_2F origin) noexcept; + + [[nodiscard]] + static UINT32 _EstimateGlyphCount(const UINT32 textLength) noexcept; + + + private: + const ::Microsoft::WRL::ComPtr _factory; + + // DirectWrite analyzer + const ::Microsoft::WRL::ComPtr _analyzer; + + // DirectWrite text format + const ::Microsoft::WRL::ComPtr _format; + + // DirectWrite font face + const ::Microsoft::WRL::ComPtr _font; + + // The text we're analyzing and processing into a layout + std::wstring _text; + std::vector _textClusterColumns; + size_t _width; + + // Properties of the text that might be relevant. + std::wstring _localeName; + ::Microsoft::WRL::ComPtr _numberSubstitution; + DWRITE_READING_DIRECTION _readingDirection; + + // Text analysis results + std::vector _runs; + std::vector _breakpoints; + + // Text analysis interim status variable (to assist the Analyzer Sink in operations involving _runs) + UINT32 _runIndex; + + // Glyph shaping results + std::vector _glyphOffsets; + std::vector _glyphClusters; + std::vector _glyphIndices; + std::vector _glyphAdvances; + }; +} diff --git a/src/renderer/dx/CustomTextRenderer.cpp b/src/renderer/dx/CustomTextRenderer.cpp new file mode 100644 index 000000000..8ab09321f --- /dev/null +++ b/src/renderer/dx/CustomTextRenderer.cpp @@ -0,0 +1,532 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "CustomTextRenderer.h" + +#include +#include + +using namespace Microsoft::Console::Render; + +#pragma region IDWritePixelSnapping methods +// Routine Description: +// - Implementation of IDWritePixelSnapping::IsPixelSnappingDisabled +// - Determines if we're allowed to snap text to pixels for this particular drawing context +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - isDisabled - TRUE if we do not snap to nearest pixels. FALSE otherwise. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT CustomTextRenderer::IsPixelSnappingDisabled(void* /*clientDrawingContext*/, + _Out_ BOOL* isDisabled) +{ + *isDisabled = false; + return S_OK; +} + +// Routine Description: +// - Implementation of IDWritePixelSnapping::GetPixelsPerDip +// - Retrieves the number of real monitor pixels to use per device-independent-pixel (DIP) +// - DIPs are used by DirectX all the way until the final drawing surface so things are only +// scaled at the very end and the complexity can be abstracted. +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - pixelsPerDip - The number of pixels per DIP. 96 is standard DPI. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT CustomTextRenderer::GetPixelsPerDip(void* clientDrawingContext, + _Out_ FLOAT* pixelsPerDip) +{ + DrawingContext* drawingContext = static_cast(clientDrawingContext); + + float dpiX, dpiY; + drawingContext->renderTarget->GetDpi(&dpiX, &dpiY); + *pixelsPerDip = dpiX / USER_DEFAULT_SCREEN_DPI; + return S_OK; +} + +// Routine Description: +// - Implementation of IDWritePixelSnapping::GetCurrentTransform +// - Retrieves the the matrix transform to be used while laying pixels onto the +// drawing context +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - transform - The matrix transform to use to adapt DIP representations into real monitor coordinates. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT CustomTextRenderer::GetCurrentTransform(void* clientDrawingContext, + DWRITE_MATRIX* transform) +{ + DrawingContext* drawingContext = static_cast(clientDrawingContext); + + // Matrix structures are defined identically + drawingContext->renderTarget->GetTransform((D2D1_MATRIX_3X2_F*)transform); + return S_OK; +} +#pragma endregion + +#pragma region IDWriteTextRenderer methods +// Routine Description: +// - Implementation of IDWriteTextRenderer::DrawUnderline +// - Directs us to draw an underline on the given context at the given position. +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - baselineOriginX - The text baseline position's X coordinate +// - baselineOriginY - The text baseline position's Y coordinate +// - The baseline is generally not the top nor the bottom of the "cell" that +// text is drawn into. It's usually somewhere "in the middle" and depends on the +// font and the glyphs. It can be calculated during layout and analysis in respect +// to the given font and glyphs. +// - underline - The properties of the underline that we should use for drawing +// - clientDrawingEffect - any special effect to pass along for rendering +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT CustomTextRenderer::DrawUnderline(void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + _In_ const DWRITE_UNDERLINE* underline, + IUnknown* clientDrawingEffect) +{ + _FillRectangle(clientDrawingContext, + clientDrawingEffect, + baselineOriginX, + baselineOriginY + underline->offset, + underline->width, + underline->thickness, + underline->readingDirection, + underline->flowDirection); + return S_OK; +} + +// Routine Description: +// - Implementation of IDWriteTextRenderer::DrawStrikethrough +// - Directs us to draw a strikethrough on the given context at the given position. +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - baselineOriginX - The text baseline position's X coordinate +// - baselineOriginY - The text baseline position's Y coordinate +// - The baseline is generally not the top nor the bottom of the "cell" that +// text is drawn into. It's usually somewhere "in the middle" and depends on the +// font and the glyphs. It can be calculated during layout and analysis in respect +// to the given font and glyphs. +// - strikethrough - The properties of the strikethrough that we should use for drawing +// - clientDrawingEffect - any special effect to pass along for rendering +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT CustomTextRenderer::DrawStrikethrough(void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + _In_ const DWRITE_STRIKETHROUGH* strikethrough, + IUnknown* clientDrawingEffect) +{ + _FillRectangle(clientDrawingContext, + clientDrawingEffect, + baselineOriginX, + baselineOriginY + strikethrough->offset, + strikethrough->width, + strikethrough->thickness, + strikethrough->readingDirection, + strikethrough->flowDirection); + return S_OK; +} + +// Routine Description: +// - Helper method to draw a line through our text. +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - clientDrawingEffect - any special effect passed along for rendering +// - x - The left coordinate of the rectangle +// - y - The top coordinate of the rectangle +// - width - The width of the rectangle (from X to the right) +// - height - The height of the rectangle (from Y down) +// - readingDirection - textual reading information that could affect the rectangle +// - flowDirection - textual flow information that could affect the rectangle +// Return Value: +// - S_OK +void CustomTextRenderer::_FillRectangle(void* clientDrawingContext, + IUnknown* clientDrawingEffect, + float x, + float y, + float width, + float thickness, + DWRITE_READING_DIRECTION /*readingDirection*/, + DWRITE_FLOW_DIRECTION /*flowDirection*/) +{ + DrawingContext* drawingContext = static_cast(clientDrawingContext); + + // Get brush + ID2D1Brush* brush = drawingContext->foregroundBrush; + + if (clientDrawingEffect != nullptr) + { + brush = static_cast(clientDrawingEffect); + } + + D2D1_RECT_F rect = D2D1::RectF(x, y, x + width, y + thickness); + drawingContext->renderTarget->FillRectangle(&rect, brush); +} + +// Routine Description: +// - Implementation of IDWriteTextRenderer::DrawInlineObject +// - Passes drawing control from the outer layout down into the context of an embedded object +// which can have its own drawing layout and renderer properties at a given position +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - originX - The left coordinate of the draw position +// - originY - The top coordinate of the draw position +// - inlineObject - The object to draw at the position +// - isSideways - Should be drawn vertically instead of horizontally +// - isRightToLeft - Should be drawn RTL (or bottom to top) instead of the default way +// - clientDrawingEffect - any special effect passed along for rendering +// Return Value: +// - S_OK or appropriate error from the delegated inline object's draw call +[[nodiscard]] +HRESULT CustomTextRenderer::DrawInlineObject(void* clientDrawingContext, + FLOAT originX, + FLOAT originY, + IDWriteInlineObject* inlineObject, + BOOL isSideways, + BOOL isRightToLeft, + IUnknown* clientDrawingEffect) +{ + return inlineObject->Draw(clientDrawingContext, + this, + originX, + originY, + isSideways, + isRightToLeft, + clientDrawingEffect); +} + +// Routine Description: +// - Implementation of IDWriteTextRenderer::DrawInlineObject +// - Passes drawing control from the outer layout down into the context of an embedded object +// which can have its own drawing layout and renderer properties at a given position +// Arguments: +// - clientDrawingContext - Pointer to structure of information required to draw +// - baselineOriginX - The text baseline position's X coordinate +// - baselineOriginY - The text baseline position's Y coordinate +// - The baseline is generally not the top nor the bottom of the "cell" that +// text is drawn into. It's usually somewhere "in the middle" and depends on the +// font and the glyphs. It can be calculated during layout and analysis in respect +// to the given font and glyphs. +// - measuringMode - The mode to measure glyphs in the DirectWrite context +// - glyphRun - Information on the glyphs +// - glyphRunDescription - Further metadata about the glyphs used while drawing +// - clientDrawingEffect - any special effect passed along for rendering +// Return Value: +// - S_OK, GSL/WIL/STL error, or appropriate DirectX/Direct2D/DirectWrite based error while drawing. +[[nodiscard]] +HRESULT CustomTextRenderer::DrawGlyphRun( + void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + DWRITE_MEASURING_MODE measuringMode, + const DWRITE_GLYPH_RUN* glyphRun, + const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, + IUnknown* /*clientDrawingEffect*/) +{ + // Color glyph rendering sourced from https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/DWriteColorGlyph + + DrawingContext* drawingContext = static_cast(clientDrawingContext); + + // Since we've delegated the drawing of the background of the text into this function, the origin passed in isn't actually the baseline. + // It's the top left corner. Save that off first. + D2D1_POINT_2F origin = D2D1::Point2F(baselineOriginX, baselineOriginY); + + // Then make a copy for the baseline origin (which is part way down the left side of the text, not the top or bottom). + // We'll use this baseline Origin for drawing the actual text. + D2D1_POINT_2F baselineOrigin = origin; + baselineOrigin.y += drawingContext->spacing.baseline; + + ::Microsoft::WRL::ComPtr d2dContext4; + RETURN_IF_FAILED(drawingContext->renderTarget->QueryInterface(d2dContext4.GetAddressOf())); + + // Draw the background + D2D1_RECT_F rect; + rect.top = origin.y; + rect.bottom = rect.top + drawingContext->cellSize.height; + rect.left = origin.x; + rect.right = rect.left; + + for (UINT32 i = 0; i < glyphRun->glyphCount; i++) + { + rect.right += glyphRun->glyphAdvances[i]; + } + + d2dContext4->FillRectangle(rect, drawingContext->backgroundBrush); + + // Now go onto drawing the text. + + // First check if we want a color font and try to extract color emoji first. + if (WI_IsFlagSet(drawingContext->options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT)) + { + ::Microsoft::WRL::ComPtr dwriteFactory4; + RETURN_IF_FAILED(drawingContext->dwriteFactory->QueryInterface(dwriteFactory4.GetAddressOf())); + + // The list of glyph image formats this renderer is prepared to support. + DWRITE_GLYPH_IMAGE_FORMATS supportedFormats = + DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | + DWRITE_GLYPH_IMAGE_FORMATS_CFF | + DWRITE_GLYPH_IMAGE_FORMATS_COLR | + DWRITE_GLYPH_IMAGE_FORMATS_SVG | + DWRITE_GLYPH_IMAGE_FORMATS_PNG | + DWRITE_GLYPH_IMAGE_FORMATS_JPEG | + DWRITE_GLYPH_IMAGE_FORMATS_TIFF | + DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8; + + // Determine whether there are any color glyph runs within glyphRun. If + // there are, glyphRunEnumerator can be used to iterate through them. + ::Microsoft::WRL::ComPtr glyphRunEnumerator; + HRESULT hr = dwriteFactory4->TranslateColorGlyphRun(baselineOrigin, + glyphRun, + glyphRunDescription, + supportedFormats, + measuringMode, + nullptr, + 0, + &glyphRunEnumerator); + + // If the analysis found no color glyphs in the run, just draw normally. + if (hr == DWRITE_E_NOCOLOR) + { + RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, + baselineOrigin, + measuringMode, + glyphRun, + glyphRunDescription, + drawingContext->foregroundBrush)); + } + else + { + RETURN_IF_FAILED(hr); + + ::Microsoft::WRL::ComPtr tempBrush; + + // Complex case: the run has one or more color runs within it. Iterate + // over the sub-runs and draw them, depending on their format. + for (;;) + { + BOOL haveRun; + RETURN_IF_FAILED(glyphRunEnumerator->MoveNext(&haveRun)); + if (!haveRun) + break; + + DWRITE_COLOR_GLYPH_RUN1 const* colorRun; + RETURN_IF_FAILED(glyphRunEnumerator->GetCurrentRun(&colorRun)); + + D2D1_POINT_2F currentBaselineOrigin = D2D1::Point2F(colorRun->baselineOriginX, colorRun->baselineOriginY); + + switch (colorRun->glyphImageFormat) + { + case DWRITE_GLYPH_IMAGE_FORMATS_PNG: + case DWRITE_GLYPH_IMAGE_FORMATS_JPEG: + case DWRITE_GLYPH_IMAGE_FORMATS_TIFF: + case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8: + { + // This run is bitmap glyphs. Use Direct2D to draw them. + d2dContext4->DrawColorBitmapGlyphRun(colorRun->glyphImageFormat, + currentBaselineOrigin, + &colorRun->glyphRun, + measuringMode); + } + break; + + case DWRITE_GLYPH_IMAGE_FORMATS_SVG: + { + // This run is SVG glyphs. Use Direct2D to draw them. + d2dContext4->DrawSvgGlyphRun(currentBaselineOrigin, + &colorRun->glyphRun, + drawingContext->foregroundBrush, + nullptr, // svgGlyphStyle + 0, // colorPaletteIndex + measuringMode); + } + break; + + case DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE: + case DWRITE_GLYPH_IMAGE_FORMATS_CFF: + case DWRITE_GLYPH_IMAGE_FORMATS_COLR: + default: + { + // This run is solid-color outlines, either from non-color + // glyphs or from COLR glyph layers. Use Direct2D to draw them. + + ID2D1Brush* layerBrush; + // The rule is "if 0xffff, use current brush." See: + // https://docs.microsoft.com/en-us/windows/desktop/api/dwrite_2/ns-dwrite_2-dwrite_color_glyph_run + if (colorRun->paletteIndex == 0xFFFF) + { + // This run uses the current text color. + layerBrush = drawingContext->foregroundBrush; + } + else + { + if (!tempBrush) + { + RETURN_IF_FAILED(d2dContext4->CreateSolidColorBrush(colorRun->runColor, &tempBrush)); + } + else + { + // This run specifies its own color. + tempBrush->SetColor(colorRun->runColor); + + } + layerBrush = tempBrush.Get(); + } + + // Draw the run with the selected color. + RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, + currentBaselineOrigin, + measuringMode, + &colorRun->glyphRun, + colorRun->glyphRunDescription, + layerBrush)); + } + break; + } + } + } + } + else + { + // Simple case: the run has no color glyphs. Draw the main glyph run + // using the current text color. + RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, + baselineOrigin, + measuringMode, + glyphRun, + glyphRunDescription, + drawingContext->foregroundBrush)); + } + return S_OK; +} +#pragma endregion + +[[nodiscard]] +HRESULT CustomTextRenderer::_DrawBasicGlyphRun(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, + ID2D1Brush* brush) +{ + ::Microsoft::WRL::ComPtr d2dContext4; + RETURN_IF_FAILED(clientDrawingContext->renderTarget->QueryInterface(d2dContext4.GetAddressOf())); + + // Using the context is the easiest/default way of drawing. + d2dContext4->DrawGlyphRun(baselineOrigin, glyphRun, glyphRunDescription, brush, measuringMode); + + // However, we could probably add options here and switch out to one of these other drawing methods (making it + // conditional based on the IUnknown* clientDrawingEffect or on some other switches and try these out instead: + + //_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); + //_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); + + return S_OK; +} + +[[nodiscard]] +HRESULT CustomTextRenderer::_DrawBasicGlyphRunManually(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE /*measuringMode*/, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) +{ + // This is regular text but manually + ::Microsoft::WRL::ComPtr d2dFactory; + clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf()); + + ::Microsoft::WRL::ComPtr pathGeometry; + d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf()); + + ::Microsoft::WRL::ComPtr geometrySink; + pathGeometry->Open(geometrySink.GetAddressOf()); + + glyphRun->fontFace->GetGlyphRunOutline( + glyphRun->fontEmSize, + glyphRun->glyphIndices, + glyphRun->glyphAdvances, + glyphRun->glyphOffsets, + glyphRun->glyphCount, + glyphRun->isSideways, + glyphRun->bidiLevel % 2, + geometrySink.Get() + ); + + geometrySink->Close(); + + D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); + + ::Microsoft::WRL::ComPtr transformedGeometry; + d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), + &matrixAlign, + transformedGeometry.GetAddressOf()); + + clientDrawingContext->renderTarget->FillGeometry(transformedGeometry.Get(), clientDrawingContext->foregroundBrush); + + return S_OK; +} + +[[nodiscard]] +HRESULT CustomTextRenderer::_DrawGlowGlyphRun(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE /*measuringMode*/, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) +{ + // This is glow text manually + ::Microsoft::WRL::ComPtr d2dFactory; + clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf()); + + ::Microsoft::WRL::ComPtr pathGeometry; + d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf()); + + ::Microsoft::WRL::ComPtr geometrySink; + pathGeometry->Open(geometrySink.GetAddressOf()); + + glyphRun->fontFace->GetGlyphRunOutline( + glyphRun->fontEmSize, + glyphRun->glyphIndices, + glyphRun->glyphAdvances, + glyphRun->glyphOffsets, + glyphRun->glyphCount, + glyphRun->isSideways, + glyphRun->bidiLevel % 2, + geometrySink.Get() + ); + + geometrySink->Close(); + + D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); + + ::Microsoft::WRL::ComPtr transformedGeometry; + d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), + &matrixAlign, + transformedGeometry.GetAddressOf()); + + + ::Microsoft::WRL::ComPtr alignedGeometry; + d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), + &matrixAlign, + alignedGeometry.GetAddressOf()); + + ::Microsoft::WRL::ComPtr brush; + ::Microsoft::WRL::ComPtr outlineBrush; + + clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 1.0f), brush.GetAddressOf()); + clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red, 1.0f), outlineBrush.GetAddressOf()); + + clientDrawingContext->renderTarget->DrawGeometry(transformedGeometry.Get(), outlineBrush.Get(), 2.0f); + + clientDrawingContext->renderTarget->FillGeometry(alignedGeometry.Get(), brush.Get()); + + return S_OK; +} diff --git a/src/renderer/dx/CustomTextRenderer.h b/src/renderer/dx/CustomTextRenderer.h new file mode 100644 index 000000000..186f7dcc9 --- /dev/null +++ b/src/renderer/dx/CustomTextRenderer.h @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +namespace Microsoft::Console::Render +{ + struct DrawingContext + { + DrawingContext(ID2D1RenderTarget* renderTarget, + ID2D1Brush* foregroundBrush, + ID2D1Brush* backgroundBrush, + IDWriteFactory* dwriteFactory, + const DWRITE_LINE_SPACING spacing, + const D2D_SIZE_F cellSize, + const D2D1_DRAW_TEXT_OPTIONS options = D2D1_DRAW_TEXT_OPTIONS_NONE) + { + this->renderTarget = renderTarget; + this->foregroundBrush = foregroundBrush; + this->backgroundBrush = backgroundBrush; + this->dwriteFactory = dwriteFactory; + this->spacing = spacing; + this->cellSize = cellSize; + this->options = options; + } + + ID2D1RenderTarget* renderTarget; + ID2D1Brush* foregroundBrush; + ID2D1Brush* backgroundBrush; + IDWriteFactory* dwriteFactory; + DWRITE_LINE_SPACING spacing; + D2D_SIZE_F cellSize; + D2D1_DRAW_TEXT_OPTIONS options; + }; + + class CustomTextRenderer : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | + ::Microsoft::WRL::InhibitFtmBase>, + IDWriteTextRenderer> + { + public: + + // http://www.charlespetzold.com/blog/2014/01/Character-Formatting-Extensions-with-DirectWrite.html + // https://docs.microsoft.com/en-us/windows/desktop/DirectWrite/how-to-implement-a-custom-text-renderer + + // IDWritePixelSnapping methods + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled(void* clientDrawingContext, + _Out_ BOOL* isDisabled) override; + + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetPixelsPerDip(void* clientDrawingContext, + _Out_ FLOAT* pixelsPerDip) override; + + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE GetCurrentTransform(void* clientDrawingContext, + _Out_ DWRITE_MATRIX* transform) override; + + // IDWriteTextRenderer methods + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE DrawGlyphRun(void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, + IUnknown* clientDrawingEffect) override; + + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE DrawUnderline(void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + _In_ const DWRITE_UNDERLINE* underline, + IUnknown* clientDrawingEffect) override; + + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE DrawStrikethrough(void* clientDrawingContext, + FLOAT baselineOriginX, + FLOAT baselineOriginY, + _In_ const DWRITE_STRIKETHROUGH * strikethrough, + IUnknown* clientDrawingEffect) override; + + [[nodiscard]] + virtual HRESULT STDMETHODCALLTYPE DrawInlineObject(void* clientDrawingContext, + FLOAT originX, + FLOAT originY, + IDWriteInlineObject* inlineObject, + BOOL isSideways, + BOOL isRightToLeft, + IUnknown* clientDrawingEffect) override; + private: + void _FillRectangle(void* clientDrawingContext, + IUnknown* clientDrawingEffect, + float x, float y, float width, float thickness, + DWRITE_READING_DIRECTION readingDirection, + DWRITE_FLOW_DIRECTION flowDirection); + + [[nodiscard]] + HRESULT _DrawBasicGlyphRun(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, + ID2D1Brush* brush); + + [[nodiscard]] + HRESULT _DrawBasicGlyphRunManually(DrawingContext*clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription); + + [[nodiscard]] + HRESULT _DrawGlowGlyphRun(DrawingContext* clientDrawingContext, + D2D1_POINT_2F baselineOrigin, + DWRITE_MEASURING_MODE measuringMode, + _In_ const DWRITE_GLYPH_RUN* glyphRun, + _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription); + }; +} diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp new file mode 100644 index 000000000..9b44bb58d --- /dev/null +++ b/src/renderer/dx/DxRenderer.cpp @@ -0,0 +1,1592 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DxRenderer.hpp" +#include "CustomTextLayout.h" + +#include "../../interactivity/win32/CustomWindowMessages.h" +#include "../../types/inc/Viewport.hpp" +#include "../../inc/unicode.hpp" +#include "../../inc/DefaultSettings.h" + +#pragma hdrstop + +static constexpr float POINTS_PER_INCH = 72.0f; + +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Constructs a DirectX-based renderer for console text +// which primarily uses DirectWrite on a Direct2D surface +DxEngine::DxEngine() : + RenderEngineBase(), + _isInvalidUsed{ false }, + _invalidRect{ 0 }, + _invalidScroll{ 0 }, + _presentParams{ 0 }, + _presentReady{ false }, + _presentScroll{ 0 }, + _presentDirty{ 0 }, + _presentOffset{ 0 }, + _isEnabled{ false }, + _isPainting{ false }, + _displaySizePixels{ 0 }, + _foregroundColor{ 0 }, + _backgroundColor{ 0 }, + _glyphCell{ 0 }, + _haveDeviceResources{ false }, + _hwndTarget{ static_cast(INVALID_HANDLE_VALUE) }, + _sizeTarget{ 0 }, + _dpi{ USER_DEFAULT_SCREEN_DPI }, + _scale{ 1.0f }, + _chainMode{ SwapChainMode::ForComposition }, + _customRenderer{ ::Microsoft::WRL::Make() } +{ + THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_PPV_ARGS(&_d2dFactory))); + + THROW_IF_FAILED(DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(_dwriteFactory), + reinterpret_cast(_dwriteFactory.GetAddressOf()) + )); +} + +// Routine Description: +// - Destroys an instance of the DirectX rendering engine +DxEngine::~DxEngine() +{ + _ReleaseDeviceResources(); +} + +// Routine Description: +// - Sets this engine to enabled allowing painting and presentation to occur +// Arguments: +// - +// Return Value: +// - Generally S_OK, but might return a DirectX or memory error if +// resources need to be created or adjusted when enabling to prepare for draw +// Can give invalid state if you enable an enabled class. +[[nodiscard]] +HRESULT DxEngine::Enable() noexcept +{ + return _EnableDisplayAccess(true); +} + +// Routine Description: +// - Sets this engine to disabled to prevent painting and presentation from occuring +// Arguments: +// - +// Return Value: +// - Should be OK. We might close/free resources, but that shouldn't error. +// Can give invalid state if you disable a disabled class. +[[nodiscard]] +HRESULT DxEngine::Disable() noexcept +{ + return _EnableDisplayAccess(false); +} + +// Routine Description: +// - Helper to enable/disable painting/display access/presentation in a unified +// manner between enable/disable functions. +// Arguments: +// - outputEnabled - true to enable, false to disable +// Return Value: +// - Generally OK. Can return invalid state if you set to the state that is already +// active (enabling enabled, disabling disabled). +[[nodiscard]] +HRESULT DxEngine::_EnableDisplayAccess(const bool outputEnabled) noexcept +{ + // Invalid state if we're setting it to the same as what we already have. + RETURN_HR_IF(E_NOT_VALID_STATE, outputEnabled == _isEnabled); + + _isEnabled = outputEnabled; + if (!_isEnabled) { + _ReleaseDeviceResources(); + } + + return S_OK; +} + +// Routine Description; +// - Creates device-specific resources required for drawing +// which generally means those that are represented on the GPU and can +// vary based on the monitor, display adapter, etc. +// - These may need to be recreated during the course of painting a frame +// should something about that hardware pipeline change. +// - Will free device resources that already existed as first operation. +// Arguments: +// - createSwapChain - If true, we create the entire rendering pipeline +// - If false, we just set up the adapter. +// Return Value: +// - Could be any DirectX/D3D/D2D/DXGI/DWrite error or memory issue. +[[nodiscard]] +HRESULT DxEngine::_CreateDeviceResources(const bool createSwapChain) noexcept +{ + if (_haveDeviceResources) + { + _ReleaseDeviceResources(); + } + + auto freeOnFail = wil::scope_exit([&] { _ReleaseDeviceResources(); }); + + RETURN_IF_FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&_dxgiFactory2))); + + RETURN_IF_FAILED(_dxgiFactory2->EnumAdapters1(0, &_dxgiAdapter1)); + + const DWORD DeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | + // This causes problems for folks who do not have the whole DirectX SDK installed + // when they try to run the rest of the project in debug mode. + // As such, I'm leaving this flag here for people doing DX-specific work to toggle it + // only when they need it and shutting it off otherwise. + // Find out more about the debug layer here: + // https://docs.microsoft.com/en-us/windows/desktop/direct3d11/overviews-direct3d-11-devices-layers + // You can find out how to install it here: + // https://docs.microsoft.com/en-us/windows/uwp/gaming/use-the-directx-runtime-and-visual-studio-graphics-diagnostic-features + // D3D11_CREATE_DEVICE_DEBUG | + D3D11_CREATE_DEVICE_SINGLETHREADED; + + D3D_FEATURE_LEVEL FeatureLevels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_1, + }; + + RETURN_IF_FAILED(D3D11CreateDevice(_dxgiAdapter1.Get(), + D3D_DRIVER_TYPE_UNKNOWN, + NULL, + DeviceFlags, + FeatureLevels, + ARRAYSIZE(FeatureLevels), + D3D11_SDK_VERSION, + &_d3dDevice, + NULL, + &_d3dDeviceContext)); + + RETURN_IF_FAILED(_dxgiAdapter1->EnumOutputs(0, &_dxgiOutput)); + + _displaySizePixels = _GetClientSize(); + + if (createSwapChain) { + + DXGI_SWAP_CHAIN_DESC1 SwapChainDesc = { 0 }; + SwapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + SwapChainDesc.BufferCount = 2; + SwapChainDesc.SampleDesc.Count = 1; + SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + SwapChainDesc.Scaling = DXGI_SCALING_NONE; + + switch (_chainMode) + { + case SwapChainMode::ForHwnd: + { + // use the HWND's dimensions for the swap chain dimensions. + RECT rect = { 0 }; + RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &rect)); + + SwapChainDesc.Width = rect.right - rect.left; + SwapChainDesc.Height = rect.bottom - rect.top; + + // We can't do alpha for HWNDs. Set to ignore. It will fail otherwise. + SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + + RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForHwnd(_d3dDevice.Get(), + _hwndTarget, + &SwapChainDesc, + nullptr, + nullptr, + &_dxgiSwapChain)); + break; + } + case SwapChainMode::ForComposition: + { + // Use the given target size for compositions. + SwapChainDesc.Width = _displaySizePixels.cx; + SwapChainDesc.Height = _displaySizePixels.cy; + + // We're doing advanced composition pretty much for the purpose of pretty alpha, so turn it on. + SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; + // It's 100% required to use scaling mode stretch for composition. There is no other choice. + SwapChainDesc.Scaling = DXGI_SCALING_STRETCH; + + RETURN_IF_FAILED(_dxgiFactory2->CreateSwapChainForComposition(_d3dDevice.Get(), + &SwapChainDesc, + nullptr, + &_dxgiSwapChain)); + break; + } + default: + THROW_HR(E_NOTIMPL); + } + + // With a new swap chain, mark the entire thing as invalid. + RETURN_IF_FAILED(InvalidateAll()); + + RETURN_IF_FAILED(_PrepareRenderTarget()); + } + + _haveDeviceResources = true; + if (_isPainting) { + // TODO: MSFT: 21169176 - remove this or restore the "try a few times to render" code... I think + _d2dRenderTarget->BeginDraw(); + } + + freeOnFail.release(); // don't need to release if we made it to the bottom and everything was good. + + // Notify that swap chain changed. + if (_pfn) + { + _pfn(); + } + + return S_OK; +} + +[[nodiscard]] +HRESULT DxEngine::_PrepareRenderTarget() noexcept +{ + RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, IID_PPV_ARGS(&_dxgiSurface))); + + D2D1_RENDER_TARGET_PROPERTIES props = + D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), + 0.0f, + 0.0f); + + RETURN_IF_FAILED(_d2dFactory->CreateDxgiSurfaceRenderTarget(_dxgiSurface.Get(), + &props, + &_d2dRenderTarget)); + + _d2dRenderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + RETURN_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::DarkRed), + &_d2dBrushBackground)); + + _d2dBrushBackground->SetOpacity(.9f); + + RETURN_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), + &_d2dBrushForeground)); + + // If in composition mode, apply scaling factor matrix + if (_chainMode == SwapChainMode::ForComposition) + { + const auto fdpi = static_cast(_dpi); + _d2dRenderTarget->SetDpi(fdpi, fdpi); + + DXGI_MATRIX_3X2_F inverseScale = { 0 }; + inverseScale._11 = 1.0f / _scale; + inverseScale._22 = inverseScale._11; + + ::Microsoft::WRL::ComPtr sc2; + RETURN_IF_FAILED(_dxgiSwapChain.As(&sc2)); + + RETURN_IF_FAILED(sc2->SetMatrixTransform(&inverseScale)); + } + + return S_OK; +} + +// Routine Description: +// - Releases device-specific resources (typically held on the GPU) +// Arguments: +// - +// Return Value: +// - +void DxEngine::_ReleaseDeviceResources() noexcept +{ + _haveDeviceResources = false; + _d2dBrushForeground.Reset(); + _d2dBrushBackground.Reset(); + + if (nullptr != _d2dRenderTarget.Get() && _isPainting) + { + _d2dRenderTarget->EndDraw(); + } + + _d2dRenderTarget.Reset(); + + _dxgiSurface.Reset(); + _dxgiSwapChain.Reset(); + _dxgiOutput.Reset(); + + if (nullptr != _d3dDeviceContext.Get()) + { + // To ensure the swap chain goes away we must unbind any views from the + // D3D pipeline + _d3dDeviceContext->OMSetRenderTargets(0, nullptr, nullptr); + } + _d3dDeviceContext.Reset(); + + _d3dDevice.Reset(); + + _dxgiAdapter1.Reset(); + _dxgiFactory2.Reset(); +} + +// Routine Description: +// - Helper to create a DirectWrite text layout object +// out of a string. +// Arguments: +// - string - The text to attempt to layout +// - stringLength - Length of string above in characters +// - ppTextLayout - Location to receive new layout object +// Return Value: +// - S_OK if layout created successfully, otherwise a DirectWrite error +[[nodiscard]] +HRESULT DxEngine::_CreateTextLayout( + _In_reads_(stringLength) PCWCHAR string, + _In_ size_t stringLength, + _Out_ IDWriteTextLayout **ppTextLayout) noexcept +{ + return _dwriteFactory->CreateTextLayout(string, + static_cast(stringLength), + _dwriteTextFormat.Get(), + (float)_displaySizePixels.cx, + _glyphCell.cy != 0 ? _glyphCell.cy : (float)_displaySizePixels.cy, + ppTextLayout); +} + +// Routine Description: +// - Sets the target window handle for our display pipeline +// - We will take over the surface of this window for drawing +// Arguments: +// - hwnd - Window handle +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::SetHwnd(const HWND hwnd) noexcept +{ + _hwndTarget = hwnd; + _chainMode = SwapChainMode::ForHwnd; + return S_OK; +} + +[[nodiscard]] +HRESULT DxEngine::SetWindowSize(const SIZE Pixels) noexcept +{ + _sizeTarget = Pixels; + + RETURN_IF_FAILED(InvalidateAll()); + + return S_OK; +} + +void DxEngine::SetCallback(std::function pfn) +{ + _pfn = pfn; +} + +Microsoft::WRL::ComPtr DxEngine::GetSwapChain() noexcept +{ + if (_dxgiSwapChain.Get() == nullptr) + { + THROW_IF_FAILED(_CreateDeviceResources(true)); + } + + return _dxgiSwapChain; +} + +// Routine Description: +// - Invalidates a rectangle described in characters +// Arguments: +// - psrRegion - Character rectangle +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept +{ + _InvalidOr(*psrRegion); + return S_OK; +} + +// Routine Description: +// - Invalidates one specific character coordinate +// Arguments: +// - pcoordCursor - single point in the character cell grid +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +{ + SMALL_RECT sr = Microsoft::Console::Types::Viewport::FromCoord(*pcoordCursor).ToInclusive(); + return Invalidate(&sr); +} + +// Routine Description: +// - Invalidates a rectangle describing a pixel area on the display +// Arguments: +// - prcDirtyClient - pixel rectangle +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept +{ + _InvalidOr(*prcDirtyClient); + + return S_OK; +} + +// Routine Description: +// - Invalidates a series of character rectangles +// Arguments: +// - rectangles - One or more rectangles describing character positions on the grid +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::InvalidateSelection(const std::vector& rectangles) noexcept +{ + for (const auto& rect : rectangles) + { + RETURN_IF_FAILED(Invalidate(&rect)); + } + return S_OK; +} + +// Routine Description: +// - Scrolls the existing dirty region (if it exists) and +// invalidates the area that is uncovered in the window. +// Arguments: +// - pcoordDelta - The number of characters to move and uncover. +// - -Y is up, Y is down, -X is left, X is right. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +{ + if (pcoordDelta->X != 0 || pcoordDelta->Y != 0) + { + POINT delta = { 0 }; + delta.x = pcoordDelta->X * _glyphCell.cx; + delta.y = pcoordDelta->Y * _glyphCell.cy; + + _InvalidOffset(delta); + + _invalidScroll.cx += delta.x; + _invalidScroll.cy += delta.y; + + // Add the revealed portion of the screen from the scroll to the invalid area. + const RECT display = _GetDisplayRect(); + RECT reveal = display; + + // X delta first + OffsetRect(&reveal, delta.x, 0); + IntersectRect(&reveal, &reveal, &display); + SubtractRect(&reveal, &display, &reveal); + + if (!IsRectEmpty(&reveal)) + { + _InvalidOr(reveal); + } + + // Y delta second (subtract rect won't work if you move both) + reveal = display; + OffsetRect(&reveal, 0, delta.y); + IntersectRect(&reveal, &reveal, &display); + SubtractRect(&reveal, &display, &reveal); + + if (!IsRectEmpty(&reveal)) + { + _InvalidOr(reveal); + } + } + + return S_OK; +} + +// Routine Description: +// - Invalidates the entire window area +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::InvalidateAll() noexcept +{ + const RECT screen = _GetDisplayRect(); + _InvalidOr(screen); + + return S_OK; +} + +// Routine Description: +// - This currently has no effect in this renderer. +// Arguments: +// - pForcePaint - Always filled with false +// Return Value: +// - S_FALSE because we don't use this. +[[nodiscard]] +HRESULT DxEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +// Routine Description: +// - Gets the area in pixels of the surface we are targeting +// Arguments: +// - +// Return Value: +// - X by Y area in pixels of the surface +[[nodiscard]] +SIZE DxEngine::_GetClientSize() const noexcept +{ + switch (_chainMode) + { + case SwapChainMode::ForHwnd: + { + RECT clientRect = { 0 }; + LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &clientRect)); + + SIZE clientSize = { 0 }; + clientSize.cx = clientRect.right - clientRect.left; + clientSize.cy = clientRect.bottom - clientRect.top; + + return clientSize; + } + case SwapChainMode::ForComposition: + { + SIZE size = _sizeTarget; + size.cx = static_cast(size.cx * _scale); + size.cy = static_cast(size.cy * _scale); + return size; + } + default: + THROW_HR(E_NOTIMPL); + } +} + +// Routine Description: +// - Helper to multiply all parameters of a rectangle by the font size +// to convert from characters to pixels. +// Arguments: +// - cellsToPixels - rectangle to update +// - fontSize - scaling factors +// Return Value: +// - - Updates reference +void _ScaleByFont(RECT& cellsToPixels, SIZE fontSize) noexcept +{ + cellsToPixels.left *= fontSize.cx; + cellsToPixels.right *= fontSize.cx; + cellsToPixels.top *= fontSize.cy; + cellsToPixels.bottom *= fontSize.cy; +} + +// Routine Description: +// - Retrieves a rectangle representation of the pixel size of the +// surface we are drawing on +// Arguments: +// - +// Return Value; +// - Origin-placed rectangle representing the pixel size of the surface +[[nodiscard]] +RECT DxEngine::_GetDisplayRect() const noexcept +{ + return { 0, 0, _displaySizePixels.cx, _displaySizePixels.cy }; +} + +// Routine Description: +// - Helper to shift the existing dirty rectangle by a pixel offset +// and crop it to still be within the bounds of the display surface +// Arguments: +// - delta - Adjustment distance in pixels +// - -Y is up, Y is down, -X is left, X is right. +// Return Value: +// - +void DxEngine::_InvalidOffset(POINT delta) noexcept +{ + if (_isInvalidUsed) + { + // Copy the existing invalid rect + RECT invalidNew = _invalidRect; + + // Offset it to the new position + THROW_IF_WIN32_BOOL_FALSE(OffsetRect(&invalidNew, delta.x, delta.y)); + + // Get the rect representing the display + const RECT rectScreen = _GetDisplayRect(); + + // Ensure that the new invalid rectangle is still on the display + IntersectRect(&invalidNew, &invalidNew, &rectScreen); + + _invalidRect = invalidNew; + } +} + +// Routine description: +// - Adds the given character rectangle to the total dirty region +// - Will scale internally to pixels based on the current font. +// Arguments: +// - sr - character rectangle +// Return Value: +// - +void DxEngine::_InvalidOr(SMALL_RECT sr) noexcept +{ + RECT region; + region.left = sr.Left; + region.top = sr.Top; + region.right = sr.Right; + region.bottom = sr.Bottom; + _ScaleByFont(region, _glyphCell); + + region.right += _glyphCell.cx; + region.bottom += _glyphCell.cy; + + _InvalidOr(region); +} + +// Routine Description: +// - Adds the given pixel rectangle to the total dirty region +// Arguments: +// - rc - Dirty pixel rectangle +// Return Value: +// - +void DxEngine::_InvalidOr(RECT rc) noexcept +{ + if (_isInvalidUsed) + { + UnionRect(&_invalidRect, &_invalidRect, &rc); + + RECT rcScreen = _GetDisplayRect(); + IntersectRect(&_invalidRect, &_invalidRect, &rcScreen); + } + else + { + _invalidRect = rc; + _isInvalidUsed = true; + } +} + +// Routine Description: +// - This is unused by this renderer. +// Arguments: +// - pForcePaint - always filled with false. +// Return Value: +// - S_FALSE because this is unused. +[[nodiscard]] +HRESULT DxEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +// Routine description: +// - Prepares the surfaces for painting and begins a drawing batch +// Arguments: +// - +// Return Value: +// - Any DirectX error, a memory error, etc. +[[nodiscard]] +HRESULT DxEngine::StartPaint() noexcept +{ + FAIL_FAST_IF_FAILED(InvalidateAll()); + RETURN_HR_IF(E_NOT_VALID_STATE, _isPainting); // invalid to start a paint while painting. + + if (_isEnabled) { + const auto clientSize = _GetClientSize(); + if (!_haveDeviceResources) + { + RETURN_IF_FAILED(_CreateDeviceResources(true)); + } + else if (_displaySizePixels.cy != clientSize.cy || + _displaySizePixels.cx != clientSize.cx) + { + _dxgiSurface.Reset(); + _d2dRenderTarget.Reset(); + _dxgiSwapChain->ResizeBuffers(2, clientSize.cx, clientSize.cy, DXGI_FORMAT_B8G8R8A8_UNORM, 0); + RETURN_IF_FAILED(_PrepareRenderTarget()); + _displaySizePixels = clientSize; + } + + _d2dRenderTarget->BeginDraw(); + _isPainting = true; + } + + return S_OK; +} + +// Routine Description: +// - Ends batch drawing and captures any state necessary for presentation +// Arguments: +// - +// Return Value: +// - Any DirectX error, a memory error, etc. +[[nodiscard]] +HRESULT DxEngine::EndPaint() noexcept +{ + RETURN_HR_IF(E_INVALIDARG, !_isPainting); // invalid to end paint when we're not painting + + HRESULT hr = S_OK; + + if (_haveDeviceResources) { + _isPainting = false; + + hr = _d2dRenderTarget->EndDraw(); + + if (SUCCEEDED(hr)) { + + if (_invalidScroll.cy != 0 || _invalidScroll.cx != 0) + { + _presentDirty = _invalidRect; + + RECT display = _GetDisplayRect(); + SubtractRect(&_presentScroll, &display, &_presentDirty); + _presentOffset.x = _invalidScroll.cx; + _presentOffset.y = _invalidScroll.cy; + + _presentParams.DirtyRectsCount = 1; + _presentParams.pDirtyRects = &_presentDirty; + + _presentParams.pScrollOffset = &_presentOffset; + _presentParams.pScrollRect = &_presentScroll; + + if (IsRectEmpty(&_presentScroll)) + { + _presentParams.pScrollRect = nullptr; + _presentParams.pScrollOffset = nullptr; + } + } + + _presentReady = true; + } + else + { + _presentReady = false; + _ReleaseDeviceResources(); + } + } + + _invalidRect = { 0 }; + _isInvalidUsed = false; + + _invalidScroll = { 0 }; + + return hr; +} + +// Routine Description: +// - Copies the front surface of the swap chain (the one being displayed) +// to the back surface of the swap chain (the one we draw on next) +// so we can draw on top of what's already there. +// Arguments: +// - +// Return Value: +// - Any DirectX error, a memory error, etc. +[[nodiscard]] +HRESULT DxEngine::_CopyFrontToBack() noexcept +{ + Microsoft::WRL::ComPtr backBuffer; + Microsoft::WRL::ComPtr frontBuffer; + + RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer))); + RETURN_IF_FAILED(_dxgiSwapChain->GetBuffer(1, IID_PPV_ARGS(&frontBuffer))); + + _d3dDeviceContext->CopyResource(backBuffer.Get(), frontBuffer.Get()); + + return S_OK; +} + +// Routine Description: +// - Takes queued drawing information and presents it to the screen. +// - This is separated out so it can be done outside the lock as it's expensive +// Arguments: +// - +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] +HRESULT DxEngine::Present() noexcept +{ + if (_presentReady) + { + FAIL_FAST_IF_FAILED(_dxgiSwapChain->Present(1, 0)); + /*FAIL_FAST_IF_FAILED(_dxgiSwapChain->Present1(1, 0, &_presentParams));*/ + + RETURN_IF_FAILED(_CopyFrontToBack()); + _presentReady = false; + + _presentDirty = { 0 }; + _presentOffset = { 0 }; + _presentScroll = { 0 }; + _presentParams = { 0 }; + } + + return S_OK; +} + +// Routine Description: +// - This is currently unused. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::ScrollFrame() noexcept +{ + return S_OK; +} + +// Routine Description: +// - This paints in the back most layer of the frame with the background color. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::PaintBackground() noexcept +{ + /*_d2dRenderTarget->FillRectangle(D2D1::RectF((float)_invalidRect.left, + (float)_invalidRect.top, + (float)_invalidRect.right, + (float)_invalidRect.bottom), + _d2dBrushBackground.Get()); +*/ + + D2D1_COLOR_F nothing = { 0 }; + + _d2dRenderTarget->Clear(nothing); + + return S_OK; +} + +// Routine Description: +// - Places one line of text onto the screen at the given position +// Arguments: +// - clusters - Iterable collection of cluster information (text and columns it should consume) +// - coord - Character coordinate position in the cell grid +// - fTrimLeft - Whether or not to trim off the left half of a double wide character +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] +HRESULT DxEngine::PaintBufferLine(std::basic_string_view const clusters, + COORD const coord, + const bool /*trimLeft*/) noexcept +{ + try + { + // Calculate positioning of our origin. + D2D1_POINT_2F origin; + origin.x = static_cast(coord.X * _glyphCell.cx); + origin.y = static_cast(coord.Y * _glyphCell.cy); + + // Create the text layout + CustomTextLayout layout(_dwriteFactory.Get(), + _dwriteTextAnalyzer.Get(), + _dwriteTextFormat.Get(), + _dwriteFontFace.Get(), + clusters, + _glyphCell.cx); + + // Get the baseline for this font as that's where we draw from + DWRITE_LINE_SPACING spacing; + RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing)); + + // Assemble the drawing context information + DrawingContext context(_d2dRenderTarget.Get(), + _d2dBrushForeground.Get(), + _d2dBrushBackground.Get(), + _dwriteFactory.Get(), + spacing, + D2D1::SizeF(gsl::narrow(_glyphCell.cx), gsl::narrow(_glyphCell.cy)), + D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); + + // Layout then render the text + RETURN_IF_FAILED(layout.Draw(&context, _customRenderer.Get(), origin.x, origin.y)); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Paints lines around cells (draws in pieces of the grid) +// Arguments: +// - lines - Which grid lines (top, left, bottom, right) to draw +// - color - The color to use for drawing the lines +// - cchLine - Length of the line to draw in character cells +// - coordTarget - The X,Y character position in the grid where we should start drawing +// - We will draw rightward (+X) from here +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] +HRESULT DxEngine::PaintBufferGridLines(GridLines const lines, + COLORREF const color, + size_t const cchLine, + COORD const coordTarget) noexcept +{ + const auto existingColor = _d2dBrushForeground->GetColor(); + const auto restoreBrushOnExit = wil::scope_exit([&] {_d2dBrushForeground->SetColor(existingColor); }); + + _d2dBrushForeground->SetColor(D2D1::ColorF(color)); + + const auto font = _GetFontSize(); + D2D_POINT_2F target; + target.x = static_cast(coordTarget.X) * font.X; + target.y = static_cast(coordTarget.Y) * font.Y; + + D2D_POINT_2F start = { 0 }; + D2D_POINT_2F end = { 0 }; + + for (size_t i = 0; i < cchLine; i++) + { + start = target; + + if (lines & GridLines::Top) + { + end = start; + end.x += font.X; + + _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get()); + } + + if (lines & GridLines::Left) + { + end = start; + end.y += font.Y; + + _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get()); + } + + // NOTE: Watch out for inclusive/exclusive rectangles here. + // We have to remove 1 from the font size for the bottom and right lines to ensure that the + // starting point remains within the clipping rectangle. + // For example, if we're drawing a letter at 0,0 and the font size is 8x16.... + // The bottom left corner inclusive is at 0,15 which is Y (0) + Font Height (16) - 1 = 15. + // The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7. + + start = target; + start.y += font.Y - 1; + + if (lines & GridLines::Bottom) + { + end = start; + end.x += font.X; + + _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get()); + } + + start = target; + start.x += font.X - 1; + + if (lines & GridLines::Right) + { + end = start; + end.y += font.Y; + + _d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get()); + } + + // Move to the next character in this run. + target.x += font.X; + } + + return S_OK; +} + +// Routine Description: +// - Paints an overlay highlight on a portion of the frame to represent selected text +// Arguments: +// - rect - Rectangle to invert or highlight to make the selection area +// Return Value: +// - S_OK or relevant DirectX error. +[[nodiscard]] +HRESULT DxEngine::PaintSelection(const SMALL_RECT rect) noexcept +{ + const auto existingColor = _d2dBrushForeground->GetColor(); + const auto selectionColor = D2D1::ColorF(_defaultForegroundColor.r, + _defaultForegroundColor.g, + _defaultForegroundColor.b, + 0.5f); + + _d2dBrushForeground->SetColor(selectionColor); + const auto resetColorOnExit = wil::scope_exit([&] {_d2dBrushForeground->SetColor(existingColor); }); + + RECT pixels; + pixels.left = rect.Left * _glyphCell.cx; + pixels.top = rect.Top * _glyphCell.cy; + pixels.right = rect.Right * _glyphCell.cx; + pixels.bottom = rect.Bottom * _glyphCell.cy; + + D2D1_RECT_F draw = { 0 }; + draw.left = static_cast(pixels.left); + draw.top = static_cast(pixels.top); + draw.right = static_cast(pixels.right); + draw.bottom = static_cast(pixels.bottom); + + _d2dRenderTarget->FillRectangle(draw, _d2dBrushForeground.Get()); + + return S_OK; +} + +// Helper to choose which Direct2D method to use when drawing the cursor rectangle +enum class CursorPaintType +{ + Fill, + Outline +}; + +// Routine Description: +// - Draws a block at the given position to represent the cursor +// - May be a styled cursor at the character cell location that is less than a full block +// Arguments: +// - options - Packed options relevant to how to draw the cursor +// Return Value: +// - S_OK or relevant DirectX error. +[[nodiscard]] +HRESULT DxEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept +{ + // if the cursor is off, do nothing - it should not be visible. + if (!options.isOn) + { + return S_FALSE; + } + // Create rectangular block representing where the cursor can fill. + D2D1_RECT_F rect = { 0 }; + rect.left = static_cast(options.coordCursor.X * _glyphCell.cx); + rect.top = static_cast(options.coordCursor.Y * _glyphCell.cy); + rect.right = static_cast(rect.left + _glyphCell.cx); + rect.bottom = static_cast(rect.top + _glyphCell.cy); + + // If we're double-width, make it one extra glyph wider + if (options.fIsDoubleWidth) + { + rect.right += _glyphCell.cx; + } + + CursorPaintType paintType = CursorPaintType::Fill; + + switch (options.cursorType) + { + case CursorType::Legacy: + { + // Enforce min/max cursor height + ULONG ulHeight = std::clamp(options.ulCursorHeightPercent, s_ulMinCursorHeightPercent, s_ulMaxCursorHeightPercent); + ulHeight = (ULONG)((_glyphCell.cy * ulHeight) / 100); + rect.top = rect.bottom - ulHeight; + break; + } + case CursorType::VerticalBar: + { + // It can't be wider than one cell or we'll have problems in invalidation, so restrict here. + // It's either the left + the proposed width from the ease of access setting, or + // it's the right edge of the block cursor as a maximum. + rect.right = std::min(rect.right, rect.left + options.cursorPixelWidth); + break; + } + case CursorType::Underscore: + { + rect.top = rect.bottom - 1; + break; + } + case CursorType::EmptyBox: + { + paintType = CursorPaintType::Outline; + break; + } + case CursorType::FullBox: + { + break; + } + default: + return E_NOTIMPL; + } + + Microsoft::WRL::ComPtr brush = _d2dBrushForeground; + + if (options.fUseColor) + { + // Make sure to make the cursor opaque + RETURN_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(_ColorFFromColorRef(OPACITY_OPAQUE | options.cursorColor), &brush)); + } + + switch (paintType) + { + case CursorPaintType::Fill: + { + _d2dRenderTarget->FillRectangle(rect, brush.Get()); + break; + } + case CursorPaintType::Outline: + { + _d2dRenderTarget->DrawRectangle(rect, brush.Get()); + break; + } + default: + return E_NOTIMPL; + } + + return S_OK; +} + +// Routine Description: +// - Updates the default brush colors used for drawing +// Arguments: +// - colorForeground - Foreground brush color +// - colorBackground - Background brush color +// - legacyColorAttribute - +// - isBold - +// - isSettingDefaultBrushes - Lets us know that these are the default brushes to paint the swapchain background or selection +// Return Value: +// - S_OK or relevant DirectX error. +[[nodiscard]] +HRESULT DxEngine::UpdateDrawingBrushes(COLORREF const colorForeground, + COLORREF const colorBackground, + const WORD /*legacyColorAttribute*/, + const bool /*isBold*/, + bool const isSettingDefaultBrushes) noexcept +{ + _foregroundColor = _ColorFFromColorRef(colorForeground); + _backgroundColor = _ColorFFromColorRef(colorBackground); + + _d2dBrushForeground->SetColor(_foregroundColor); + _d2dBrushBackground->SetColor(_backgroundColor); + + // If this flag is set, then we need to update the default brushes too and the swap chain background. + if (isSettingDefaultBrushes) + { + _defaultForegroundColor = _foregroundColor; + _defaultBackgroundColor = _backgroundColor; + + // If we have a swap chain, set the background color there too so the area + // outside the chain on a resize can be filled in with an appropriate color value. + /*if (_dxgiSwapChain) + { + const auto dxgiColor = s_RgbaFromColorF(_defaultBackgroundColor); + RETURN_IF_FAILED(_dxgiSwapChain->SetBackgroundColor(&dxgiColor)); + }*/ + } + + return S_OK; +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - pfiFontInfoDesired - Information specifying the font that is requested +// - fiFontInfo - Filled with the nearest font actually chosen for drawing +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] +HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept +{ + const auto hr = _GetProposedFont(pfiFontInfoDesired, + fiFontInfo, + _dpi, + _dwriteTextFormat, + _dwriteTextAnalyzer, + _dwriteFontFace); + + const auto size = fiFontInfo.GetSize(); + + _glyphCell.cx = size.X; + _glyphCell.cy = size.Y; + + return hr; +} + +[[nodiscard]] +Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept +{ + short widthInChars = static_cast(viewInPixels.Width() / _glyphCell.cx); + short heightInChars = static_cast(viewInPixels.Height() / _glyphCell.cy); + + return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars }); +} + +// Routine Description: +// - Sets the DPI in this renderer +// Arguments: +// - iDpi - DPI +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::UpdateDpi(int const iDpi) noexcept +{ + _dpi = iDpi; + + // The scale factor may be necessary for composition contexts, so save it once here. + _scale = _dpi / static_cast(USER_DEFAULT_SCREEN_DPI); + + RETURN_IF_FAILED(InvalidateAll()); + + return S_OK; +} + +// Method Description: +// - Get the current scale factor of this renderer. The actual DPI the renderer +// is USER_DEFAULT_SCREEN_DPI * GetScaling() +// Arguments: +// - +// Return Value: +// - the scaling multiplier of this render engine +float DxEngine::GetScaling() const noexcept +{ + return _scale; +} + +// Method Description: +// - This method will update our internal reference for how big the viewport is. +// Does nothing for DX. +// Arguments: +// - srNewViewport - The bounds of the new viewport. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT DxEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept +{ + return S_OK; +} + +// Routine Description: +// - Currently unused by this renderer +// Arguments: +// - pfiFontInfoDesired - +// - pfiFontInfo - +// - iDpi - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::GetProposedFont(const FontInfoDesired& pfiFontInfoDesired, + FontInfo& pfiFontInfo, + int const iDpi) noexcept +{ + Microsoft::WRL::ComPtr format; + Microsoft::WRL::ComPtr analyzer; + Microsoft::WRL::ComPtr face; + + return _GetProposedFont(pfiFontInfoDesired, + pfiFontInfo, + iDpi, + format, + analyzer, + face); +} + +// Routine Description: +// - Gets the area that we currently believe is dirty within the character cell grid +// Arguments: +// - +// Return Value: +// - Rectangle describing dirty area in characters. +[[nodiscard]] +SMALL_RECT DxEngine::GetDirtyRectInChars() noexcept +{ + SMALL_RECT r; + r.Top = (SHORT)(floor(_invalidRect.top / _glyphCell.cy)); + r.Left = (SHORT)(floor(_invalidRect.left / _glyphCell.cx)); + r.Bottom = (SHORT)(floor(_invalidRect.bottom / _glyphCell.cy)); + r.Right = (SHORT)(floor(_invalidRect.right / _glyphCell.cx)); + + // Exclusive to inclusive + r.Bottom--; + r.Right--; + + return r; +} + +// Routine Description: +// - Gets COORD packed with shorts of each glyph (character) cell's +// height and width. +// Arguments: +// - +// Return Value: +// - Nearest integer short x and y values for each cell. +[[nodiscard]] +COORD DxEngine::_GetFontSize() const noexcept +{ + return { (SHORT)(_glyphCell.cx), (SHORT)(_glyphCell.cy) }; +} + +// Routine Description: +// - Gets the current font size +// Arguments: +// - pFontSize - Filled with the font size. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +{ + *pFontSize = _GetFontSize(); + return S_OK; +} + +// Routine Description: +// - Currently unused by this renderer. +// Arguments: +// - glyph - +// - pResult - Filled with false. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::IsGlyphWideByFont(const std::wstring_view /*glyph*/, _Out_ bool* const pResult) noexcept +{ + *pResult = false; + return S_OK; +} + +// Method Description: +// - Updates the window's title string. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT DxEngine::_DoUpdateTitle(_In_ const std::wstring& /*newTitle*/) noexcept +{ + return PostMessageW(_hwndTarget, CM_UPDATE_TITLE, 0, (LPARAM)nullptr) ? S_OK : E_FAIL; +} + +// Routine Description: +// - Locates a suitable font face from the given information +// Arguments: +// - familyName - The font name we should be looking for +// - weight - The weight (bold, light, etc.) +// - stretch - The stretch of the font is the spacing between each letter +// - style - Normal, italic, etc. +// Return Value: +// - Smart pointer holding interface reference for queryable font data. +[[nodiscard]] +Microsoft::WRL::ComPtr DxEngine::_FindFontFace(const std::wstring& familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STRETCH stretch, + DWRITE_FONT_STYLE style) const +{ + Microsoft::WRL::ComPtr fontFace; + + Microsoft::WRL::ComPtr fontCollection; + THROW_IF_FAILED(_dwriteFactory->GetSystemFontCollection(&fontCollection, false)); + + UINT32 familyIndex; + BOOL familyExists; + THROW_IF_FAILED(fontCollection->FindFamilyName(familyName.c_str(), &familyIndex, &familyExists)); + + if (familyExists) + { + Microsoft::WRL::ComPtr fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(familyIndex, &fontFamily)); + + Microsoft::WRL::ComPtr font; + THROW_IF_FAILED(fontFamily->GetFirstMatchingFont(weight, stretch, style, &font)); + + Microsoft::WRL::ComPtr fontFace0; + THROW_IF_FAILED(font->CreateFontFace(&fontFace0)); + + THROW_IF_FAILED(fontFace0.As(&fontFace)); + } + + return fontFace; +} + +// Routine Description: +// - Updates the font used for drawing +// Arguments: +// - desired - Information specifying the font that is requested +// - actual - Filled with the nearest font actually chosen for drawing +// - dpi - The DPI of the screen +// Return Value: +// - S_OK or relevant DirectX error +[[nodiscard]] +HRESULT DxEngine::_GetProposedFont(const FontInfoDesired& desired, + FontInfo& actual, + const int dpi, + Microsoft::WRL::ComPtr& textFormat, + Microsoft::WRL::ComPtr& textAnalyzer, + Microsoft::WRL::ComPtr& fontFace) const noexcept +{ + try + { + const std::wstring fontName(desired.GetFaceName()); + const DWRITE_FONT_WEIGHT weight = DWRITE_FONT_WEIGHT_NORMAL; + const DWRITE_FONT_STYLE style = DWRITE_FONT_STYLE_NORMAL; + const DWRITE_FONT_STRETCH stretch = DWRITE_FONT_STRETCH_NORMAL; + + const auto face = _FindFontFace(fontName, weight, stretch, style); + THROW_IF_NULL_ALLOC_MSG(face, "Failed to find the requested font"); + + DWRITE_FONT_METRICS1 fontMetrics; + face->GetMetrics(&fontMetrics); + + const UINT32 spaceCodePoint = UNICODE_SPACE; + UINT16 spaceGlyphIndex; + THROW_IF_FAILED(face->GetGlyphIndicesW(&spaceCodePoint, 1, &spaceGlyphIndex)); + + INT32 advanceInDesignUnits; + THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits)); + + // The math here is actually: + // Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor. + // - DPI = dots per inch + // - PPI = points per inch or "points" as usually seen when choosing a font size + // - The DPI scaling factor is the current monitor DPI divided by 96, the default DPI. + // - The Points to Pixels factor is based on the typography definition of 72 points per inch. + // As such, converting requires taking the 96 pixel per inch default and dividing by the 72 points per inch + // to get a factor of 1 and 1/3. + // This turns into something like: + // - 12 ppi font * (96 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 16 pixels tall font for 100% display (96 dpi is 100%) + // - 12 ppi font * (144 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 24 pixels tall font for 150% display (144 dpi is 150%) + // - 12 ppi font * (192 dpi / 96 dpi) * (96 dpi / 72 points per inch) = 32 pixels tall font for 200% display (192 dpi is 200%) + float heightDesired = static_cast(desired.GetEngineSize().Y) * static_cast(USER_DEFAULT_SCREEN_DPI) / POINTS_PER_INCH; + + // The advance is the number of pixels left-to-right (X dimension) for the given font. + // We're finding a proportional factor here with the design units in "ems", not an actual pixel measurement. + + // For HWND swap chains, we play trickery with the font size. For others, we use inherent scaling. + // For composition swap chains, we scale by the DPI later during drawing and presentation. + if (_chainMode == SwapChainMode::ForHwnd) + { + heightDesired *= (static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI)); + } + + const float widthAdvance = static_cast(advanceInDesignUnits) / fontMetrics.designUnitsPerEm; + + // Use the real pixel height desired by the "em" factor for the width to get the number of pixels + // we will need per character in width. This will almost certainly result in fractional X-dimension pixels. + const float widthApprox = heightDesired * widthAdvance; + + // Since we can't deal with columns of the presentation grid being fractional pixels in width, round to the nearest whole pixel. + const float widthExact = round(widthApprox); + + // Now reverse the "em" factor from above to turn the exact pixel width into a (probably) fractional + // height in pixels of each character. It's easier for us to pad out height and align vertically + // than it is horizontally. + const auto fontSize = widthExact / widthAdvance; + + // Now figure out the basic properties of the character height which include ascent and descent + // for this specific font size. + const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm; + const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm; + + // We're going to build a line spacing object here to track all of this data in our format. + DWRITE_LINE_SPACING lineSpacing = {}; + lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM; + + // We need to make sure the baseline falls on a round pixel (not a fractional pixel). + // If the baseline is fractional, the text appears blurry, especially at small scales. + // Since we also need to make sure the bounding box as a whole is round pixels + // (because the entire console system maths in full cell units), + // we're just going to ceiling up the ascent and descent to make a full pixel amount + // and set the baseline to the full round pixel ascent value. + // + // For reference, for the letters "ag": + // aaaaaa ggggggg <=================================== + // a g g | | + // aaaaa ggggg |<-ascent | + // a a g | |---- height + // aaaaa a gggggg <-------------------baseline | + // g g |<-descent | + // gggggg <=================================== + // + const auto fullPixelAscent = ceil(ascent); + const auto fullPixelDescent = ceil(descent); + lineSpacing.height = fullPixelAscent + fullPixelDescent; + lineSpacing.baseline = fullPixelAscent; + + // Create the font with the fractional pixel height size. + // It should have an integer pixel width by our math above. + // Then below, apply the line spacing to the format to position the floating point pixel height characters + // into a cell that has an integer pixel height leaving some padding above/below as necessary to round them out. + Microsoft::WRL::ComPtr format; + THROW_IF_FAILED(_dwriteFactory->CreateTextFormat(fontName.data(), + nullptr, + weight, + style, + stretch, + fontSize, + L"", + &format)); + + THROW_IF_FAILED(format.As(&textFormat)); + + Microsoft::WRL::ComPtr analyzer; + THROW_IF_FAILED(_dwriteFactory->CreateTextAnalyzer(&analyzer)); + THROW_IF_FAILED(analyzer.As(&textAnalyzer)); + + fontFace = face; + + THROW_IF_FAILED(textFormat->SetLineSpacing(&lineSpacing)); + THROW_IF_FAILED(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR)); + THROW_IF_FAILED(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP)); + + // The scaled size needs to represent the pixel box that each character will fit within for the purposes + // of hit testing math and other such multiplication/division. + COORD coordSize = { 0 }; + coordSize.X = gsl::narrow(widthExact); + coordSize.Y = gsl::narrow(lineSpacing.height); + + const auto familyNameLength = textFormat->GetFontFamilyNameLength() + 1; // 1 for space for null + const auto familyNameBuffer = std::make_unique(familyNameLength); + THROW_IF_FAILED(textFormat->GetFontFamilyName(familyNameBuffer.get(), familyNameLength)); + + const DWORD weightDword = static_cast(textFormat->GetFontWeight()); + + // Unscaled is for the purposes of re-communicating this font back to the renderer again later. + // As such, we need to give the same original size parameter back here without padding + // or rounding or scaling manipulation. + COORD unscaled = desired.GetEngineSize(); + + COORD scaled = coordSize; + + actual.SetFromEngine(familyNameBuffer.get(), + desired.GetFamily(), + weightDword, + false, + scaled, + unscaled); + + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Helps convert a GDI COLORREF into a Direct2D ColorF +// Arguments: +// - color - GDI color +// Return Value: +// - D2D color +[[nodiscard]] +D2D1_COLOR_F DxEngine::_ColorFFromColorRef(const COLORREF color) noexcept +{ + // Converts BGR color order to RGB. + const UINT32 rgb = ((color & 0x0000FF) << 16) | (color & 0x00FF00) | ((color & 0xFF0000) >> 16); + + switch (_chainMode) + { + case SwapChainMode::ForHwnd: + { + return D2D1::ColorF(rgb); + } + case SwapChainMode::ForComposition: + { + // Get the A value we've snuck into the highest byte + const BYTE a = ((color >> 24) & 0xFF); + const float aFloat = a / 255.0f; + + return D2D1::ColorF(rgb, aFloat); + } + default: + THROW_HR(E_NOTIMPL); + } +} + +// Routine Description: +// - Helps convert a Direct2D ColorF into a DXGI RGBA +// Arguments: +// - color - Direct2D Color F +// Return Value: +// - DXGI RGBA +[[nodiscard]] +DXGI_RGBA DxEngine::s_RgbaFromColorF(const D2D1_COLOR_F color) noexcept +{ + DXGI_RGBA rgba; + rgba.a = color.a; + rgba.b = color.b; + rgba.g = color.g; + rgba.r = color.r; + return rgba; +} diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp new file mode 100644 index 000000000..08e0da31d --- /dev/null +++ b/src/renderer/dx/DxRenderer.hpp @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "../../renderer/inc/RenderEngineBase.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "CustomTextRenderer.h" + +#include "../../types/inc/Viewport.hpp" + +namespace Microsoft::Console::Render +{ + class DxEngine final : public RenderEngineBase + { + public: + DxEngine(); + virtual ~DxEngine() override; + + // Used to release device resources so that another instance of + // conhost can render to the screen (i.e. only one DirectX + // application may control the screen at a time.) + [[nodiscard]] + HRESULT Enable() noexcept; + [[nodiscard]] + HRESULT Disable() noexcept; + + [[nodiscard]] + HRESULT SetHwnd(const HWND hwnd) noexcept; + + [[nodiscard]] + HRESULT SetWindowSize(const SIZE pixels) noexcept; + + void SetCallback(std::function pfn); + + ::Microsoft::WRL::ComPtr GetSwapChain() noexcept; + + // IRenderEngine Members + [[nodiscard]] + HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; + [[nodiscard]] + HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override; + [[nodiscard]] + HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; + [[nodiscard]] + HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] + HRESULT InvalidateAll() noexcept override; + [[nodiscard]] + HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] + HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; + + [[nodiscard]] + HRESULT StartPaint() noexcept override; + [[nodiscard]] + HRESULT EndPaint() noexcept override; + [[nodiscard]] + HRESULT Present() noexcept override; + + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT PaintBackground() noexcept override; + [[nodiscard]] + HRESULT PaintBufferLine(std::basic_string_view const clusters, + COORD const coord, + bool const fTrimLeft) noexcept override; + + [[nodiscard]] + HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] + HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + + [[nodiscard]] + HRESULT PaintCursor(const CursorOptions& options) noexcept override; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(COLORREF const colorForeground, + COLORREF const colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + bool const isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] + HRESULT UpdateDpi(int const iDpi) noexcept override; + [[nodiscard]] + HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + + [[nodiscard]] + HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; + + [[nodiscard]] + SMALL_RECT GetDirtyRectInChars() noexcept override; + + [[nodiscard]] + HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; + [[nodiscard]] + HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; + + [[nodiscard]] + ::Microsoft::Console::Types::Viewport GetViewportInCharacters(const ::Microsoft::Console::Types::Viewport& viewInPixels) noexcept; + + float GetScaling() const noexcept; + + protected: + [[nodiscard]] + HRESULT _DoUpdateTitle(_In_ const std::wstring& newTitle) noexcept override; + + private: + enum class SwapChainMode + { + ForHwnd, + ForComposition + }; + + SwapChainMode _chainMode; + + HWND _hwndTarget; + SIZE _sizeTarget; + int _dpi; + float _scale; + + std::function _pfn; + + bool _isEnabled; + bool _isPainting; + + SIZE _displaySizePixels; + SIZE _glyphCell; + + D2D1_COLOR_F _defaultForegroundColor; + D2D1_COLOR_F _defaultBackgroundColor; + + D2D1_COLOR_F _foregroundColor; + D2D1_COLOR_F _backgroundColor; + + [[nodiscard]] + RECT _GetDisplayRect() const noexcept; + + bool _isInvalidUsed; + RECT _invalidRect; + SIZE _invalidScroll; + + void _InvalidOr(SMALL_RECT sr) noexcept; + void _InvalidOr(RECT rc) noexcept; + + void _InvalidOffset(POINT pt) noexcept; + + bool _presentReady; + RECT _presentDirty; + RECT _presentScroll; + POINT _presentOffset; + DXGI_PRESENT_PARAMETERS _presentParams; + + static const ULONG s_ulMinCursorHeightPercent = 25; + static const ULONG s_ulMaxCursorHeightPercent = 100; + + // Device-Independent Resources + ::Microsoft::WRL::ComPtr _d2dFactory; + ::Microsoft::WRL::ComPtr _dwriteFactory; + ::Microsoft::WRL::ComPtr _dwriteTextFormat; + ::Microsoft::WRL::ComPtr _dwriteFontFace; + ::Microsoft::WRL::ComPtr _dwriteTextAnalyzer; + ::Microsoft::WRL::ComPtr _customRenderer; + + // Device-Dependent Resources + bool _haveDeviceResources; + ::Microsoft::WRL::ComPtr _d3dDevice; + ::Microsoft::WRL::ComPtr _d3dDeviceContext; + ::Microsoft::WRL::ComPtr _dxgiAdapter1; + ::Microsoft::WRL::ComPtr _dxgiFactory2; + ::Microsoft::WRL::ComPtr _dxgiOutput; + ::Microsoft::WRL::ComPtr _dxgiSurface; + ::Microsoft::WRL::ComPtr _d2dRenderTarget; + ::Microsoft::WRL::ComPtr _d2dBrushForeground; + ::Microsoft::WRL::ComPtr _d2dBrushBackground; + ::Microsoft::WRL::ComPtr _dxgiSwapChain; + + [[nodiscard]] + HRESULT _CreateDeviceResources(const bool createSwapChain) noexcept; + + [[nodiscard]] + HRESULT _PrepareRenderTarget() noexcept; + + void _ReleaseDeviceResources() noexcept; + + [[nodiscard]] + HRESULT _CreateTextLayout( + _In_reads_(StringLength) PCWCHAR String, + _In_ size_t StringLength, + _Out_ IDWriteTextLayout **ppTextLayout) noexcept; + + [[nodiscard]] + HRESULT _CopyFrontToBack() noexcept; + + [[nodiscard]] + HRESULT _EnableDisplayAccess(const bool outputEnabled) noexcept; + + [[nodiscard]] + ::Microsoft::WRL::ComPtr _FindFontFace(const std::wstring& familyName, + DWRITE_FONT_WEIGHT weight, + DWRITE_FONT_STRETCH stretch, + DWRITE_FONT_STYLE style) const; + + [[nodiscard]] + HRESULT _GetProposedFont(const FontInfoDesired& desired, + FontInfo& actual, + const int dpi, + ::Microsoft::WRL::ComPtr& textFormat, + ::Microsoft::WRL::ComPtr& textAnalyzer, + ::Microsoft::WRL::ComPtr& fontFace) const noexcept; + + [[nodiscard]] + COORD _GetFontSize() const noexcept; + + [[nodiscard]] + SIZE _GetClientSize() const noexcept; + + [[nodiscard]] + D2D1_COLOR_F _ColorFFromColorRef(const COLORREF color) noexcept; + + [[nodiscard]] + static DXGI_RGBA s_RgbaFromColorF(const D2D1_COLOR_F color) noexcept; + }; +} diff --git a/src/renderer/dx/dirs b/src/renderer/dx/dirs new file mode 100644 index 000000000..46c4fdd5a --- /dev/null +++ b/src/renderer/dx/dirs @@ -0,0 +1,2 @@ +DIRS= \ + lib diff --git a/src/renderer/dx/lib/dx.vcxproj b/src/renderer/dx/lib/dx.vcxproj new file mode 100644 index 000000000..df4208367 --- /dev/null +++ b/src/renderer/dx/lib/dx.vcxproj @@ -0,0 +1,28 @@ + + + + + + + + Create + + + + + + + + + + + {48D21369-3D7B-4431-9967-24E81292CF62} + Win32Proj + dx + RendererDx + ConRenderDx + + + + + \ No newline at end of file diff --git a/src/renderer/dx/lib/sources b/src/renderer/dx/lib/sources new file mode 100644 index 000000000..7b2df6658 --- /dev/null +++ b/src/renderer/dx/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderDx +TARGETTYPE = LIBRARY diff --git a/src/renderer/dx/precomp.cpp b/src/renderer/dx/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/renderer/dx/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/renderer/dx/precomp.h b/src/renderer/dx/precomp.h new file mode 100644 index 000000000..858413f4b --- /dev/null +++ b/src/renderer/dx/precomp.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include + +#include "..\host\conddkrefs.h" +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma hdrstop diff --git a/src/renderer/dx/sources.inc b/src/renderer/dx/sources.inc new file mode 100644 index 000000000..c44e752dd --- /dev/null +++ b/src/renderer/dx/sources.inc @@ -0,0 +1,36 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Renderer for DirectX +# ------------------------------------- + +# This module provides a rendering engine implementation that +# draws to a DirectX surface. + +# ------------------------------------- +# CRT Configuration +# ------------------------------------- + +BUILD_FOR_CORESYSTEM = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\..\inc; \ + ..\..\..\inc; \ + ..\..\..\host; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + +SOURCES = \ + $(SOURCES) \ + ..\DxRenderer.cpp \ + ..\CustomTextRenderer.cpp \ + ..\CustomTextLayout.cpp \ diff --git a/src/renderer/gdi/dirs b/src/renderer/gdi/dirs new file mode 100644 index 000000000..3cff781c8 --- /dev/null +++ b/src/renderer/gdi/dirs @@ -0,0 +1,2 @@ +DIRS= \ + lib \ diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp new file mode 100644 index 000000000..330c732f2 --- /dev/null +++ b/src/renderer/gdi/gdirenderer.hpp @@ -0,0 +1,203 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- GdiRenderer.hpp + +Abstract: +- This is the definition of the GDI specific implementation of the renderer. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "..\inc\RenderEngineBase.hpp" + +namespace Microsoft::Console::Render +{ + class GdiEngine final : public RenderEngineBase + { + public: + GdiEngine(); + ~GdiEngine() override; + + [[nodiscard]] + HRESULT SetHwnd(const HWND hwnd) noexcept; + + [[nodiscard]] + HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] + HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; + [[nodiscard]] + HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; + [[nodiscard]] + HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override; + [[nodiscard]] + HRESULT InvalidateAll() noexcept override; + [[nodiscard]] + HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] + HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; + + [[nodiscard]] + HRESULT StartPaint() noexcept override; + [[nodiscard]] + HRESULT EndPaint() noexcept override; + [[nodiscard]] + HRESULT Present() noexcept override; + + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT PaintBackground() noexcept override; + [[nodiscard]] + HRESULT PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool trimLeft) noexcept override; + [[nodiscard]] + HRESULT PaintBufferGridLines(const GridLines lines, + const COLORREF color, + const size_t cchLine, + const COORD coordTarget) noexcept override; + [[nodiscard]] + HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + + [[nodiscard]] + HRESULT PaintCursor(const CursorOptions& options) noexcept override; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) noexcept override; + [[nodiscard]] + HRESULT UpdateDpi(const int iDpi) noexcept override; + [[nodiscard]] + HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + + [[nodiscard]] + HRESULT GetProposedFont(const FontInfoDesired& FontDesired, + _Out_ FontInfo& Font, + const int iDpi) noexcept override; + + SMALL_RECT GetDirtyRectInChars() override; + [[nodiscard]] + HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; + [[nodiscard]] + HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; + + protected: + [[nodiscard]] + HRESULT _DoUpdateTitle(_In_ const std::wstring& newTitle) noexcept override; + + private: + HWND _hwndTargetWindow; + + [[nodiscard]] + static HRESULT s_SetWindowLongWHelper(const HWND hWnd, + const int nIndex, + const LONG dwNewLong) noexcept; + + bool _fPaintStarted; + + PAINTSTRUCT _psInvalidData; + HDC _hdcMemoryContext; + bool _isTrueTypeFont; + UINT _fontCodepage; + HFONT _hfont; + TEXTMETRICW _tmFontMetrics; + + static const size_t s_cPolyTextCache = 80; + POLYTEXTW _pPolyText[s_cPolyTextCache]; + size_t _cPolyText; + [[nodiscard]] + HRESULT _FlushBufferLines() noexcept; + + std::vector cursorInvertRects; + + COORD _coordFontLast; + int _iCurrentDpi; + + static const int s_iBaseDpi = USER_DEFAULT_SCREEN_DPI; + + SIZE _szMemorySurface; + HBITMAP _hbitmapMemorySurface; + [[nodiscard]] + HRESULT _PrepareMemoryBitmap(const HWND hwnd) noexcept; + + SIZE _szInvalidScroll; + RECT _rcInvalid; + bool _fInvalidRectUsed; + + COLORREF _lastFg; + COLORREF _lastBg; + + [[nodiscard]] + HRESULT _InvalidCombine(const RECT* const prc) noexcept; + [[nodiscard]] + HRESULT _InvalidOffset(const POINT* const ppt) noexcept; + [[nodiscard]] + HRESULT _InvalidRestrict() noexcept; + + [[nodiscard]] + HRESULT _InvalidateRect(const RECT* const prc) noexcept; + + [[nodiscard]] + HRESULT _PaintBackgroundColor(const RECT* const prc) noexcept; + + static const ULONG s_ulMinCursorHeightPercent = 25; + static const ULONG s_ulMaxCursorHeightPercent = 100; + + [[nodiscard]] + HRESULT _ScaleByFont(const COORD* const pcoord, _Out_ POINT* const pPoint) const noexcept; + [[nodiscard]] + HRESULT _ScaleByFont(const SMALL_RECT* const psr, _Out_ RECT* const prc) const noexcept; + [[nodiscard]] + HRESULT _ScaleByFont(const RECT* const prc, _Out_ SMALL_RECT* const psr) const noexcept; + + static int s_ScaleByDpi(const int iPx, const int iDpi); + static int s_ShrinkByDpi(const int iPx, const int iDpi); + + POINT _GetInvalidRectPoint() const; + SIZE _GetInvalidRectSize() const; + SIZE _GetRectSize(const RECT* const pRect) const; + + void _OrRect(_In_ RECT* const pRectExisting, const RECT* const pRectToOr) const; + + bool _IsFontTrueType() const; + + [[nodiscard]] + HRESULT _GetProposedFont(const FontInfoDesired& FontDesired, + _Out_ FontInfo& Font, + const int iDpi, + _Inout_ wil::unique_hfont& hFont) noexcept; + + COORD _GetFontSize() const; + bool _IsMinimized() const; + bool _IsWindowValid() const; + +#ifdef DBG + // Helper functions to diagnose issues with painting from the in-memory buffer. + // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. + bool _fDebug = false; + void _PaintDebugRect(const RECT* const prc) const; + void _DoDebugBlt(const RECT* const prc) const; + + void _DebugBltAll() const; + + HWND _debugWindow; + void _CreateDebugWindow(); + HDC _debugContext; +#endif + }; +} diff --git a/src/renderer/gdi/invalidate.cpp b/src/renderer/gdi/invalidate.cpp new file mode 100644 index 000000000..5ef3d7ab9 --- /dev/null +++ b/src/renderer/gdi/invalidate.cpp @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "gdirenderer.hpp" +#include "../../types/inc/Viewport.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Notifies us that the system has requested a particular pixel area of the client rectangle should be redrawn. (On WM_PAINT) +// Arguments: +// - prcDirtyClient - Pointer to pixel area (RECT) of client region the system believes is dirty +// Return Value: +// - HRESULT S_OK, GDI-based error code, or safemath error +HRESULT GdiEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept +{ + RETURN_HR(_InvalidCombine(prcDirtyClient)); +} + +// Routine Description: +// - Notifies us that the console is attempting to scroll the existing screen area +// Arguments: +// - pcoordDelta - Pointer to character dimension (COORD) of the distance the console would like us to move while scrolling. +// Return Value: +// - HRESULT S_OK, GDI-based error code, or safemath error +HRESULT GdiEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +{ + if (pcoordDelta->X != 0 || pcoordDelta->Y != 0) + { + POINT ptDelta = { 0 }; + RETURN_IF_FAILED(_ScaleByFont(pcoordDelta, &ptDelta)); + + RETURN_IF_FAILED(_InvalidOffset(&ptDelta)); + + SIZE szInvalidScrollNew; + RETURN_IF_FAILED(LongAdd(_szInvalidScroll.cx, ptDelta.x, &szInvalidScrollNew.cx)); + RETURN_IF_FAILED(LongAdd(_szInvalidScroll.cy, ptDelta.y, &szInvalidScrollNew.cy)); + + // Store if safemath succeeded + _szInvalidScroll = szInvalidScrollNew; + } + + return S_OK; +} + +// Routine Description: +// - Notifies us that the console has changed the selection region and would like it updated +// Arguments: +// - rectangles - Vector of rectangles to draw, line by line +// Return Value: +// - HRESULT S_OK or GDI-based error code +HRESULT GdiEngine::InvalidateSelection(const std::vector& rectangles) noexcept +{ + for (const auto& rect : rectangles) + { + RETURN_IF_FAILED(Invalidate(&rect)); + } + + return S_OK; +} + +// Routine Description: +// - Notifies us that the console has changed the character region specified. +// - NOTE: This typically triggers on cursor or text buffer changes +// Arguments: +// - psrRegion - Character region (SMALL_RECT) that has been changed +// Return Value: +// - S_OK, GDI related failure, or safemath failure. +HRESULT GdiEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept +{ + RECT rcRegion = { 0 }; + RETURN_IF_FAILED(_ScaleByFont(psrRegion, &rcRegion)); + RETURN_HR(_InvalidateRect(&rcRegion)); +} + +// Routine Description: +// - Notifies us that the console has changed the position of the cursor. +// Arguments: +// - pcoordCursor - the new position of the cursor +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +HRESULT GdiEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +{ + SMALL_RECT sr = Viewport::FromCoord(*pcoordCursor).ToExclusive(); + return this->Invalidate(&sr); +} + +// Routine Description: +// - Notifies to repaint everything. +// - NOTE: Use sparingly. Only use when something that could affect the entire frame simultaneously occurs. +// Arguments: +// - +// Return Value: +// - S_OK, S_FALSE (if no window yet), GDI related failure, or safemath failure. +HRESULT GdiEngine::InvalidateAll() noexcept +{ + // If we don't have a window, don't bother. + if (!_IsWindowValid()) + { + return S_FALSE; + } + + RECT rc; + RETURN_HR_IF(E_FAIL, !(GetClientRect(_hwndTargetWindow, &rc))); + RETURN_HR(InvalidateSystem(&rc)); +} + +// Method Description: +// - Notifies us that we're about to circle the buffer, giving us a chance to +// force a repaint before the buffer contents are lost. The GDI renderer +// doesn't care if we lose text - we're only painting visible text anyways, +// so we return false. +// Arguments: +// - Recieves a bool indicating if we should force the repaint. +// Return Value: +// - S_FALSE - we succeeded, but the result was false. +HRESULT GdiEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +// Method Description: +// - Notifies us that we're about to be torn down. This gives us a last chance +// to force a repaint before the buffer contents are lost. The GDI renderer +// doesn't care if we lose text - we're only painting visible text anyways, +// so we return false. +// Arguments: +// - Recieves a bool indicating if we should force the repaint. +// Return Value: +// - S_FALSE - we succeeded, but the result was false. +HRESULT GdiEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +// Routine Description: +// - Helper to combine the given rectangle into the invalid region to be updated on the next paint +// Arguments: +// - prc - Pixel region (RECT) that should be repainted on the next frame +// Return Value: +// - S_OK, GDI related failure, or safemath failure. +HRESULT GdiEngine::_InvalidCombine(const RECT* const prc) noexcept +{ + if (!_fInvalidRectUsed) + { + _rcInvalid = *prc; + _fInvalidRectUsed = true; + } + else + { + _OrRect(&_rcInvalid, prc); + } + + // Ensure invalid areas remain within bounds of window. + RETURN_IF_FAILED(_InvalidRestrict()); + + return S_OK; +} + +// Routine Description: +// - Helper to adjust the invalid region by the given offset such as when a scroll operation occurs. +// Arguments: +// - ppt - Distances by which we should move the invalid region in response to a scroll +// Return Value: +// - S_OK, GDI related failure, or safemath failure. +HRESULT GdiEngine::_InvalidOffset(const POINT* const ppt) noexcept +{ + if (_fInvalidRectUsed) + { + RECT rcInvalidNew; + + RETURN_IF_FAILED(LongAdd(_rcInvalid.left, ppt->x, &rcInvalidNew.left)); + RETURN_IF_FAILED(LongAdd(_rcInvalid.right, ppt->x, &rcInvalidNew.right)); + RETURN_IF_FAILED(LongAdd(_rcInvalid.top, ppt->y, &rcInvalidNew.top)); + RETURN_IF_FAILED(LongAdd(_rcInvalid.bottom, ppt->y, &rcInvalidNew.bottom)); + + // Add the scrolled invalid rectangle to what was left behind to get the new invalid area. + // This is the equivalent of adding in the "update rectangle" that we would get out of ScrollWindowEx/ScrollDC. + UnionRect(&_rcInvalid, &_rcInvalid, &rcInvalidNew); + + // Ensure invalid areas remain within bounds of window. + RETURN_IF_FAILED(_InvalidRestrict()); + } + + return S_OK; +} + +// Routine Description: +// - Helper to ensure the invalid region remains within the bounds of the window. +// Arguments: +// - +// Return Value: +// - S_OK, GDI related failure, or safemath failure. +HRESULT GdiEngine::_InvalidRestrict() noexcept +{ + // Ensure that the invalid area remains within the bounds of the client area + RECT rcClient; + + // Do restriction only if retrieving the client rect was successful. + RETURN_HR_IF(E_FAIL, !(GetClientRect(_hwndTargetWindow, &rcClient))); + + _rcInvalid.left = std::clamp(_rcInvalid.left, rcClient.left, rcClient.right); + _rcInvalid.right = std::clamp(_rcInvalid.right, rcClient.left, rcClient.right); + _rcInvalid.top = std::clamp(_rcInvalid.top, rcClient.top, rcClient.bottom); + _rcInvalid.bottom = std::clamp(_rcInvalid.bottom, rcClient.top, rcClient.bottom); + + return S_OK; +} + +// Routine Description: +// - Helper to add a pixel rectangle to the invalid area +// Arguments: +// - prc - Pointer to pixel rectangle representing invalid area to add to next paint frame +// Return Value: +// - S_OK, GDI related failure, or safemath failure. +HRESULT GdiEngine::_InvalidateRect(const RECT* const prc) noexcept +{ + RETURN_HR(_InvalidCombine(prc)); +} diff --git a/src/renderer/gdi/lib/gdi.vcxproj b/src/renderer/gdi/lib/gdi.vcxproj new file mode 100644 index 000000000..d64e4ad5f --- /dev/null +++ b/src/renderer/gdi/lib/gdi.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + + + Create + + + + + + + + {1C959542-BAC2-4E55-9A6D-13251914CBB9} + Win32Proj + gdi + RendererGdi + ConRenderGdi + + + + + \ No newline at end of file diff --git a/src/renderer/gdi/lib/gdi.vcxproj.filters b/src/renderer/gdi/lib/gdi.vcxproj.filters new file mode 100644 index 000000000..e65ed3729 --- /dev/null +++ b/src/renderer/gdi/lib/gdi.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/renderer/gdi/lib/sources b/src/renderer/gdi/lib/sources new file mode 100644 index 000000000..0a58ccf3a --- /dev/null +++ b/src/renderer/gdi/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderGdi +TARGETTYPE = LIBRARY diff --git a/src/renderer/gdi/math.cpp b/src/renderer/gdi/math.cpp new file mode 100644 index 000000000..522c251af --- /dev/null +++ b/src/renderer/gdi/math.cpp @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" + +#include "gdirenderer.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Gets the size in characters of the current dirty portion of the frame. +// Arguments: +// - +// Return Value: +// - The character dimensions of the current dirty area of the frame. +// This is an Inclusive rect. +SMALL_RECT GdiEngine::GetDirtyRectInChars() +{ + RECT rc = _psInvalidData.rcPaint; + + SMALL_RECT sr = { 0 }; + LOG_IF_FAILED(_ScaleByFont(&rc, &sr)); + + return sr; +} + +// Routine Description: +// - Uses the currently selected font to determine how wide the given character will be when renderered. +// - NOTE: Only supports determining half-width/full-width status for CJK-type languages (e.g. is it 1 character wide or 2. a.k.a. is it a rectangle or square.) +// Arguments: +// - glyph - utf16 encoded codepoint to check +// - pResult - recieves return value, True if it is full-width (2 wide). False if it is half-width (1 wide). +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT GdiEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept +{ + bool isFullWidth = false; + + if (glyph.size() == 1) + { + const wchar_t wch = glyph.front(); + if (_IsFontTrueType()) + { + ABC abc; + if (GetCharABCWidthsW(_hdcMemoryContext, wch, wch, &abc)) + { + int const totalWidth = abc.abcA + abc.abcB + abc.abcC; + + isFullWidth = totalWidth > _GetFontSize().X; + } + } + else + { + INT cpxWidth = 0; + if (GetCharWidth32W(_hdcMemoryContext, wch, wch, &cpxWidth)) + { + isFullWidth = cpxWidth > _GetFontSize().X; + } + } + } + else + { + // can't find a way to make gdi measure the width of utf16 surrogate pairs. + // in the meantime, better to be too wide than too narrow. + isFullWidth = true; + } + + *pResult = isFullWidth; + return S_OK; +} + +// Routine Description: +// - Scales a character region (SMALL_RECT) into a pixel region (RECT) by the current font size. +// Arguments: +// - psr = Character region (SMALL_RECT) from the console text buffer. +// - prc - Pixel region (RECT) for drawing to the client surface. +// Return Value: +// - S_OK or safe math failure value. +[[nodiscard]] +HRESULT GdiEngine::_ScaleByFont(const SMALL_RECT* const psr, _Out_ RECT* const prc) const noexcept +{ + COORD const coordFontSize = _GetFontSize(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + + RECT rc; + RETURN_IF_FAILED(LongMult(psr->Left, coordFontSize.X, &rc.left)); + RETURN_IF_FAILED(LongMult(psr->Right, coordFontSize.X, &rc.right)); + RETURN_IF_FAILED(LongMult(psr->Top, coordFontSize.Y, &rc.top)); + RETURN_IF_FAILED(LongMult(psr->Bottom, coordFontSize.Y, &rc.bottom)); + + *prc = rc; + + return S_OK; +} + +// Routine Description: +// - Scales a character coordinate (COORD) into a pixel coordinate (POINT) by the current font size. +// Arguments: +// - pcoord - Character coordinate (COORD) from the console text buffer. +// - ppt - Pixel coordinate (POINT) for drawing to the client surface. +// Return Value: +// - S_OK or safe math failure value. +[[nodiscard]] +HRESULT GdiEngine::_ScaleByFont(const COORD* const pcoord, _Out_ POINT* const pPoint) const noexcept +{ + COORD const coordFontSize = _GetFontSize(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + + POINT pt; + RETURN_IF_FAILED(LongMult(pcoord->X, coordFontSize.X, &pt.x)); + RETURN_IF_FAILED(LongMult(pcoord->Y, coordFontSize.Y, &pt.y)); + + *pPoint = pt; + + return S_OK; +} + +// Routine Description: +// - Scales a pixel region (RECT) into a character region (SMALL_RECT) by the current font size. +// Arguments: +// - prc - Pixel region (RECT) from drawing to the client surface. +// - psr - Character region (SMALL_RECT) from the console text buffer. +// Return Value: +// - S_OK or safe math failure value. +[[nodiscard]] +HRESULT GdiEngine::_ScaleByFont(const RECT* const prc, _Out_ SMALL_RECT* const psr) const noexcept +{ + COORD const coordFontSize = _GetFontSize(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + + SMALL_RECT sr; + sr.Left = static_cast(prc->left / coordFontSize.X); + sr.Top = static_cast(prc->top / coordFontSize.Y); + + // We're dividing integers so we're always going to round down to the next whole number on division. + // To make sure that when we round down, we remain an exclusive rectangle, we need to add the width (or height) - 1 before + // dividing such that a 1 px size rectangle will become a 1 ch size character. + // For example: + // L = 1px, R = 2px. Font Width = 8. What we want to see is a character rect that will only draw the 0th character (0 to 1). + // A. Simple divide + // 1px / 8px = 0ch for the Left measurement. + // 2px / 8px = 0ch for the Right which would be inclusive not exclusive. + // A Conclusion = doesn't work. + // B. Add a character + // 1px / 8px = 0ch for the Left measurement. + // (2px + 8px) / 8px = 1ch for the Right which seems alright. + // B Conclusion = plausible, but see C for why not. + // C. Add one pixel less than a full character, but this time R = 8px (which in exclusive terms still only addresses 1 character of pixels.) + // 1px / 8px = 0ch for the Left measurement. + // (8px + 8px) / 8px = 2ch for the Right measurement. Now we're redrawing 2 chars when we only needed to do one because this caused us to effectively round up. + // C Conclusion = this works because our addition can never completely push us over to adding an additional ch to the rectangle. + // So the algorithm below is using the C conclusion's math. + + + // Do math as long and fit to short at the end. + LONG lRight = prc->right; + LONG lBottom = prc->bottom; + + // Add the width of a font (in pixels) to the rect + RETURN_IF_FAILED(LongAdd(lRight, coordFontSize.X, &lRight)); + RETURN_IF_FAILED(LongAdd(lBottom, coordFontSize.Y, &lBottom)); + + // Subtract 1 to ensure that we round down. + RETURN_IF_FAILED(LongSub(lRight, 1, &lRight)); + RETURN_IF_FAILED(LongSub(lBottom, 1, &lBottom)); + + // Divide by font size to see how many rows/columns + // note: no safe math for div. + lRight /= coordFontSize.X; + lBottom /= coordFontSize.Y; + + // Attempt to fit into SMALL_RECT's short variable. + RETURN_IF_FAILED(LongToShort(lRight, &sr.Right)); + RETURN_IF_FAILED(LongToShort(lBottom, &sr.Bottom)); + + // Pixels are exclusive and character rects are inclusive. Subtract 1 to go from exclusive to inclusive rect. + RETURN_IF_FAILED(ShortSub(sr.Right, 1, &sr.Right)); + RETURN_IF_FAILED(ShortSub(sr.Bottom, 1, &sr.Bottom)); + + *psr = sr; + + return S_OK; +} + +// Routine Description: +// - Scales the given pixel measurement up from the typical system DPI (generally 96) to whatever the given DPI is. +// Arguments: +// - iPx - Pixel length measurement. +// - iDpi - Given DPI scalar value +// Return Value: +// - Pixel measurement scaled against the given DPI scalar. +int GdiEngine::s_ScaleByDpi(const int iPx, const int iDpi) +{ + return MulDiv(iPx, iDpi, s_iBaseDpi); +} + +// Routine Description: +// - Shrinks the given pixel measurement down from whatever the given DPI is to the typical system DPI (generally 96). +// Arguments: +// - iPx - Pixel measurement scaled against the given DPI. +// - iDpi - Given DPI for pixel scaling +// Return Value: +// - Pixel length measurement. +int GdiEngine::s_ShrinkByDpi(const int iPx, const int iDpi) +{ + return MulDiv(iPx, s_iBaseDpi, iDpi); +} + +// Routine Description: +// - Uses internal invalid structure to determine the top left pixel point of the invalid frame to be painted. +// Arguments: +// - +// Return Value: +// - Top left corner in pixels of where to start repainting the frame. +POINT GdiEngine::_GetInvalidRectPoint() const +{ + POINT pt; + pt.x = _psInvalidData.rcPaint.left; + pt.y = _psInvalidData.rcPaint.top; + + return pt; +} + +// Routine Description: +// - Uses internal invalid structure to determine the size of the invalid area of the frame to be painted. +// Arguments: +// - +// Return Value: +// - Width and height in pixels of the invalid area of the frame. +SIZE GdiEngine::_GetInvalidRectSize() const +{ + return _GetRectSize(&_psInvalidData.rcPaint); +} + +// Routine Description: +// - Converts a pixel region (RECT) into its width/height (SIZE) +// Arguments: +// - Pixel region (RECT) +// Return Value: +// - Pixel dimensions (SIZE) +SIZE GdiEngine::_GetRectSize(const RECT* const pRect) const +{ + SIZE sz; + sz.cx = pRect->right - pRect->left; + sz.cy = pRect->bottom - pRect->top; + + return sz; +} + +// Routine Description: +// - Performs a "CombineRect" with the "OR" operation. +// - Basically extends the existing rect outward to also encompass the passed-in region. +// Arguments: +// - pRectExisting - Expand this rectangle to encompass the add rect. +// - pRectToOr - Add this rectangle to the existing one. +// Return Value: +// - +void GdiEngine::_OrRect(_In_ RECT* const pRectExisting, const RECT* const pRectToOr) const +{ + pRectExisting->left = std::min(pRectExisting->left, pRectToOr->left); + pRectExisting->top = std::min(pRectExisting->top, pRectToOr->top); + pRectExisting->right = std::max(pRectExisting->right, pRectToOr->right); + pRectExisting->bottom = std::max(pRectExisting->bottom, pRectToOr->bottom); +} diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp new file mode 100644 index 000000000..5f01c9d66 --- /dev/null +++ b/src/renderer/gdi/paint.cpp @@ -0,0 +1,749 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "precomp.h" +#include +#include "gdirenderer.hpp" + +#include "../inc/unicode.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Prepares internal structures for a painting operation. +// Arguments: +// - +// Return Value: +// - S_OK if we started to paint. S_FALSE if we didn't need to paint. HRESULT error code if painting didn't start successfully. +[[nodiscard]] +HRESULT GdiEngine::StartPaint() noexcept +{ + // If we have no handle, we don't need to paint. Return quickly. + RETURN_HR_IF(S_FALSE, INVALID_HANDLE_VALUE == _hwndTargetWindow); + + // If we're already painting, we don't need to paint. Return quickly. + RETURN_HR_IF(S_FALSE, _fPaintStarted); + + // If the window we're painting on is invisible, we don't need to paint. Return quickly. + // If the title changed, we will need to try and paint this frame. This will + // make sure the window's title is updated, even if the window isn't visible. + RETURN_HR_IF(S_FALSE, (!IsWindowVisible(_hwndTargetWindow) && !_titleChanged)); + + // At the beginning of a new frame, we have 0 lines ready for painting in PolyTextOut + _cPolyText = 0; + + // Prepare our in-memory bitmap for double-buffered composition. + RETURN_IF_FAILED(_PrepareMemoryBitmap(_hwndTargetWindow)); + + // We must use Get and Release DC because BeginPaint/EndPaint can only be called in response to a WM_PAINT message (and may hang otherwise) + // We'll still use the PAINTSTRUCT for information because it's convenient. + _psInvalidData.hdc = GetDC(_hwndTargetWindow); + RETURN_HR_IF_NULL(E_FAIL, _psInvalidData.hdc); + + // Signal that we're starting to paint. + _fPaintStarted = true; + + _psInvalidData.fErase = TRUE; + _psInvalidData.rcPaint = _rcInvalid; + +#if DBG + _debugContext = GetDC(_debugWindow); +#endif + + return S_OK; +} + +// Routine Description: +// - Scrolls the existing data on the in-memory frame by the scroll region +// deltas we have collectively received through the Invalidate methods +// since the last time this was called. +// Arguments: +// - +// Return Value: +// - S_OK, suitable GDI HRESULT error, error from Win32 windowing, or safemath error. +[[nodiscard]] +HRESULT GdiEngine::ScrollFrame() noexcept +{ + // If we don't have any scrolling to do, return early. + RETURN_HR_IF(S_OK, 0 == _szInvalidScroll.cx && 0 == _szInvalidScroll.cy); + + // If we have an inverted cursor, we have to see if we have to clean it before we scroll to prevent + // left behind cursor copies in the scrolled region. + if (cursorInvertRects.size() > 0) + { + for (RECT r : cursorInvertRects) + { + // Clean both the in-memory and actual window context. + RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r))); + RETURN_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r))); + } + + cursorInvertRects.clear(); + } + + // We have to limit the region that can be scrolled to not include the gutters. + // Gutters are defined as sub-character width pixels at the bottom or right of the screen. + COORD const coordFontSize = _GetFontSize(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + + SIZE szGutter; + szGutter.cx = _szMemorySurface.cx % coordFontSize.X; + szGutter.cy = _szMemorySurface.cy % coordFontSize.Y; + + RECT rcScrollLimit = { 0 }; + RETURN_IF_FAILED(LongSub(_szMemorySurface.cx, szGutter.cx, &rcScrollLimit.right)); + RETURN_IF_FAILED(LongSub(_szMemorySurface.cy, szGutter.cy, &rcScrollLimit.bottom)); + + // Scroll real window and memory buffer in-sync. + LOG_LAST_ERROR_IF(!ScrollWindowEx(_hwndTargetWindow, + _szInvalidScroll.cx, + _szInvalidScroll.cy, + &rcScrollLimit, + &rcScrollLimit, + nullptr, + nullptr, + 0)); + + RECT rcUpdate = { 0 }; + LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.cx, _szInvalidScroll.cy, &rcScrollLimit, &rcScrollLimit, nullptr, &rcUpdate))); + + LOG_IF_FAILED(_InvalidCombine(&rcUpdate)); + + // update invalid rect for the remainder of paint functions + _psInvalidData.rcPaint = _rcInvalid; + + return S_OK; +} + +// Routine Description: +// - BeginPaint helper to prepare the in-memory bitmap for double-buffering +// Arguments: +// - hwnd - Window handle to use for the DC properties when creating a memory DC and for checking the client area size. +// Return Value: +// - S_OK or suitable GDI HRESULT error. +[[nodiscard]] +HRESULT GdiEngine::_PrepareMemoryBitmap(const HWND hwnd) noexcept +{ + RECT rcClient; + RETURN_HR_IF(E_FAIL, !(GetClientRect(hwnd, &rcClient))); + + SIZE const szClient = _GetRectSize(&rcClient); + + // Only do work if the existing memory surface is a different size from the client area. + // Return quickly if they're the same. + RETURN_HR_IF(S_OK, _szMemorySurface.cx == szClient.cx && _szMemorySurface.cy == szClient.cy); + + wil::unique_hdc hdcRealWindow(GetDC(_hwndTargetWindow)); + RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow.get()); + + // If we already had a bitmap, Blt the old one onto the new one and clean up the old one. + if (nullptr != _hbitmapMemorySurface) + { + // Make a temporary DC for us to Blt with. + wil::unique_hdc hdcTemp(CreateCompatibleDC(hdcRealWindow.get())); + RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get()); + + // Make the new bitmap we'll use going forward with the new size. + wil::unique_hbitmap hbitmapNew(CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy)); + RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get()); + + // Select it into the DC, but hold onto the junky one pixel bitmap (made by default) to give back when we need to Delete. + wil::unique_hbitmap hbitmapOnePixelJunk(SelectBitmap(hdcTemp.get(), hbitmapNew.get())); + RETURN_HR_IF_NULL(E_FAIL, hbitmapOnePixelJunk.get()); + hbitmapNew.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object. + + // Blt from the DC/bitmap we're already holding onto into the new one. + RETURN_HR_IF(E_FAIL, !(BitBlt(hdcTemp.get(), 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY))); + + // Put the junky bitmap back into the temp DC and get our new one out. + hbitmapNew.reset(SelectBitmap(hdcTemp.get(), hbitmapOnePixelJunk.get())); + RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get()); + hbitmapOnePixelJunk.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object. + + // Move our new bitmap into the long-standing DC we're holding onto. + wil::unique_hbitmap hbitmapOld(SelectBitmap(_hdcMemoryContext, hbitmapNew.get())); + RETURN_HR_IF_NULL(E_FAIL, hbitmapOld.get()); + + // Now save a pointer to our new bitmap into the class state. + _hbitmapMemorySurface = hbitmapNew.release(); // and prevent it from being freed now that GDI is holding onto it as well. + } + else + { + _hbitmapMemorySurface = CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy); + RETURN_HR_IF_NULL(E_FAIL, _hbitmapMemorySurface); + + wil::unique_hbitmap hOldBitmap(SelectBitmap(_hdcMemoryContext, _hbitmapMemorySurface)); // DC has a default junk bitmap, take it and delete it. + RETURN_HR_IF_NULL(E_FAIL, hOldBitmap.get()); + } + + // Save the new client size. + _szMemorySurface = szClient; + + return S_OK; +} + +// Routine Description: +// - EndPaint helper to perform the final BitBlt copy from the memory bitmap onto the final window bitmap (double-buffering.) Also cleans up structures used while painting. +// Arguments: +// - +// Return Value: +// - S_OK or suitable GDI HRESULT error. +[[nodiscard]] +HRESULT GdiEngine::EndPaint() noexcept +{ + // If we try to end a paint that wasn't started, it's invalid. Return. + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !(_fPaintStarted)); + + LOG_IF_FAILED(_FlushBufferLines()); + + POINT const pt = _GetInvalidRectPoint(); + SIZE const sz = _GetInvalidRectSize(); + + LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.cx, sz.cy, _hdcMemoryContext, pt.x, pt.y, SRCCOPY))); + WHEN_DBG(_DebugBltAll()); + + _rcInvalid = { 0 }; + _fInvalidRectUsed = false; + _szInvalidScroll = { 0 }; + + LOG_HR_IF(E_FAIL, !(GdiFlush())); + LOG_HR_IF(E_FAIL, !(ReleaseDC(_hwndTargetWindow, _psInvalidData.hdc))); + _psInvalidData.hdc = nullptr; + + _fPaintStarted = false; + +#if DBG + ReleaseDC(_debugWindow, _debugContext); + _debugContext = nullptr; +#endif + + return S_OK; +} + +// Routine Description: +// - Used to perform longer running presentation steps outside the lock so the other threads can continue. +// - Not currently used by GdiEngine. +// Arguments: +// - +// Return Value: +// - S_FALSE since we do nothing. +[[nodiscard]] +HRESULT GdiEngine::Present() noexcept +{ + return S_FALSE; +} + +// Routine Description: +// - Fills the given rectangle with the background color on the drawing context. +// Arguments: +// - prc - Rectangle to fill with color +// Return Value: +// - S_OK or suitable GDI HRESULT error. +[[nodiscard]] +HRESULT GdiEngine::_PaintBackgroundColor(const RECT* const prc) noexcept +{ + wil::unique_hbrush hbr(GetStockBrush(DC_BRUSH)); + RETURN_HR_IF_NULL(E_FAIL, hbr.get()); + + WHEN_DBG(_PaintDebugRect(prc)); + + LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get()))); + + WHEN_DBG(_DoDebugBlt(prc)); + + return S_OK; +} + +// Routine Description: +// - Paints the background of the invalid area of the frame. +// Arguments: +// - +// Return Value: +// - S_OK or suitable GDI HRESULT error. +[[nodiscard]] +HRESULT GdiEngine::PaintBackground() noexcept +{ + if (_psInvalidData.fErase) + { + RETURN_IF_FAILED(_PaintBackgroundColor(&_psInvalidData.rcPaint)); + } + + return S_OK; +} + +// Routine Description: +// - Draws one line of the buffer to the screen. +// - This will now be cached in a PolyText buffer and flushed periodically instead of drawing every individual segment. Note this means that the PolyText buffer must be flushed before some operations (changing the brush color, drawing lines on top of the characters, inverting for cursor/selection, etc.) +// Arguments: +// - clusters - text to be written and columns expected per cluster +// - coord - character coordinate target to render within viewport +// - trimLeft - This specifies whether to trim one character width off the left side of the output. Used for drawing the right-half only of a double-wide character. +// Return Value: +// - S_OK or suitable GDI HRESULT error. +// - HISTORICAL NOTES: +// ETO_OPAQUE will paint the background color before painting the text. +// ETO_CLIPPED required for ClearType fonts. Cleartype rendering can escape bounding rectangle unless clipped. +// Unclipped rectangles results in ClearType cutting off the right edge of the previous character when adding chars +// and in leaving behind artifacts when backspace/removing chars. +// This mainly applies to ClearType fonts like Lucida Console at small font sizes (10pt) or bolded. +// See: Win7: 390673, 447839 and then superseded by http://osgvsowi/638274 when FE/non-FE rendering condensed. +//#define CONSOLE_EXTTEXTOUT_FLAGS ETO_OPAQUE | ETO_CLIPPED +//#define MAX_POLY_LINES 80 +[[nodiscard]] +HRESULT GdiEngine::PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool trimLeft) noexcept +{ + try + { + const auto cchLine = clusters.size(); + + // Exit early if there are no lines to draw. + RETURN_HR_IF(S_OK, 0 == cchLine); + + POINT ptDraw = { 0 }; + RETURN_IF_FAILED(_ScaleByFont(&coord, &ptDraw)); + + const auto pPolyTextLine = &_pPolyText[_cPolyText]; + + auto pwsPoly = std::make_unique(cchLine); + RETURN_IF_NULL_ALLOC(pwsPoly); + + COORD const coordFontSize = _GetFontSize(); + + auto rgdxPoly = std::make_unique(cchLine); + RETURN_IF_NULL_ALLOC(rgdxPoly); + + // Sum up the total widths the entire line/run is expected to take while + // copying the pixel widths into a structure to direct GDI how many pixels to use per character. + size_t cchCharWidths = 0; + + // Convert data from clusters into the text array and the widths array. + for (size_t i = 0; i < cchLine; i++) + { + const auto& cluster = clusters.at(i); + + // Our GDI renderer hasn't and isn't going to handle things above U+FFFF or sequences. + // So replace anything complicated with a replacement character for drawing purposes. + pwsPoly[i] = cluster.GetTextAsSingle(); + rgdxPoly[i] = gsl::narrow(cluster.GetColumns()) * coordFontSize.X; + cchCharWidths += rgdxPoly[i]; + } + + // Detect and convert for raster font... + if (!_isTrueTypeFont) + { + // dispatch conversion into our codepage + + // Find out the bytes required + int const cbRequired = WideCharToMultiByte(_fontCodepage, 0, pwsPoly.get(), (int)cchLine, nullptr, 0, nullptr, nullptr); + + if (cbRequired != 0) + { + // Allocate buffer for MultiByte + auto psConverted = std::make_unique(cbRequired); + + // Attempt conversion to current codepage + int const cbConverted = WideCharToMultiByte(_fontCodepage, 0, pwsPoly.get(), (int)cchLine, psConverted.get(), cbRequired, nullptr, nullptr); + + // If successful... + if (cbConverted != 0) + { + // Now we have to convert back to Unicode but using the system ANSI codepage. Find buffer size first. + int const cchRequired = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, nullptr, 0); + + if (cchRequired != 0) + { + auto pwsConvert = std::make_unique(cchRequired); + + // Then do the actual conversion. + int const cchConverted = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, pwsConvert.get(), cchRequired); + + if (cchConverted != 0) + { + // If all successful, use this instead. + pwsPoly.swap(pwsConvert); + } + } + } + } + } + + pPolyTextLine->lpstr = pwsPoly.release(); + pPolyTextLine->n = gsl::narrow(clusters.size()); + pPolyTextLine->x = ptDraw.x; + pPolyTextLine->y = ptDraw.y; + pPolyTextLine->uiFlags = ETO_OPAQUE | ETO_CLIPPED; + pPolyTextLine->rcl.left = pPolyTextLine->x; + pPolyTextLine->rcl.top = pPolyTextLine->y; + pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + ((SHORT)cchCharWidths * coordFontSize.X); + pPolyTextLine->rcl.bottom = pPolyTextLine->rcl.top + coordFontSize.Y; + pPolyTextLine->pdx = rgdxPoly.release(); + + if (trimLeft) + { + pPolyTextLine->rcl.left += coordFontSize.X; + } + + _cPolyText++; + + if (_cPolyText >= s_cPolyTextCache) + { + LOG_IF_FAILED(_FlushBufferLines()); + } + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Flushes any buffer lines in the PolyTextOut cache by drawing them and freeing the strings. +// - See also: PaintBufferLine +// Arguments: +// - +// Return Value: +// - S_OK or E_FAIL if GDI failed. +[[nodiscard]] +HRESULT GdiEngine::_FlushBufferLines() noexcept +{ + HRESULT hr = S_OK; + + if (_cPolyText > 0) + { + if (!PolyTextOutW(_hdcMemoryContext, _pPolyText, (UINT)_cPolyText)) + { + hr = E_FAIL; + } + + for (size_t iPoly = 0; iPoly < _cPolyText; iPoly++) + { + if (nullptr != _pPolyText[iPoly].lpstr) + { + delete[] _pPolyText[iPoly].lpstr; + _pPolyText[iPoly].lpstr = nullptr; + } + + if (nullptr != _pPolyText[iPoly].pdx) + { + delete[] _pPolyText[iPoly].pdx; + _pPolyText[iPoly].pdx = nullptr; + } + } + + _cPolyText = 0; + } + + RETURN_HR(hr); +} + +// Routine Description: +// - Draws up to one line worth of grid lines on top of characters. +// Arguments: +// - lines - Enum defining which edges of the rectangle to draw +// - color - The color to use for drawing the edges. +// - cchLine - How many characters we should draw the grid lines along (left to right in a row) +// - coordTarget - The starting X/Y position of the first character to draw on. +// Return Value: +// - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code. +[[nodiscard]] +HRESULT GdiEngine::PaintBufferGridLines(const GridLines lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept +{ + // Return early if there are no lines to paint. + RETURN_HR_IF(S_OK, GridLines::None == lines); + + LOG_IF_FAILED(_FlushBufferLines()); + + // Convert the target from characters to pixels. + POINT ptTarget; + RETURN_IF_FAILED(_ScaleByFont(&coordTarget, &ptTarget)); + // Set the brush color as requested and save the previous brush to restore at the end. + wil::unique_hbrush hbr(CreateSolidBrush(color)); + RETURN_HR_IF_NULL(E_FAIL, hbr.get()); + + wil::unique_hbrush hbrPrev(SelectBrush(_hdcMemoryContext, hbr.get())); + RETURN_HR_IF_NULL(E_FAIL, hbrPrev.get()); + hbr.release(); // If SelectBrush was successful, GDI owns the brush. Release for now. + + // On exit, be sure we try to put the brush back how it was originally. + auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); }); + + // Get the font size so we know the size of the rectangle lines we'll be inscribing. + COORD const coordFontSize = _GetFontSize(); + + // For each length of the line, inscribe the various lines as specified by the enum + for (size_t i = 0; i < cchLine; i++) + { + if (lines & GridLines::Top) + { + RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y, coordFontSize.X, 1, PATCOPY))); + } + + if (lines & GridLines::Left) + { + RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y, 1, coordFontSize.Y, PATCOPY))); + } + + // NOTE: Watch out for inclusive/exclusive rectangles here. + // We have to remove 1 from the font size for the bottom and right lines to ensure that the + // starting point remains within the clipping rectangle. + // For example, if we're drawing a letter at 0,0 and the font size is 8x16.... + // The bottom left corner inclusive is at 0,15 which is Y (0) + Font Height (16) - 1 = 15. + // The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7. + + if (lines & GridLines::Bottom) + { + RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x, ptTarget.y + coordFontSize.Y - 1, coordFontSize.X, 1, PATCOPY))); + } + + if (lines & GridLines::Right) + { + RETURN_HR_IF(E_FAIL, !(PatBlt(_hdcMemoryContext, ptTarget.x + coordFontSize.X - 1, ptTarget.y, 1, coordFontSize.Y, PATCOPY))); + } + + // Move to the next character in this run. + ptTarget.x += coordFontSize.X; + } + + return S_OK; +} + +// Routine Description: +// - Draws the cursor on the screen +// Arguments: +// - options - Parameters that affect the way that the cursor is drawn +// Return Value: +// - S_OK, suitable GDI HRESULT error, or safemath error, or E_FAIL in a GDI error where a specific error isn't set. +[[nodiscard]] +HRESULT GdiEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept +{ + // if the cursor is off, do nothing - it should not be visible. + if (!options.isOn) + { + return S_FALSE; + } + LOG_IF_FAILED(_FlushBufferLines()); + + COORD const coordFontSize = _GetFontSize(); + RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0); + + // First set up a block cursor the size of the font. + RECT rcBoundaries; + RETURN_IF_FAILED(LongMult(options.coordCursor.X, coordFontSize.X, &rcBoundaries.left)); + RETURN_IF_FAILED(LongMult(options.coordCursor.Y, coordFontSize.Y, &rcBoundaries.top)); + RETURN_IF_FAILED(LongAdd(rcBoundaries.left, coordFontSize.X, &rcBoundaries.right)); + RETURN_IF_FAILED(LongAdd(rcBoundaries.top, coordFontSize.Y, &rcBoundaries.bottom)); + + // If we're double-width cursor, make it an extra font wider. + if (options.fIsDoubleWidth) + { + RETURN_IF_FAILED(LongAdd(rcBoundaries.right, coordFontSize.X, &rcBoundaries.right)); + } + + // Make a set of RECTs to paint. + cursorInvertRects.clear(); + + RECT rcInvert = rcBoundaries; + // depending on the cursorType, add rects to that set + switch (options.cursorType) + { + case CursorType::Legacy: + { + // Now adjust the cursor height + // enforce min/max cursor height + ULONG ulHeight = options.ulCursorHeightPercent; + ulHeight = std::max(ulHeight, s_ulMinCursorHeightPercent); // No smaller than 25% + ulHeight = std::min(ulHeight, s_ulMaxCursorHeightPercent); // No larger than 100% + + ulHeight = MulDiv(coordFontSize.Y, ulHeight, 100); // divide by 100 because percent. + + // Reduce the height of the top to be relative to the bottom by the height we want. + RETURN_IF_FAILED(LongSub(rcInvert.bottom, ulHeight, &rcInvert.top)); + + cursorInvertRects.push_back(rcInvert); + } + break; + + case CursorType::VerticalBar: + LONG proposedWidth; + RETURN_IF_FAILED(LongAdd(rcInvert.left, options.cursorPixelWidth, &proposedWidth)); + // It can't be wider than one cell or we'll have problems in invalidation, so restrict here. + // It's either the left + the proposed width from the ease of access setting, or + // it's the right edge of the block cursor as a maximum. + rcInvert.right = std::min(rcInvert.right, proposedWidth); + cursorInvertRects.push_back(rcInvert); + break; + + case CursorType::Underscore: + RETURN_IF_FAILED(LongAdd(rcInvert.bottom, -1, &rcInvert.top)); + cursorInvertRects.push_back(rcInvert); + break; + + case CursorType::EmptyBox: + { + RECT top, left, right, bottom; + top = left = right = bottom = rcBoundaries; + RETURN_IF_FAILED(LongAdd(top.top, 1, &top.bottom)); + RETURN_IF_FAILED(LongAdd(bottom.bottom, -1, &bottom.top)); + RETURN_IF_FAILED(LongAdd(left.left, 1, &left.right)); + RETURN_IF_FAILED(LongAdd(right.right, -1, &right.left)); + + RETURN_IF_FAILED(LongAdd(top.left, 1, &top.left)); + RETURN_IF_FAILED(LongAdd(bottom.left, 1, &bottom.left)); + RETURN_IF_FAILED(LongAdd(top.right, -1, &top.right)); + RETURN_IF_FAILED(LongAdd(bottom.right, -1, &bottom.right)); + + cursorInvertRects.push_back(top); + cursorInvertRects.push_back(left); + cursorInvertRects.push_back(right); + cursorInvertRects.push_back(bottom); + } + break; + + case CursorType::FullBox: + cursorInvertRects.push_back(rcInvert); + break; + + default: + return E_NOTIMPL; + } + // Either invert all the RECTs, or paint them. + if (options.fUseColor) + { + HBRUSH hCursorBrush = CreateSolidBrush(options.cursorColor); + for (RECT r : cursorInvertRects) + { + RETURN_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, &r, hCursorBrush))); + } + DeleteObject(hCursorBrush); + // Clear out the inverted rects, so that we don't re-invert them next frame. + cursorInvertRects.clear(); + } + else + { + for (RECT r : cursorInvertRects) + { + RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r))); + } + } + + return S_OK; +} + +// Routine Description: +// - Inverts the selected region on the current screen buffer. +// - Reads the selected area, selection mode, and active screen buffer +// from the global properties and dispatches a GDI invert on the selected text area. +// Arguments: +// - rect - Rectangle to invert or highlight to make the selection area +// Return Value: +// - S_OK or suitable GDI HRESULT error. +[[nodiscard]] +HRESULT GdiEngine::PaintSelection(const SMALL_RECT rect) noexcept +{ + LOG_IF_FAILED(_FlushBufferLines()); + + RECT pixelRect = { 0 }; + RETURN_IF_FAILED(_ScaleByFont(&rect, &pixelRect)); + + RETURN_HR_IF(E_FAIL, !InvertRect(_hdcMemoryContext, &pixelRect)); + + return S_OK; +} + +#ifdef DBG + +void GdiEngine::_CreateDebugWindow() +{ + if (_fDebug) + { + const auto className = L"ConsoleGdiDebugWindow"; + + WNDCLASSEX wc = { 0 }; + wc.cbSize = sizeof(WNDCLASSEX); + wc.style = CS_OWNDC; + wc.lpfnWndProc = DefWindowProcW; + wc.hInstance = nullptr; + wc.lpszClassName = className; + + THROW_LAST_ERROR_IF(0 == RegisterClassExW(&wc)); + + _debugWindow = CreateWindowExW(0, + className, + L"ConhostGdiDebugWindow", + 0, + 0, + 0, + 0, + 0, + 0, + nullptr, + nullptr, + nullptr); + + THROW_LAST_ERROR_IF_NULL(_debugWindow); + + ShowWindow(_debugWindow, SW_SHOWNORMAL); + } +} + +// Routine Description: +// - Will fill a given rectangle with a gray shade to help identify which portion of the screen is being debugged. +// - Will attempt immediate BLT so you can see it. +// - NOTE: You must set _fDebug flag for this to operate using a debugger. +// - NOTE: This only works in Debug (DBG) builds. +// Arguments: +// - prc - Pointer to rectangle to fill +// Return Value: +// - +void GdiEngine::_PaintDebugRect(const RECT* const prc) const +{ + if (_fDebug) + { + if (!IsRectEmpty(prc)) + { + wil::unique_hbrush hbr(GetStockBrush(GRAY_BRUSH)); + if (nullptr != LOG_HR_IF_NULL(E_FAIL, hbr.get())) + { + LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get()))); + + _DoDebugBlt(prc); + } + } + } +} + +// Routine Description: +// - Will immediately Blt the given rectangle to the screen for aid in debugging when it is tough to see +// what is occuring with the in-memory DC. +// - This will pause the thread for 200ms when called to give you an opportunity to see the paint. +// - NOTE: You must set _fDebug flag for this to operate using a debugger. +// - NOTE: This only works in Debug (DBG) builds. +// Arguments: +// - prc - Pointer to region to immediately Blt to the real screen DC. +// Return Value: +// - +void GdiEngine::_DoDebugBlt(const RECT* const prc) const +{ + if (_fDebug) + { + if (!IsRectEmpty(prc)) + { + LOG_HR_IF(E_FAIL, !(BitBlt(_debugContext, prc->left, prc->top, prc->right - prc->left, prc->bottom - prc->top, _hdcMemoryContext, prc->left, prc->top, SRCCOPY))); + Sleep(100); + } + } +} + +void GdiEngine::_DebugBltAll() const +{ + if (_fDebug) + { + BitBlt(_debugContext, 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY); + Sleep(100); + } +} +#endif diff --git a/src/renderer/gdi/precomp.cpp b/src/renderer/gdi/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/renderer/gdi/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/renderer/gdi/precomp.h b/src/renderer/gdi/precomp.h new file mode 100644 index 000000000..7afaf5c0f --- /dev/null +++ b/src/renderer/gdi/precomp.h @@ -0,0 +1,48 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#include + +#ifndef _NTSTATUS_DEFINED +#define _NTSTATUS_DEFINED +typedef _Return_type_success_(return >= 0) long NTSTATUS; +#endif + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +//#include +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth +#define FACILITY_NTWIN32 0x7 +__inline int NTSTATUS_FROM_WIN32(long x) { return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); } + +#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) +#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); + +#if defined(DEBUG) || defined(_DEBUG) || defined(DBG) +#define WHEN_DBG(x) x +#else +#define WHEN_DBG(x) +#endif + +// SafeMath +#pragma prefast(push) +#pragma prefast(disable:26071, "Range violation in Intsafe. Not ours.") +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def +#include +#pragma prefast(pop) diff --git a/src/renderer/gdi/sources.inc b/src/renderer/gdi/sources.inc new file mode 100644 index 000000000..57714201b --- /dev/null +++ b/src/renderer/gdi/sources.inc @@ -0,0 +1,33 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Renderer for GDI +# ------------------------------------- + +# This module provides a rendering engine implementation that +# utilizes the GDI framework for drawing the console to a window. + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\invalidate.cpp \ + ..\math.cpp \ + ..\paint.cpp \ + ..\state.cpp \ + +INCLUDES = \ + ..; \ + ..\..\..\inc; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp new file mode 100644 index 000000000..86adbe79f --- /dev/null +++ b/src/renderer/gdi/state.cpp @@ -0,0 +1,530 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "gdirenderer.hpp" +#include "../../inc/conattrs.hpp" +#include // for GWL_CONSOLE_BKCOLOR +#include "../../interactivity/win32/CustomWindowMessages.h" +#pragma hdrstop + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Creates a new GDI-based rendering engine +// - NOTE: Will throw if initialization failure. Caller must catch. +// Arguments: +// - +// Return Value: +// - An instance of a Renderer. +GdiEngine::GdiEngine() : + _hwndTargetWindow((HWND)INVALID_HANDLE_VALUE), +#if DBG + _debugWindow((HWND)INVALID_HANDLE_VALUE), +#endif + _iCurrentDpi(s_iBaseDpi), + _hbitmapMemorySurface(nullptr), + _cPolyText(0), + _fInvalidRectUsed(false), + _lastFg(INVALID_COLOR), + _lastBg(INVALID_COLOR), + _fPaintStarted(false), + _hfont((HFONT)INVALID_HANDLE_VALUE) +{ + ZeroMemory(_pPolyText, sizeof(POLYTEXTW) * s_cPolyTextCache); + _rcInvalid = { 0 }; + _szInvalidScroll = { 0 }; + _szMemorySurface = { 0 }; + + _hdcMemoryContext = CreateCompatibleDC(nullptr); + THROW_HR_IF_NULL(E_FAIL, _hdcMemoryContext); + + // On session zero, text GDI APIs might not be ready. + // Calling GetTextFace causes a wait that will be + // satisfied while GDI text APIs come online. + // + // (Session zero is the non-interactive session + // where long running services processes are hosted. + // this increase security and reliability as user + // applications in interactive session will not be + // able to interact with services through the common + // desktop (e.g., window messages)). + GetTextFaceW(_hdcMemoryContext, 0, nullptr); + +#if DBG + if (_fDebug) + { + _CreateDebugWindow(); + } +#endif +} + +// Routine Description: +// - Destroys an instance of a GDI-based rendering engine +// Arguments: +// - +// Return Value: +// - +GdiEngine::~GdiEngine() +{ + for (size_t iPoly = 0; iPoly < _cPolyText; iPoly++) + { + if (_pPolyText[iPoly].lpstr != nullptr) + { + delete[] _pPolyText[iPoly].lpstr; + } + } + + if (_hbitmapMemorySurface != nullptr) + { + LOG_HR_IF(E_FAIL, !(DeleteObject(_hbitmapMemorySurface))); + _hbitmapMemorySurface = nullptr; + } + + if (_hfont != nullptr) + { + LOG_HR_IF(E_FAIL, !(DeleteObject(_hfont))); + _hfont = nullptr; + } + + if (_hdcMemoryContext != nullptr) + { + LOG_HR_IF(E_FAIL, !(DeleteObject(_hdcMemoryContext))); + _hdcMemoryContext = nullptr; + } +} + +// Routine Description: +// - Updates the window to which this GDI renderer will be bound. +// - A window handle is required for determining the client area and other properties about the rendering surface and monitor. +// Arguments: +// - hwnd - Handle to the window on which we will be drawing. +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT GdiEngine::SetHwnd(const HWND hwnd) noexcept +{ + // First attempt to get the DC and create an appropriate DC + HDC const hdcRealWindow = GetDC(hwnd); + RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow); + + HDC const hdcNewMemoryContext = CreateCompatibleDC(hdcRealWindow); + RETURN_HR_IF_NULL(E_FAIL, hdcNewMemoryContext); + + // If we had an existing memory context stored, release it before proceeding. + if (nullptr != _hdcMemoryContext) + { + LOG_HR_IF(E_FAIL, !(DeleteObject(_hdcMemoryContext))); + _hdcMemoryContext = nullptr; + } + + // Store new window handle and memory context + _hwndTargetWindow = hwnd; + _hdcMemoryContext = hdcNewMemoryContext; + + // If we have a font, apply it to the context. + if (nullptr != _hfont) + { + LOG_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, _hfont)); + } + + if (nullptr != hdcRealWindow) + { + LOG_HR_IF(E_FAIL, !(ReleaseDC(_hwndTargetWindow, hdcRealWindow))); + } + +#if DBG + if (_debugWindow != INVALID_HANDLE_VALUE && _debugWindow != 0) + { + RECT rc = { 0 }; + THROW_IF_WIN32_BOOL_FALSE(GetWindowRect(_hwndTargetWindow, &rc)); + + THROW_IF_WIN32_BOOL_FALSE(SetWindowPos(_debugWindow, nullptr, 0, 0, rc.right - rc.left, rc.bottom - rc.top, SWP_NOMOVE)); + } +#endif + + return S_OK; +} + +// Routine Description: +// - This routine will help call SetWindowLongW with the correct semantics to retrieve the appropriate error code. +// Arguments: +// - hWnd - Window handle to use for setting +// - nIndex - Window handle item offset +// - dwNewLong - Value to update in window structure +// Return Value: +// - S_OK or converted HRESULT from last Win32 error from SetWindowLongW +[[nodiscard]] +HRESULT GdiEngine::s_SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept +{ + // SetWindowLong has strange error handling. On success, it returns the previous Window Long value and doesn't modify the Last Error state. + // To deal with this, we set the last error to 0/S_OK first, call it, and if the previous long was 0, we check if the error was non-zero before reporting. + // Otherwise, we'll get an "Error: The operation has completed successfully." and there will be another screenshot on the internet making fun of Windows. + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx + SetLastError(0); + LONG const lResult = SetWindowLongW(hWnd, nIndex, dwNewLong); + if (0 == lResult) + { + RETURN_LAST_ERROR_IF(0 != GetLastError()); + } + + return S_OK; +} + +// Routine Description: +// - This method will set the GDI brushes in the drawing context (and update the hung-window background color) +// Arguments: +// - colorForeground - Foreground Color +// - colorBackground - Background colo +// - legacyColorAttribute - +// - isBold - +// - isSettingDefaultBrushes - Lets us know that the default brushes are being set so we can update the DC background +// and the hung app background painting color +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT GdiEngine::UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD /*legacyColorAttribute*/, + const bool /*isBold*/, + const bool isSettingDefaultBrushes) noexcept +{ + RETURN_IF_FAILED(_FlushBufferLines()); + + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), _hdcMemoryContext); + + // Set the colors for painting text + if (colorForeground != _lastFg) + { + RETURN_HR_IF(E_FAIL, CLR_INVALID == SetTextColor(_hdcMemoryContext, colorForeground)); + _lastFg = colorForeground; + } + if (colorBackground != _lastBg) + { + RETURN_HR_IF(E_FAIL, CLR_INVALID == SetBkColor(_hdcMemoryContext, colorBackground)); + _lastBg = colorBackground; + } + + if (isSettingDefaultBrushes) + { + // Set the color for painting the extra DC background area + RETURN_HR_IF(E_FAIL, CLR_INVALID == SetDCBrushColor(_hdcMemoryContext, colorBackground)); + + // Set the hung app background painting color + RETURN_IF_FAILED(s_SetWindowLongWHelper(_hwndTargetWindow, GWL_CONSOLE_BKCOLOR, colorBackground)); + } + + return S_OK; +} + +// Routine Description: +// - This method will update the active font on the current device context +// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure. +// Arguments: +// - FontDesired - reference to font information we should use while instantiating a font. +// - Font - reference to font information where the chosen font information will be populated. +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT GdiEngine::UpdateFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font) noexcept +{ + wil::unique_hfont hFont; + RETURN_IF_FAILED(_GetProposedFont(FontDesired, Font, _iCurrentDpi, hFont)); + + // Select into DC + RETURN_HR_IF_NULL(E_FAIL, SelectFont(_hdcMemoryContext, hFont.get())); + + // Save off the font metrics for various other calculations + RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(_hdcMemoryContext, &_tmFontMetrics))); + + // Now find the size of a 0 in this current font and save it for conversions done later. + _coordFontLast = Font.GetSize(); + + // Persist font for cleanup (and free existing if necessary) + if (_hfont != nullptr) + { + LOG_HR_IF(E_FAIL, !(DeleteObject(_hfont))); + _hfont = nullptr; + } + + // Save the font. + _hfont = hFont.release(); + + // Save raster vs. TrueType and codepage data in case we need to convert. + _isTrueTypeFont = Font.IsTrueTypeFont(); + _fontCodepage = Font.GetCodePage(); + + LOG_IF_FAILED(InvalidateAll()); + + return S_OK; +} + +// Routine Description: +// - This method will modify the DPI we're using for scaling calculations. +// Arguments: +// - iDpi - The Dots Per Inch to use for scaling. We will use this relative to the system default DPI defined in Windows headers as a constant. +// Return Value: +// - HRESULT S_OK, GDI-based error code, or safemath error +[[nodiscard]] +HRESULT GdiEngine::UpdateDpi(const int iDpi) noexcept +{ + _iCurrentDpi = iDpi; + return S_OK; +} + +// Method Description: +// - This method will update our internal reference for how big the viewport is. +// Does nothing for GDI. +// Arguments: +// - srNewViewport - The bounds of the new viewport. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT GdiEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept +{ + return S_OK; +} + +// Routine Description: +// - This method will figure out what the new font should be given the starting font information and a DPI. +// - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match. +// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure. +// - If the intent is to immediately turn around and use this font, pass the optional handle parameter and use it immediately. +// Arguments: +// - FontDesired - reference to font information we should use while instantiating a font. +// - Font - reference to font information where the chosen font information will be populated. +// - iDpi - The DPI we will have when rendering +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT GdiEngine::GetProposedFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font, const int iDpi) noexcept +{ + wil::unique_hfont hFont; + return _GetProposedFont(FontDesired, Font, iDpi, hFont); +} + +// Method Description: +// - Updates the window's title string. For GDI, this does nothing, because the +// title must be updated on the main window's windowproc thread. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK if PostMessageW succeeded, otherwise E_FAIL +[[nodiscard]] +HRESULT GdiEngine::_DoUpdateTitle(_In_ const std::wstring& /*newTitle*/) noexcept +{ + // the CM_UPDATE_TITLE handler in windowproc will query the updated title. + return PostMessageW(_hwndTargetWindow, CM_UPDATE_TITLE, 0, (LPARAM)nullptr)? S_OK : E_FAIL; +} + +// Routine Description: +// - This method will figure out what the new font should be given the starting font information and a DPI. +// - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match. +// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure. +// - If the intent is to immediately turn around and use this font, pass the optional handle parameter and use it immediately. +// Arguments: +// - FontDesired - reference to font information we should use while instantiating a font. +// - Font - the actual font +// - iDpi - The DPI we will have when rendering +// - hFont - A smart pointer to receive a handle to a ready-to-use GDI font. +// Return Value: +// - S_OK if set successfully or relevant GDI error via HRESULT. +[[nodiscard]] +HRESULT GdiEngine::_GetProposedFont(const FontInfoDesired& FontDesired, + _Out_ FontInfo& Font, + const int iDpi, + _Inout_ wil::unique_hfont& hFont) noexcept +{ + wil::unique_hdc hdcTemp(CreateCompatibleDC(_hdcMemoryContext)); + RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get()); + + // Get a special engine size because TT fonts can't specify X or we'll get weird scaling under some circumstances. + COORD coordFontRequested = FontDesired.GetEngineSize(); + + // First, check to see if we're asking for the default raster font. + if (FontDesired.IsDefaultRasterFont()) + { + // We're being asked for the default raster font, which gets special handling. In particular, it's the font + // returned by GetStockObject(OEM_FIXED_FONT). + // We do this because, for instance, if we ask GDI for an 8x12 OEM_FIXED_FONT, + // it may very well decide to choose Courier New instead of the Terminal raster. +#pragma prefast(suppress:38037, "raster fonts get special handling, we need to get it this way") + hFont.reset((HFONT)GetStockObject(OEM_FIXED_FONT)); + } + else + { + // For future reference, here is the engine weighting and internal details on Windows Font Mapping: + // https://msdn.microsoft.com/en-us/library/ms969909.aspx + // More relevant links: + // https://support.microsoft.com/en-us/kb/94646 + + // IMPORTANT: Be very careful when modifying the values being passed in below. Even the slightest change can cause + // GDI to return a font other than the one being requested. If you must change the below for any reason, make sure + // these fonts continue to work correctly, as they've been known to break: + // * Monofur + // * Iosevka Extralight + // + // While you're at it, make sure that the behavior matches what happens in the Fonts property sheet. Pay very close + // attention to the font previews to ensure that the font being selected by GDI is exactly the font requested -- + // some monospace fonts look very similar. + LOGFONTW lf = { 0 }; + lf.lfHeight = s_ScaleByDpi(coordFontRequested.Y, iDpi); + lf.lfWidth = s_ScaleByDpi(coordFontRequested.X, iDpi); + lf.lfWeight = FontDesired.GetWeight(); + + // If we're searching for Terminal, our supported Raster Font, then we must use OEM_CHARSET. + // If the System's Non-Unicode Setting is set to English (United States) which is 437 + // and we try to enumerate Terminal with the console codepage as 932, that will turn into SHIFTJIS_CHARSET. + // Despite C:\windows\fonts\vga932.fon always being present, GDI will refuse to load the Terminal font + // that doesn't correspond to the current System Non-Unicode Setting. It will then fall back to a TrueType + // font that does support the SHIFTJIS_CHARSET (because Terminal with CP 437 a.k.a. C:\windows\fonts\vgaoem.fon does NOT support it.) + // This is OK for display purposes (things will render properly) but not OK for API purposes. + // Because the API is affected by the raster/TT status of the actively selected font, we can't have + // GDI choosing a TT font for us when we ask for Raster. We have to settle for forcing the current system + // Terminal font to load even if it doesn't have the glyphs necessary such that the APIs continue to work fine. + if (0 == wcscmp(FontDesired.GetFaceName(), L"Terminal")) + { + lf.lfCharSet = OEM_CHARSET; + } + else + { + CHARSETINFO csi; + if (!TranslateCharsetInfo((DWORD *)IntToPtr(FontDesired.GetCodePage()), &csi, TCI_SRCCODEPAGE)) + { + // if we failed to translate from codepage to charset, choose our charset depending on what kind of font we're + // dealing with. Raster Fonts need to be presented with the OEM charset, while TT fonts need to be ANSI. + csi.ciCharset = (((FontDesired.GetFamily()) & TMPF_TRUETYPE) == TMPF_TRUETYPE) ? ANSI_CHARSET : OEM_CHARSET; + } + + lf.lfCharSet = (BYTE)csi.ciCharset; + } + + lf.lfQuality = DRAFT_QUALITY; + + // NOTE: not using what GDI gave us because some fonts don't quite roundtrip (e.g. MS Gothic and VL Gothic) + lf.lfPitchAndFamily = (FIXED_PITCH | FF_MODERN); + + wcscpy_s(lf.lfFaceName, ARRAYSIZE(lf.lfFaceName), FontDesired.GetFaceName()); + + // Create font. + hFont.reset(CreateFontIndirectW(&lf)); + RETURN_HR_IF_NULL(E_FAIL, hFont.get()); + } + + // Select into DC + wil::unique_hfont hFontOld(SelectFont(hdcTemp.get(), hFont.get())); + RETURN_HR_IF_NULL(E_FAIL, hFontOld.get()); + + // Save off the font metrics for various other calculations + TEXTMETRICW tm; + RETURN_HR_IF(E_FAIL, !(GetTextMetricsW(hdcTemp.get(), &tm))); + + // Now find the size of a 0 in this current font and save it for conversions done later. + SIZE sz; + RETURN_HR_IF(E_FAIL, !(GetTextExtentPoint32W(hdcTemp.get(), L"0", 1, &sz))); + + COORD coordFont; + coordFont.X = static_cast(sz.cx); + coordFont.Y = static_cast(sz.cy); + + // The extent point won't necessarily be perfect for the width, so get the ABC metrics for the 0 if possible to improve the measurement. + // This will fail for non-TrueType fonts and we'll fall back to what GetTextExtentPoint said. + { + ABC abc; + if (0 != GetCharABCWidthsW(hdcTemp.get(), '0', '0', &abc)) + { + int const abcTotal = abc.abcA + abc.abcB + abc.abcC; + + // No negatives or zeros or we'll have bad character-to-pixel math later. + if (abcTotal > 0) + { + coordFont.X = static_cast(abcTotal); + } + } + } + + // Now fill up the FontInfo we were passed with the full details of which font we actually chose + { + // Get the actual font face that we chose + WCHAR wszFaceName[LF_FACESIZE]; + RETURN_HR_IF(E_FAIL, !(GetTextFaceW(hdcTemp.get(), ARRAYSIZE(wszFaceName), wszFaceName))); + + if (FontDesired.IsDefaultRasterFont()) + { + coordFontRequested = coordFont; + } + else if (coordFontRequested.X == 0) + { + coordFontRequested.X = (SHORT)s_ShrinkByDpi(coordFont.X, iDpi); + } + + Font.SetFromEngine(wszFaceName, + tm.tmPitchAndFamily, + tm.tmWeight, + FontDesired.IsDefaultRasterFont(), + coordFont, + coordFontRequested); + } + + return S_OK; +} + +// Routine Description: +// - Retrieves the current pixel size of the font we have selected for drawing. +// Arguments: +// - pFontSize - recieves the current X by Y size of the font. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT GdiEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +{ + *pFontSize = _GetFontSize(); + return S_OK; +} + +// Routine Description: +// - Retrieves the current pixel size of the font we have selected for drawing. +// Arguments: +// - +// Return Value: +// - X by Y size of the font. +COORD GdiEngine::_GetFontSize() const +{ + return _coordFontLast; +} + +// Routine Description: +// - Retrieves whether or not the window is currently minimized. +// Arguments: +// - +// Return Value: +// - True if minimized (don't need to draw anything). False otherwise. +bool GdiEngine::_IsMinimized() const +{ + return !!IsIconic(_hwndTargetWindow); +} + +// Routine Description: +// - Determines whether or not we have a TrueType font selected. +// - Intended only for determining whether we need to perform special raster font scaling. +// Arguments: +// - +// Return Value: +// - True if TrueType. False otherwise (and generally assumed to be raster font type.) +bool GdiEngine::_IsFontTrueType() const +{ + return !!(_tmFontMetrics.tmPitchAndFamily & TMPF_TRUETYPE); +} + +// Routine Description: +// - Helper to determine whether our window handle is valid. +// Allows us to skip operations if we don't have a window. +// Return Value: +// - True if it is valid. +// - False if it is known invalid. +bool GdiEngine::_IsWindowValid() const +{ + return _hwndTargetWindow != INVALID_HANDLE_VALUE && + _hwndTargetWindow != nullptr; +} diff --git a/src/renderer/gdi/tool/main.cpp b/src/renderer/gdi/tool/main.cpp new file mode 100644 index 000000000..c68011daf --- /dev/null +++ b/src/renderer/gdi/tool/main.cpp @@ -0,0 +1,64 @@ +#include "precomp.h" +#include +#include +#include "wincon.h" + +int CALLBACK EnumFontFamiliesExProc( ENUMLOGFONTEX *lpelfe, NEWTEXTMETRICEX *lpntme, int FontType, LPARAM lParam ) +{ + lParam; + FontType; + lpntme; + + if (lpntme->ntmTm.tmPitchAndFamily & TMPF_FIXED_PITCH) + { + // skip non-monospace fonts + // NOTE: this is weird/backwards and the presence of this flag means non-monospace and the absence means monospace. + return 1; + } + + if (lpelfe->elfFullName[0] == L'@') + { + return 1; // skip vertical fonts + } + + if (FontType & DEVICE_FONTTYPE) + { + return 1; // skip device type fonts. we're only going to do raster and truetype. + } + + if (FontType & RASTER_FONTTYPE) + { + if (wcscmp(lpelfe->elfFullName, L"Terminal") != 0) + { + return 1; // skip non-"Terminal" raster fonts. + } + } + + wprintf(L"Charset: %d ", lpntme->ntmTm.tmCharSet); + + wprintf(L"W: %d H: %d", lpntme->ntmTm.tmMaxCharWidth, lpntme->ntmTm.tmHeight); + + wprintf(L"%s, %s, %s\n", lpelfe->elfFullName, lpelfe->elfScript, lpelfe->elfStyle); + return 1; +} + + int __cdecl wmain( int argc, wchar_t** argv ) +{ + argc; + argv; + + HDC hDC = GetDC( NULL ); + + /*LOGFONTW lf = { 0, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, + 0, L"Courier New" };*/ + + LOGFONTW lf = { 0 }; + lf.lfCharSet = DEFAULT_CHARSET; // enumerate this charset. + lf.lfFaceName[0] = L'\0'; // enumerate all font names + lf.lfPitchAndFamily = 0; // required by API. + + + EnumFontFamiliesExW( hDC, &lf, (FONTENUMPROC)EnumFontFamiliesExProc, 0, 0 ); + ReleaseDC( NULL, hDC ); + return 0; +} diff --git a/src/renderer/inc/Cluster.hpp b/src/renderer/inc/Cluster.hpp new file mode 100644 index 000000000..6f335313d --- /dev/null +++ b/src/renderer/inc/Cluster.hpp @@ -0,0 +1,39 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Cluster.hpp + +Abstract: +- This serves as a structure to represent a single glyph cluster drawn on the screen +- This is required to enable N wchar_ts to consume M columns in the display +- Historically, the console only supported 1 wchar_t = 1 column or 1 wchar_t = 2 columns. + +Author(s): +- Michael Niksa (MiNiksa) 25-Mar-2019 +--*/ + +#pragma once + +namespace Microsoft::Console::Render +{ + class Cluster + { + public: + Cluster(const std::wstring_view text, const size_t columns); + + const wchar_t GetTextAsSingle() const noexcept; + + const std::wstring_view& GetText() const noexcept; + + const size_t GetColumns() const noexcept; + + private: + // This is the UTF-16 string of characters that form a particular drawing cluster + const std::wstring_view _text; + + // This is how many columns we're expecting this cluster to take in the display grid + const size_t _columns; + }; +} diff --git a/src/renderer/inc/DummyRenderTarget.hpp b/src/renderer/inc/DummyRenderTarget.hpp new file mode 100644 index 000000000..1fca6153c --- /dev/null +++ b/src/renderer/inc/DummyRenderTarget.hpp @@ -0,0 +1,34 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DummyRenderTarget.hpp + +Abstract: +- Provides an empty implementation of the IRenderTarget interface. + This is needed for some tests, where certain objects need a reference to a + IRenderTarget + +Author(s): +- Mike Griese (migrie) Nov 2018 +--*/ + +#pragma once +#include "IRenderTarget.hpp" + +class DummyRenderTarget final : public Microsoft::Console::Render::IRenderTarget +{ +public: + DummyRenderTarget() {} + void TriggerRedraw(const Microsoft::Console::Types::Viewport& /*region*/) override {} + void TriggerRedraw(const COORD* const /*pcoord*/) override {} + void TriggerRedrawCursor(const COORD* const /*pcoord*/) override {} + void TriggerRedrawAll() override {} + void TriggerTeardown() override {} + void TriggerSelection() override {} + void TriggerScroll() override {} + void TriggerScroll(const COORD* const /*pcoordDelta*/) override {} + void TriggerCircling() override {} + void TriggerTitleChange() override {} +}; diff --git a/src/renderer/inc/FontInfo.hpp b/src/renderer/inc/FontInfo.hpp new file mode 100644 index 000000000..0afc8fbf4 --- /dev/null +++ b/src/renderer/inc/FontInfo.hpp @@ -0,0 +1,66 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontInfo.hpp + +Abstract: +- This serves as the structure defining font information. There are three + relevant classes defined. + +- FontInfo - derived from FontInfoBase. It also has font size + information - both the width and height of the requested font, as + well as the measured height and width of L'0' from GDI. All + coordinates { X, Y } pair are non zero and always set to some + reasonable value, even when GDI APIs fail. This helps avoid + divide by zero issues while performing various sizing + calculations. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "FontInfoBase.hpp" + +class FontInfo : public FontInfoBase +{ +public: + FontInfo(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const COORD coordSize, + const UINT uiCodePage, + const bool fSetDefaultRasterFont = false); + + FontInfo(const FontInfo &fiFont); + + COORD GetSize() const; + COORD GetUnscaledSize() const; + + void SetFromEngine(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont, + const COORD coordSize, + const COORD coordSizeUnscaled); + + void ValidateFont(); + + static void s_SetFontDefaultList(_In_ Microsoft::Console::Render::IFontDefaultList* const pFontDefaultList); + + friend bool operator==(const FontInfo& a, const FontInfo& b); + +private: + void _ValidateCoordSize(); + + COORD _coordSize; + COORD _coordSizeUnscaled; +}; + +bool operator==(const FontInfo& a, const FontInfo& b); + + +// SET AND UNSET CONSOLE_OEMFONT_DISPLAY unless we can get rid of the stupid recoding in the conhost side. diff --git a/src/renderer/inc/FontInfoBase.hpp b/src/renderer/inc/FontInfoBase.hpp new file mode 100644 index 000000000..1f3906adf --- /dev/null +++ b/src/renderer/inc/FontInfoBase.hpp @@ -0,0 +1,70 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontInfoBase.hpp + +Abstract: +- This serves as the structure defining font information. + +- FontInfoBase - the base class that holds the font's GDI's LOGFONT + lfFaceName, lfWeight and lfPitchAndFamily, as well as the code page + to use for WideCharToMultiByte and font name. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "IFontDefaultList.hpp" + +#define DEFAULT_TT_FONT_FACENAME L"__DefaultTTFont__" +#define DEFAULT_RASTER_FONT_FACENAME L"Terminal" + +class FontInfoBase +{ +public: + FontInfoBase(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont, + const UINT uiCodePage); + + FontInfoBase(const FontInfoBase &fibFont); + + ~FontInfoBase(); + + BYTE GetFamily() const; + LONG GetWeight() const; + PCWCHAR GetFaceName() const; + UINT GetCodePage() const; + + bool IsTrueTypeFont() const; + + void SetFromEngine(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const bool fSetDefaultRasterFont); + + bool WasDefaultRasterSetFromEngine() const; + void ValidateFont(); + + static Microsoft::Console::Render::IFontDefaultList* s_pFontDefaultList; + static void s_SetFontDefaultList(_In_ Microsoft::Console::Render::IFontDefaultList* const pFontDefaultList); + + friend bool operator==(const FontInfoBase& a, const FontInfoBase& b); + +protected: + bool IsDefaultRasterFontNoSize() const; + +private: + WCHAR _wszFaceName[LF_FACESIZE]; + LONG _lWeight; + BYTE _bFamily; + UINT _uiCodePage; + bool _fDefaultRasterSetFromEngine; +}; + +bool operator==(const FontInfoBase& a, const FontInfoBase& b); diff --git a/src/renderer/inc/FontInfoDesired.hpp b/src/renderer/inc/FontInfoDesired.hpp new file mode 100644 index 000000000..b64464307 --- /dev/null +++ b/src/renderer/inc/FontInfoDesired.hpp @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- FontInfo.hpp + +Abstract: +- This serves as the structure defining font information. + +- FontInfoDesired - derived from FontInfoBase. It also contains + a desired size { X, Y }, to be supplied to the GDI's LOGFONT + structure. Unlike FontInfo, both desired X and Y can be zero. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "FontInfoBase.hpp" +#include "FontInfo.hpp" + + +class FontInfoDesired : public FontInfoBase +{ +public: + FontInfoDesired(_In_ PCWSTR const pwszFaceName, + const BYTE bFamily, + const LONG lWeight, + const COORD coordSizeDesired, + const UINT uiCodePage); + + FontInfoDesired(const FontInfo &fiFont); + + COORD GetEngineSize() const; + bool IsDefaultRasterFont() const; + + friend bool operator==(const FontInfoDesired& a, const FontInfoDesired& b); + +private: + COORD _coordSizeDesired; +}; + +bool operator==(const FontInfoDesired& a, const FontInfoDesired& b); diff --git a/src/renderer/inc/IFontDefaultList.hpp b/src/renderer/inc/IFontDefaultList.hpp new file mode 100644 index 000000000..c889f9a22 --- /dev/null +++ b/src/renderer/inc/IFontDefaultList.hpp @@ -0,0 +1,26 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IFontDefaultList.hpp + +Abstract: +- This serves as an abstraction to retrieve a list of default preferred fonts that we should use if the user hasn't chosen one. + +Author(s): +- Michael Niksa (MiNiksa) 14-Mar-2016 +--*/ +#pragma once + +namespace Microsoft::Console::Render +{ + class IFontDefaultList + { + public: + [[nodiscard]] + virtual HRESULT RetrieveDefaultFontNameForCodepage(const UINT uiCodePage, + _Out_writes_(cchFaceName) PWSTR pwszFaceName, + const size_t cchFaceName) = 0; + }; +} diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp new file mode 100644 index 000000000..33bbbc947 --- /dev/null +++ b/src/renderer/inc/IRenderData.hpp @@ -0,0 +1,76 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IRenderData.hpp + +Abstract: +- This serves as the interface defining all information needed to render to the screen. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "../../host/conimeinfo.h" +#include "../../buffer/out/TextAttribute.hpp" +#include "../../types/inc/viewport.hpp" + +class TextBuffer; +class Cursor; + +namespace Microsoft::Console::Render +{ + struct RenderOverlay final + { + // This is where the data is stored + const TextBuffer& buffer; + + // This is where the top left of the stored buffer should be overlayed on the screen + // (relative to the current visible viewport) + const COORD origin; + + // This is the area of the buffer that is actually used for overlay. + // Anything outside of this is considered empty by the overlay and shouldn't be used + // for painting purposes. + const Microsoft::Console::Types::Viewport region; + }; + + class IRenderData + { + public: + virtual ~IRenderData() = 0; + virtual Microsoft::Console::Types::Viewport GetViewport() noexcept = 0; + virtual const TextBuffer& GetTextBuffer() noexcept = 0; + virtual const FontInfo& GetFontInfo() noexcept = 0; + virtual const TextAttribute GetDefaultBrushColors() noexcept = 0; + + virtual const COLORREF GetForegroundColor(const TextAttribute& attr) const noexcept = 0; + virtual const COLORREF GetBackgroundColor(const TextAttribute& attr) const noexcept = 0; + + virtual COORD GetCursorPosition() const noexcept = 0; + virtual bool IsCursorVisible() const noexcept = 0; + virtual bool IsCursorOn() const noexcept = 0; + virtual ULONG GetCursorHeight() const noexcept = 0; + virtual CursorType GetCursorStyle() const noexcept = 0; + virtual ULONG GetCursorPixelWidth() const noexcept = 0; + virtual COLORREF GetCursorColor() const noexcept = 0; + virtual bool IsCursorDoubleWidth() const noexcept = 0; + + virtual const std::vector GetOverlays() const noexcept = 0; + + virtual const bool IsGridLineDrawingAllowed() noexcept = 0; + + virtual std::vector GetSelectionRects() noexcept = 0; + + virtual const std::wstring GetConsoleTitle() const noexcept = 0; + + virtual void LockConsole() noexcept = 0; + virtual void UnlockConsole() noexcept = 0; + }; + + // See docs/virtual-dtors.md for an explanation of why this is weird. + inline IRenderData::~IRenderData() {} +} diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp new file mode 100644 index 000000000..d87ba4154 --- /dev/null +++ b/src/renderer/inc/IRenderEngine.hpp @@ -0,0 +1,147 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IRenderEngine.hpp + +Abstract: +- This serves as the entry point for a specific graphics engine specific renderer. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "../../inc/conattrs.hpp" +#include "Cluster.hpp" +#include "FontInfoDesired.hpp" + +namespace Microsoft::Console::Render +{ + class IRenderEngine + { + public: + + enum GridLines + { + None = 0x0, + Top = 0x1, + Bottom = 0x2, + Left = 0x4, + Right = 0x8 + }; + + struct CursorOptions + { + // Character cell in the grid to draw at + // This is relative to the viewport, not the buffer. + COORD coordCursor; + + // For an underscore type _ cursor, how tall it should be as a % of cell height + ULONG ulCursorHeightPercent; + + // For a vertical bar type | cursor, how many pixels wide it should be per ease of access preferences + ULONG cursorPixelWidth; + + // Whether to draw the cursor 2 cells wide (+X from the coordinate given) + bool fIsDoubleWidth; + + // Chooses a special cursor type like a full box, a vertical bar, etc. + CursorType cursorType; + + // Specifies to use the color below instead of the default color + bool fUseColor; + + // Color to use for drawing instead of the default + COLORREF cursorColor; + + // Is the cursor currently visually visible? + // If the cursor has blinked off, this is false. + // if the cursor has blinked on, this is true. + bool isOn; + }; + + virtual ~IRenderEngine() = 0; + + [[nodiscard]] + virtual HRESULT StartPaint() noexcept = 0; + [[nodiscard]] + virtual HRESULT EndPaint() noexcept = 0; + [[nodiscard]] + virtual HRESULT Present() noexcept = 0; + + [[nodiscard]] + virtual HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ScrollFrame() noexcept = 0; + + [[nodiscard]] + virtual HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateSelection(const std::vector& rectangles) noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateAll() noexcept = 0; + [[nodiscard]] + virtual HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept = 0; + + [[nodiscard]] + virtual HRESULT InvalidateTitle(const std::wstring& proposedTitle) noexcept = 0; + + [[nodiscard]] + virtual HRESULT PaintBackground() noexcept = 0; + [[nodiscard]] + virtual HRESULT PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool fTrimLeft) noexcept = 0; + [[nodiscard]] + virtual HRESULT PaintBufferGridLines(const GridLines lines, + const COLORREF color, + const size_t cchLine, + const COORD coordTarget) noexcept = 0; + [[nodiscard]] + virtual HRESULT PaintSelection(const SMALL_RECT rect) noexcept = 0; + + [[nodiscard]] + virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0; + + [[nodiscard]] + virtual HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept = 0; + [[nodiscard]] + virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) noexcept = 0; + [[nodiscard]] + virtual HRESULT UpdateDpi(const int iDpi) noexcept = 0; + [[nodiscard]] + virtual HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo, + const int iDpi) noexcept = 0; + + virtual SMALL_RECT GetDirtyRectInChars() = 0; + [[nodiscard]] + virtual HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept = 0; + [[nodiscard]] + virtual HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept = 0; + [[nodiscard]] + virtual HRESULT UpdateTitle(const std::wstring& newTitle) noexcept = 0; + }; + + inline Microsoft::Console::Render::IRenderEngine::~IRenderEngine() { } +} + +DEFINE_ENUM_FLAG_OPERATORS(Microsoft::Console::Render::IRenderEngine::GridLines) diff --git a/src/renderer/inc/IRenderTarget.hpp b/src/renderer/inc/IRenderTarget.hpp new file mode 100644 index 000000000..78f22d2f1 --- /dev/null +++ b/src/renderer/inc/IRenderTarget.hpp @@ -0,0 +1,44 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IRenderTarget.hpp + +Abstract: +- This serves as the entry point for console rendering activites. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "FontInfoDesired.hpp" +#include "IRenderEngine.hpp" +#include "../types/inc/viewport.hpp" + +namespace Microsoft::Console::Render +{ + class IRenderTarget + { + public: + virtual ~IRenderTarget() = 0; + + virtual void TriggerRedraw(const Microsoft::Console::Types::Viewport& region) = 0; + virtual void TriggerRedraw(const COORD* const pcoord) = 0; + virtual void TriggerRedrawCursor(const COORD* const pcoord) = 0; + + virtual void TriggerRedrawAll() = 0; + virtual void TriggerTeardown() = 0; + + virtual void TriggerSelection() = 0; + virtual void TriggerScroll() = 0; + virtual void TriggerScroll(const COORD* const pcoordDelta) = 0; + virtual void TriggerCircling() = 0; + virtual void TriggerTitleChange() = 0; + }; + + inline Microsoft::Console::Render::IRenderTarget::~IRenderTarget() { } + +} diff --git a/src/renderer/inc/IRenderThread.hpp b/src/renderer/inc/IRenderThread.hpp new file mode 100644 index 000000000..69189bd30 --- /dev/null +++ b/src/renderer/inc/IRenderThread.hpp @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IRenderThread.hpp + +Abstract: +- an abstraction for all the actions a render thread needs to perform. + +Author(s): +- Mike Griese (migrie) 16 Jan 2019 +--*/ + +#pragma once +namespace Microsoft::Console::Render +{ + class IRenderThread + { + public: + virtual ~IRenderThread() = 0; + virtual void NotifyPaint() = 0; + virtual void EnablePainting() = 0; + virtual void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) = 0; + }; + + inline Microsoft::Console::Render::IRenderThread::~IRenderThread() { }; +} diff --git a/src/renderer/inc/IRenderer.hpp b/src/renderer/inc/IRenderer.hpp new file mode 100644 index 000000000..e23a5d8e2 --- /dev/null +++ b/src/renderer/inc/IRenderer.hpp @@ -0,0 +1,65 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IRenderer.hpp + +Abstract: +- This serves as the entry point for console rendering activites. + +Author(s): +- Michael Niksa (MiNiksa) 17-Nov-2015 +--*/ + +#pragma once + +#include "FontInfoDesired.hpp" +#include "IRenderEngine.hpp" +#include "IRenderTarget.hpp" +#include "../types/inc/viewport.hpp" + +namespace Microsoft::Console::Render +{ + class IRenderer : public IRenderTarget + { + public: + virtual ~IRenderer() = 0; + + [[nodiscard]] + virtual HRESULT PaintFrame() = 0; + + virtual void TriggerSystemRedraw(const RECT* const prcDirtyClient) = 0; + + virtual void TriggerRedraw(const Microsoft::Console::Types::Viewport& region) = 0; + virtual void TriggerRedraw(const COORD* const pcoord) = 0; + virtual void TriggerRedrawCursor(const COORD* const pcoord) = 0; + + virtual void TriggerRedrawAll() = 0; + virtual void TriggerTeardown() = 0; + + virtual void TriggerSelection() = 0; + virtual void TriggerScroll() = 0; + virtual void TriggerScroll(const COORD* const pcoordDelta) = 0; + virtual void TriggerCircling() = 0; + virtual void TriggerTitleChange() = 0; + virtual void TriggerFontChange(const int iDpi, + const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) = 0; + + [[nodiscard]] + virtual HRESULT GetProposedFont(const int iDpi, + const FontInfoDesired& FontInfoDesired, + _Out_ FontInfo& FontInfo) = 0; + + virtual bool IsGlyphWideByFont(const std::wstring_view glyph) = 0; + + virtual void EnablePainting() = 0; + virtual void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) = 0; + + virtual void AddRenderEngine(_In_ IRenderEngine* const pEngine) = 0; + }; + + inline Microsoft::Console::Render::IRenderer::~IRenderer() { } + +} diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp new file mode 100644 index 000000000..3bd451fd7 --- /dev/null +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -0,0 +1,46 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- RenderEngineBase.hpp + +Abstract: +- Implements a set of functions with common behavior across all render engines. + For example, the behavior for setting the title. The title may change many + times in the course of a single frame, but the RenderEngine should only + actually perform it's update operation if at the start of a frame, the new + window title will be different then the last frames, and it should only ever + update the title once per frame. + +Author(s): +- Mike Griese (migrie) 10-July-2018 +--*/ +#include "IRenderEngine.hpp" + +#pragma once +namespace Microsoft::Console::Render +{ + class RenderEngineBase : public IRenderEngine + { + public: + RenderEngineBase(); + virtual ~RenderEngineBase() = 0; + + [[nodiscard]] + HRESULT InvalidateTitle(const std::wstring& proposedTitle) noexcept override; + + [[nodiscard]] + HRESULT UpdateTitle(const std::wstring& newTitle) noexcept override; + + protected: + [[nodiscard]] + virtual HRESULT _DoUpdateTitle(const std::wstring& newTitle) noexcept = 0; + + bool _titleChanged; + std::wstring _lastFrameTitle; + + }; + + inline Microsoft::Console::Render::RenderEngineBase::~RenderEngineBase() { } +} diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp new file mode 100644 index 000000000..ea74337c4 --- /dev/null +++ b/src/renderer/vt/VtSequences.cpp @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "vtrenderer.hpp" +#include "../../inc/conattrs.hpp" + +#pragma hdrstop +using namespace Microsoft::Console::Render; + +// Method Description: +// - Formats and writes a sequence to stop the cursor from blinking. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_StopCursorBlinking() noexcept +{ + return _Write("\x1b[?12l"); +} + +// Method Description: +// - Formats and writes a sequence to start the cursor blinking. If it's +// hidden, this won't also show it. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_StartCursorBlinking() noexcept +{ + return _Write("\x1b[?12h"); +} + +// Method Description: +// - Formats and writes a sequence to hide the cursor. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_HideCursor() noexcept +{ + return _Write("\x1b[?25l"); +} + +// Method Description: +// - Formats and writes a sequence to show the cursor. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_ShowCursor() noexcept +{ + return _Write("\x1b[?25h"); +} + +// Method Description: +// - Formats and writes a sequence to erase the remainer of the line starting +// from the cursor position. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_EraseLine() noexcept +{ + // The default no-param action of erase line is erase to the right. + // telnet client doesn't understand the parameterized version, + // so emit the implicit sequence instead. + return _Write("\x1b[K"); +} + +// Method Description: +// - Formats and writes a sequence to either insert or delete a number of lines +// into the buffer at the current cursor location. +// Delete/insert Character removes/adds N characters from/to the buffer, and +// shifts the remaining chars in the row to the left/right, while Erase +// Character replaces N characters with spaces, and leaves the rest +// untouched. +// Arguments: +// - chars: a number of characters to erase (by overwriting with space) +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_EraseCharacter(const short chars) noexcept +{ + static const std::string format = "\x1b[%dX"; + + return _WriteFormattedString(&format, chars); +} + +// Method Description: +// - Moves the cursor forward (right) a number of characters. +// Arguments: +// - chars: a number of characters to move cursor right by. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_CursorForward(const short chars) noexcept +{ + + static const std::string format = "\x1b[%dC"; + + return _WriteFormattedString(&format, chars); +} + +// Method Description: +// - Formats and writes a sequence to erase the remainer of the line starting +// from the cursor position. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_ClearScreen() noexcept +{ + return _Write("\x1b[2J"); +} + +// Method Description: +// - Formats and writes a sequence to either insert or delete a number of lines +// into the buffer at the current cursor location. +// Arguments: +// - sLines: a number of lines to insert or delete +// - fInsertLine: true iff we should insert the lines, false to delete them. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_InsertDeleteLine(const short sLines, const bool fInsertLine) noexcept +{ + if (sLines <= 0) + { + return S_OK; + } + if (sLines == 1) + { + return _Write(fInsertLine ? "\x1b[L" : "\x1b[M"); + } + const std::string format = fInsertLine ? "\x1b[%dL" : "\x1b[%dM"; + + return _WriteFormattedString(&format, sLines); +} + +// Method Description: +// - Formats and writes a sequence to delete a number of lines into the buffer +// at the current cursor location. +// Arguments: +// - sLines: a number of lines to insert +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_DeleteLine(const short sLines) noexcept +{ + return _InsertDeleteLine(sLines, false); +} + +// Method Description: +// - Formats and writes a sequence to insert a number of lines into the buffer +// at the current cursor location. +// Arguments: +// - sLines: a number of lines to insert +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_InsertLine(const short sLines) noexcept +{ + return _InsertDeleteLine(sLines, true); +} + +// Method Description: +// - Formats and writes a sequence to move the cursor to the specified +// coordinate position. The input coord should be in console coordinates, +// where origin=(0,0). +// Arguments: +// - coord: Console coordinates to move the cursor to. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_CursorPosition(const COORD coord) noexcept +{ + static const std::string cursorFormat = "\x1b[%d;%dH"; + + // VT coords start at 1,1 + COORD coordVt = coord; + coordVt.X++; + coordVt.Y++; + + return _WriteFormattedString(&cursorFormat, coordVt.Y, coordVt.X); +} + +// Method Description: +// - Formats and writes a sequence to move the cursor to the origin. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_CursorHome() noexcept +{ + return _Write("\x1b[H"); +} + +// Method Description: +// - Formats and writes a sequence change the boldness of the following text. +// Arguments: +// - isBold: If true, we'll embolden the text. Otherwise we'll debolden the text. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_SetGraphicsBoldness(const bool isBold) noexcept +{ + const std::string fmt = isBold ? "\x1b[1m" : "\x1b[22m"; + return _Write(fmt); +} + +// Method Description: +// - Formats and writes a sequence to change the current text attributes to the default. +// Arguments: +// +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_SetGraphicsDefault() noexcept +{ + return _Write("\x1b[m"); +} + +// Method Description: +// - Formats and writes a sequence to change the current text attributes. +// Arguments: +// - wAttr: Windows color table index to emit as a VT sequence +// - fIsForeground: true if we should emit the foreground sequence, false for background +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_SetGraphicsRendition16Color(const WORD wAttr, + const bool fIsForeground) noexcept +{ + static const std::string fmt = "\x1b[%dm"; + + // Always check using the foreground flags, because the bg flags constants + // are a higher byte + // Foreground sequences are in [30,37] U [90,97] + // Background sequences are in [40,47] U [100,107] + // The "dark" sequences are in the first 7 values, the bright sequences in the second set. + // Note that text brightness and boldness are different in VT. Boldness is + // handled by _SetGraphicsBoldness. Here, we can emit either bright or + // dark colors. For conhost as a terminal, it can't draw bold + // characters, so it displays "bold" as bright, and in fact most + // terminals display the bright color when displaying bolded text. + // By specifying the boldness and brightness seperately, we'll make sure the + // terminal has an accurate representation of our buffer. + const int vtIndex = 30 + + (fIsForeground? 0 : 10) + + ((WI_IsFlagSet(wAttr, FOREGROUND_INTENSITY)) ? 60 : 0) + + (WI_IsFlagSet(wAttr, FOREGROUND_RED) ? 1 : 0) + + (WI_IsFlagSet(wAttr, FOREGROUND_GREEN) ? 2 : 0) + + (WI_IsFlagSet(wAttr, FOREGROUND_BLUE) ? 4 : 0); + + return _WriteFormattedString(&fmt, vtIndex); +} + +// Method Description: +// - Formats and writes a sequence to change the current text attributes to an +// RGB color. +// Arguments: +// - color: The color to emit a VT sequence for +// - fIsForeground: true if we should emit the foreground sequence, false for background +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_SetGraphicsRenditionRGBColor(const COLORREF color, + const bool fIsForeground) noexcept +{ + const std::string fmt = fIsForeground ? + "\x1b[38;2;%d;%d;%dm" : + "\x1b[48;2;%d;%d;%dm"; + + DWORD const r = GetRValue(color); + DWORD const g = GetGValue(color); + DWORD const b = GetBValue(color); + + return _WriteFormattedString(&fmt, r, g, b); +} + +// Method Description: +// - Formats and writes a sequence to change the current text attributes to the +// default foreground or background. Does not affect the boldness of text. +// Arguments: +// - fIsForeground: true if we should emit the foreground sequence, false for background +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_SetGraphicsRenditionDefaultColor(const bool fIsForeground) noexcept +{ + const std::string fmt = fIsForeground ? ("\x1b[39m") : ("\x1b[49m"); + + return _Write(fmt); +} + +// Method Description: +// - Formats and writes a sequence to change the terminal's window size. +// Arguments: +// - sWidth: number of columns the terminal should display +// - sHeight: number of rows the terminal should display +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_ResizeWindow(const short sWidth, const short sHeight) noexcept +{ + static const std::string resizeFormat = "\x1b[8;%d;%dt"; + if (sWidth < 0 || sHeight < 0) + { + return E_INVALIDARG; + } + + return _WriteFormattedString(&resizeFormat, sHeight, sWidth); +} + +// Method Description: +// - Formats and writes a sequence to request the end terminal to tell us the +// cursor position. The terminal will reply back on the vt input handle. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_RequestCursor() noexcept +{ + return _Write("\x1b[6n"); +} + +// Method Description: +// - Formats and writes a sequence to change the terminal's title string +// Arguments: +// - title: string to use as the new title of the window. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_ChangeTitle(_In_ const std::string& title) noexcept +{ + const std::string titleFormat = "\x1b]0;" + title + "\x7"; + return _Write(titleFormat); +} + +// Method Description: +// - Writes a sequence to tell the terminal to start underlining text +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_BeginUnderline() noexcept +{ + return _Write("\x1b[4m"); +} + +// Method Description: +// - Writes a sequence to tell the terminal to stop underlining text +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_EndUnderline() noexcept +{ + return _Write("\x1b[24m"); +} diff --git a/src/renderer/vt/WinTelnetEngine.cpp b/src/renderer/vt/WinTelnetEngine.cpp new file mode 100644 index 000000000..42fbed6b4 --- /dev/null +++ b/src/renderer/vt/WinTelnetEngine.cpp @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WinTelnetEngine.hpp" +#include "..\..\inc\conattrs.hpp" +#pragma hdrstop +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +WinTelnetEngine::WinTelnetEngine(_In_ wil::unique_hfile hPipe, + const IDefaultColorProvider& colorProvider, + const Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) : + VtEngine(std::move(hPipe), colorProvider, initialViewport), + _ColorTable(ColorTable), + _cColorTable(cColorTable) +{ + +} + +// Routine Description: +// - Write a VT sequence to change the current colors of text. Only writes +// 16-color attributes. +// Arguments: +// - colorForeground: The RGB Color to use to paint the foreground text. +// - colorBackground: The RGB Color to use to paint the background of the text. +// - legacyColorAttribute: A console attributes bit field specifying the brush +// colors we should use. +// - isSettingDefaultBrushes: indicates if we should change the background color of +// the window. Unused for VT +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT WinTelnetEngine::UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD /*legacyColorAttribute*/, + const bool isBold, + const bool /*isSettingDefaultBrushes*/) noexcept +{ + return VtEngine::_16ColorUpdateDrawingBrushes(colorForeground, colorBackground, isBold, _ColorTable, _cColorTable); +} + +// Routine Description: +// - Write a VT sequence to move the cursor to the specified coordinates. We +// also store the last place we left the cursor for future optimizations. +// Arguments: +// - coord: location to move the cursor to. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT WinTelnetEngine::_MoveCursor(COORD const coord) noexcept +{ + HRESULT hr = S_OK; + // don't try and be clever about moving the cursor. + // Always just use the full sequence + if (coord.X != _lastText.X || coord.Y != _lastText.Y) + { + hr = _CursorPosition(coord); + if (SUCCEEDED(hr)) + { + _lastText = coord; + } + } + return hr; +} + +// Routine Description: +// - Scrolls the existing data on the in-memory frame by the scroll region +// deltas we have collectively received through the Invalidate methods +// since the last time this was called. +// Because win-telnet doesn't know how to do anything smart in response to +// scrolling, we do nothing. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT WinTelnetEngine::ScrollFrame() noexcept +{ + // win-telnet doesn't know anything about scroll vt sequences + // every frame, we're repainitng everything, always. + return S_OK; +} + +// Routine Description: +// - Notifies us that the console is attempting to scroll the existing screen +// area +// Arguments: +// - pcoordDelta - Pointer to character dimension (COORD) of the distance the +// console would like us to move while scrolling. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT WinTelnetEngine::InvalidateScroll(const COORD* const /*pcoordDelta*/) noexcept +{ + // win-telnet assumes the client doesn't know anything about inserting or + // deleting lines. + // So instead, just invalidate the entire viewport. Every line is going to + // have to move. + return InvalidateAll(); +} + +// Method Description: +// - Wrapper for ITerminalOutputConnection. Write an ascii-only string to the pipe. +// Arguments: +// - wstr - wstring of text to be written +// Return Value: +// - S_OK or suitable HRESULT error from either conversion or writing pipe. +[[nodiscard]] +HRESULT WinTelnetEngine::WriteTerminalW(_In_ const std::wstring& wstr) noexcept +{ + return VtEngine::_WriteTerminalAscii(wstr); +} diff --git a/src/renderer/vt/WinTelnetEngine.hpp b/src/renderer/vt/WinTelnetEngine.hpp new file mode 100644 index 000000000..5329c3c59 --- /dev/null +++ b/src/renderer/vt/WinTelnetEngine.hpp @@ -0,0 +1,59 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WinTelnetEngine.hpp + +Abstract: +- This is the definition of the VT specific implementation of the renderer. + This is the win-telnet implementation, which does NOT support advanced + sequences such as inserting and deleting lines, and only supports 16 colors. + +Author(s): +- Mike Griese (migrie) 01-Sept-2017 +--*/ + +#pragma once + +#include "vtrenderer.hpp" + +namespace Microsoft::Console::Render +{ + class WinTelnetEngine : public VtEngine + { + public: + WinTelnetEngine(_In_ wil::unique_hfile hPipe, + const Microsoft::Console::IDefaultColorProvider& colorProvider, + const Microsoft::Console::Types::Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable); + virtual ~WinTelnetEngine() override = default; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + + [[nodiscard]] + HRESULT WriteTerminalW(const std::wstring& wstr) noexcept override; + +protected: + [[nodiscard]] + HRESULT _MoveCursor(const COORD coord) noexcept; + private: + const COLORREF* const _ColorTable; + const WORD _cColorTable; + + #ifdef UNIT_TESTING + friend class VtRendererTest; + #endif + }; +} diff --git a/src/renderer/vt/Xterm256Engine.cpp b/src/renderer/vt/Xterm256Engine.cpp new file mode 100644 index 000000000..6fab012dd --- /dev/null +++ b/src/renderer/vt/Xterm256Engine.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "Xterm256Engine.hpp" +#pragma hdrstop +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +Xterm256Engine::Xterm256Engine(_In_ wil::unique_hfile hPipe, + const IDefaultColorProvider& colorProvider, + const Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) : + XtermEngine(std::move(hPipe), colorProvider, initialViewport, ColorTable, cColorTable, false) +{ +} + +// Routine Description: +// - Write a VT sequence to change the current colors of text. Writes true RGB +// color sequences. +// Arguments: +// - colorForeground: The RGB Color to use to paint the foreground text. +// - colorBackground: The RGB Color to use to paint the background of the text. +// - legacyColorAttribute: A console attributes bit field specifying the brush +// colors we should use. +// - isSettingDefaultBrushes: indicates if we should change the background color of +// the window. Unused for VT +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT Xterm256Engine::UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool /*isSettingDefaultBrushes*/) noexcept +{ + //When we update the brushes, check the wAttrs to see if the LVB_UNDERSCORE + // flag is there. If the state of that flag is different then our + // current state, change the underlining state. + // We have to do this here, instead of in PaintBufferGridLines, because + // we'll have already painted the text by the time PaintBufferGridLines + // is called. + RETURN_IF_FAILED(_UpdateUnderline(legacyColorAttribute)); + + return VtEngine::_RgbUpdateDrawingBrushes(colorForeground, + colorBackground, + isBold, + _ColorTable, + _cColorTable); +} diff --git a/src/renderer/vt/Xterm256Engine.hpp b/src/renderer/vt/Xterm256Engine.hpp new file mode 100644 index 000000000..9e23ecd6c --- /dev/null +++ b/src/renderer/vt/Xterm256Engine.hpp @@ -0,0 +1,47 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Xterm256Engine.hpp + +Abstract: +- This is the definition of the VT specific implementation of the renderer. + This is the xterm-256color implementation, which supports advanced sequences such as + inserting and deleting lines, and true rgb color. + +Author(s): +- Mike Griese (migrie) 01-Sept-2017 +--*/ + +#pragma once + +#include "XtermEngine.hpp" + +namespace Microsoft::Console::Render +{ + class Xterm256Engine : public XtermEngine + { + public: + Xterm256Engine(_In_ wil::unique_hfile hPipe, + const Microsoft::Console::IDefaultColorProvider& colorProvider, + const Microsoft::Console::Types::Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable); + + virtual ~Xterm256Engine() override = default; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept override; + + private: + + #ifdef UNIT_TESTING + friend class VtRendererTest; + #endif + }; +} diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp new file mode 100644 index 000000000..6d6ac419e --- /dev/null +++ b/src/renderer/vt/XtermEngine.cpp @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "XtermEngine.hpp" +#include "../../types/inc/convert.hpp" +#pragma hdrstop +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, + const IDefaultColorProvider& colorProvider, + const Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable, + const bool fUseAsciiOnly) : + VtEngine(std::move(hPipe), colorProvider, initialViewport), + _ColorTable(ColorTable), + _cColorTable(cColorTable), + _fUseAsciiOnly(fUseAsciiOnly), + _previousLineWrapped(false), + _usingUnderLine(false), + _needToDisableCursor(false) +{ + // Set out initial cursor position to -1, -1. This will force our initial + // paint to manually move the cursor to 0, 0, not just ignore it. + _lastText = VtEngine::INVALID_COORDS; +} + +// Method Description: +// - Prepares internal structures for a painting operation. Turns the cursor +// off, so we don't see it flashing all over the client's screen as we +// paint the new contents. +// Arguments: +// - +// Return Value: +// - S_OK if we started to paint. S_FALSE if we didn't need to paint. HRESULT +// error code if painting didn't start successfully, or we failed to write +// the pipe. +[[nodiscard]] +HRESULT XtermEngine::StartPaint() noexcept +{ + RETURN_IF_FAILED(VtEngine::StartPaint()); + + _trace.TraceLastText(_lastText); + + if (_firstPaint) + { + // MSFT:17815688 + // If the caller requested to inherit the cursor, we shouldn't + // clear the screen on the first paint. Otherwise, we'll clear + // the screen on the first paint, just to make sure that the + // terminal's state is consistent with what we'll be rendering. + RETURN_IF_FAILED(_ClearScreen()); + _clearedAllThisFrame = true; + _firstPaint = false; + } + else + { + const auto dirtyRect = GetDirtyRectInChars(); + const auto dirtyView = Viewport::FromInclusive(dirtyRect); + if (!_resized && dirtyView == _lastViewport) + { + // TODO: MSFT:21096414 - This is never actually hit. We set + // _resized=true on every frame (see VtEngine::UpdateViewport). + // Unfortunately, not always setting _resized is not a good enough + // solution, see that work item for a description why. + RETURN_IF_FAILED(_ClearScreen()); + _clearedAllThisFrame = true; + } + } + + if (!_quickReturn) + { + if (!_WillWriteSingleChar()) + { + // MSFT:TODO:20331739 + // Make sure to match the cursor visibility in the terminal to the console's + // // Turn off cursor + // RETURN_IF_FAILED(_HideCursor()); + } + else + { + // Don't re-enable the cursor. + _quickReturn = true; + } + } + + return S_OK; +} + +// Routine Description: +// - EndPaint helper to perform the final rendering steps. Turn the cursor back +// on. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT XtermEngine::EndPaint() noexcept +{ + + // MSFT:TODO:20331739 + // Make sure to match the cursor visibility in the terminal to the console's + // if (!_quickReturn) + // { + // // Turn on cursor + // RETURN_IF_FAILED(_ShowCursor()); + // } + + // If during the frame we determined that the cursor needed to be disabled, + // then insert a cursor off at the start of the buffer, and re-enable + // the cursor here. + if (_needToDisableCursor) + { + _buffer.insert(0, "\x1b[25l"); + RETURN_IF_FAILED(_ShowCursor()); + } + + RETURN_IF_FAILED(VtEngine::EndPaint()); + + _needToDisableCursor = false; + + return S_OK; +} + + +// Routine Description: +// - Write a VT sequence to either start or stop underlining text. +// Arguments: +// - legacyColorAttribute: A console attributes bit field containing information +// about the underlining state of the text. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT XtermEngine::_UpdateUnderline(const WORD legacyColorAttribute) noexcept +{ + bool textUnderlined = WI_IsFlagSet(legacyColorAttribute, COMMON_LVB_UNDERSCORE); + if (textUnderlined != _usingUnderLine) + { + if (textUnderlined) + { + RETURN_IF_FAILED(_BeginUnderline()); + } + else + { + RETURN_IF_FAILED(_EndUnderline()); + } + _usingUnderLine = textUnderlined; + } + return S_OK; +} + +// Routine Description: +// - Write a VT sequence to change the current colors of text. Only writes +// 16-color attributes. +// Arguments: +// - colorForeground: The RGB Color to use to paint the foreground text. +// - colorBackground: The RGB Color to use to paint the background of the text. +// - legacyColorAttribute: A console attributes bit field specifying the brush +// colors we should use. +// - isSettingDefaultBrushes: indicates if we should change the background color of +// the window. Unused for VT +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT XtermEngine::UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool /*isSettingDefaultBrushes*/) noexcept +{ + //When we update the brushes, check the wAttrs to see if the LVB_UNDERSCORE + // flag is there. If the state of that flag is different then our + // current state, change the underlining state. + // We have to do this here, instead of in PaintBufferGridLines, because + // we'll have already painted the text by the time PaintBufferGridLines + // is called. + + RETURN_IF_FAILED(_UpdateUnderline(legacyColorAttribute)); + // The base xterm mode only knows about 16 colors + return VtEngine::_16ColorUpdateDrawingBrushes(colorForeground, colorBackground, isBold, _ColorTable, _cColorTable); +} + +// Routine Description: +// - Write a VT sequence to move the cursor to the specified coordinates. We +// also store the last place we left the cursor for future optimizations. +// If the cursor only needs to go to the origin, only write the home sequence. +// If the new cursor is only down one line from the current, only write a newline +// If the new cursor is only down one line and at the start of the line, write +// a carriage return. +// Otherwise just write the whole sequence for moving it. +// Arguments: +// - coord: location to move the cursor to. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT XtermEngine::_MoveCursor(COORD const coord) noexcept +{ + HRESULT hr = S_OK; + + if (coord.X != _lastText.X || coord.Y != _lastText.Y) + { + if (coord.X == 0 && coord.Y == 0) + { + _needToDisableCursor = true; + hr = _CursorHome(); + } + else if (coord.X == 0 && coord.Y == (_lastText.Y+1)) + { + // Down one line, at the start of the line. + + // If the previous line wrapped, then the cursor is already at this + // position, we just don't know it yet. Don't emit anything. + if (_previousLineWrapped) + { + hr = S_OK; + } + else + { + std::string seq = "\r\n"; + hr = _Write(seq); + } + } + else if (coord.X == 0 && coord.Y == _lastText.Y) + { + // Start of this line + std::string seq = "\r"; + hr = _Write(seq); + } + else if (coord.X == _lastText.X && coord.Y == (_lastText.Y+1)) + { + // Down one line, same X position + std::string seq = "\n"; + hr = _Write(seq); + } + else if (coord.X == (_lastText.X-1) && coord.Y == (_lastText.Y)) + { + // Back one char, same Y position + std::string seq = "\b"; + hr = _Write(seq); + } + else if (coord.Y == _lastText.Y && coord.X > _lastText.X) + { + // Same line, forward some distance + short distance = coord.X - _lastText.X; + hr = _CursorForward(distance); + } + else + { + _needToDisableCursor = true; + hr = _CursorPosition(coord); + } + + if (SUCCEEDED(hr)) + { + _lastText = coord; + } + } + if (_lastText.Y != _lastViewport.ToOrigin().BottomInclusive()) + { + _newBottomLine = false; + } + _deferredCursorPos = INVALID_COORDS; + return hr; +} + +// Routine Description: +// - Scrolls the existing data on the in-memory frame by the scroll region +// deltas we have collectively received through the Invalidate methods +// since the last time this was called. +// Move the cursor to the origin, and insert or delete rows as appropriate. +// The inserted rows will be blank, but marked invalid by InvalidateScroll, +// so they will later be written by PaintBufferLine. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT XtermEngine::ScrollFrame() noexcept +{ + if (_scrollDelta.X != 0) + { + // No easy way to shift left-right. Everything needs repainting. + return InvalidateAll(); + } + if (_scrollDelta.Y == 0) + { + // There's nothing to do here. Do nothing. + return S_OK; + } + + const short dy = _scrollDelta.Y; + const short absDy = static_cast(abs(dy)); + + HRESULT hr = S_OK; + if (dy < 0) + { + // Instead of deleting the first line (causing everything to move up) + // move to the bottom of the buffer, and newline. + // That will cause everything to move up, by moving the viewport down. + // This will let remote conhosts scroll up to see history like normal. + const short bottom = _lastViewport.ToOrigin().BottomInclusive(); + hr = _MoveCursor({0, bottom}); + if (SUCCEEDED(hr)) + { + std::string seq = std::string(absDy, '\n'); + hr = _Write(seq); + // Mark that the bottom line is new, so we won't spend time with an + // ECH on it. + _newBottomLine = true; + } + // We don't need to _MoveCursor the cursor again, because it's still + // at the bottom of the viewport. + } + else if (dy > 0) + { + // Move to the top of the buffer, and insert some lines of text, to + // cause the viewport contents to shift down. + hr = _MoveCursor({0, 0}); + if (SUCCEEDED(hr)) + { + hr = _InsertLine(absDy); + } + } + + return hr; +} + +// Routine Description: +// - Notifies us that the console is attempting to scroll the existing screen +// area. Add the top or bottom rows to the invalid region, and update the +// total scroll delta accumulated this frame. +// Arguments: +// - pcoordDelta - Pointer to character dimension (COORD) of the distance the +// console would like us to move while scrolling. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for safemath failure +[[nodiscard]] +HRESULT XtermEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +{ + const short dx = pcoordDelta->X; + const short dy = pcoordDelta->Y; + + if (dx != 0 || dy != 0) + { + // Scroll the current offset + RETURN_IF_FAILED(_InvalidOffset(pcoordDelta)); + + // Add the top/bottom of the window to the invalid area + SMALL_RECT invalid = _lastViewport.ToOrigin().ToExclusive(); + + if (dy > 0) + { + invalid.Bottom = dy; + } + else if (dy < 0) + { + invalid.Top = invalid.Bottom + dy; + } + LOG_IF_FAILED(_InvalidCombine(Viewport::FromExclusive(invalid))); + + COORD invalidScrollNew; + RETURN_IF_FAILED(ShortAdd(_scrollDelta.X, dx, &invalidScrollNew.X)); + RETURN_IF_FAILED(ShortAdd(_scrollDelta.Y, dy, &invalidScrollNew.Y)); + + // Store if safemath succeeded + _scrollDelta = invalidScrollNew; + + } + + return S_OK; +} + +// Routine Description: +// - Draws one line of the buffer to the screen. Writes the characters to the +// pipe, encoded in UTF-8 or ASCII only, depending on the VtIoMode. +// (See descriptions of both implementations for details.) +// Arguments: +// - clusters - text and column counts for each piece of text. +// - coord - character coordinate target to render within viewport +// - trimLeft - This specifies whether to trim one character width off the left +// side of the output. Used for drawing the right-half only of a +// double-wide character. +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT XtermEngine::PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool /*trimLeft*/) noexcept +{ + return _fUseAsciiOnly ? + VtEngine::_PaintAsciiBufferLine(clusters, coord) : + VtEngine::_PaintUtf8BufferLine(clusters, coord); +} + +// Method Description: +// - Wrapper for ITerminalOutputConnection. Write either an ascii-only, or a +// proper utf-8 string, depending on our mode. +// Arguments: +// - wstr - wstring of text to be written +// Return Value: +// - S_OK or suitable HRESULT error from either conversion or writing pipe. +[[nodiscard]] +HRESULT XtermEngine::WriteTerminalW(const std::wstring& wstr) noexcept +{ + return _fUseAsciiOnly ? + VtEngine::_WriteTerminalAscii(wstr) : + VtEngine::_WriteTerminalUtf8(wstr); +} + +// Method Description: +// - Updates the window's title string. Emits the VT sequence to SetWindowTitle. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT XtermEngine::_DoUpdateTitle(const std::wstring& newTitle) noexcept +{ + // inbox telnet uses xterm-ascii as it's mode. If we're in ascii mode, don't + // do anything, to maintain compatibility. + if (_fUseAsciiOnly) + { + return S_OK; + } + + try + { + const auto converted = ConvertToA(CP_UTF8, newTitle); + return VtEngine::_ChangeTitle(converted); + } + CATCH_RETURN(); +} diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp new file mode 100644 index 000000000..672d6202a --- /dev/null +++ b/src/renderer/vt/XtermEngine.hpp @@ -0,0 +1,84 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- XtermEngine.hpp + +Abstract: +- This is the definition of the VT specific implementation of the renderer. + This is the xterm implementation, which supports advanced sequences such as + inserting and deleting lines, but only 16 colors. + + This engine supports both xterm and xterm-ascii VT modes. + The difference being that xterm-ascii will render any characters above 0x7f + as '?', in order to support older legacy tools. + +Author(s): +- Mike Griese (migrie) 01-Sept-2017 +--*/ + +#pragma once + +#include "vtrenderer.hpp" + +namespace Microsoft::Console::Render +{ + class XtermEngine : public VtEngine + { + public: + XtermEngine(_In_ wil::unique_hfile hPipe, + const Microsoft::Console::IDefaultColorProvider& colorProvider, + const Microsoft::Console::Types::Viewport initialViewport, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable, + const bool fUseAsciiOnly); + + virtual ~XtermEngine() override = default; + + [[nodiscard]] + HRESULT StartPaint() noexcept override; + [[nodiscard]] + HRESULT EndPaint() noexcept override; + + [[nodiscard]] + virtual HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool trimLeft) noexcept override; + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + + [[nodiscard]] + HRESULT WriteTerminalW(_In_ const std::wstring& str) noexcept override; + + protected: + const COLORREF* const _ColorTable; + const WORD _cColorTable; + const bool _fUseAsciiOnly; + bool _previousLineWrapped; + bool _usingUnderLine; + bool _needToDisableCursor; + + [[nodiscard]] + HRESULT _MoveCursor(const COORD coord) noexcept override; + + [[nodiscard]] + HRESULT _UpdateUnderline(const WORD wLegacyAttrs) noexcept; + + [[nodiscard]] + HRESULT _DoUpdateTitle(const std::wstring& newTitle) noexcept override; + + #ifdef UNIT_TESTING + friend class VtRendererTest; + #endif + }; +} diff --git a/src/renderer/vt/dirs b/src/renderer/vt/dirs new file mode 100644 index 000000000..4d3cf932b --- /dev/null +++ b/src/renderer/vt/dirs @@ -0,0 +1,3 @@ +DIRS= \ + lib \ + ut_lib \ diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp new file mode 100644 index 000000000..100bb40cb --- /dev/null +++ b/src/renderer/vt/invalidate.cpp @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "vtrenderer.hpp" + +using namespace Microsoft::Console::Types; +#pragma hdrstop + +using namespace Microsoft::Console::Render; + +// Routine Description: +// - Notifies us that the system has requested a particular pixel area of the +// client rectangle should be redrawn. (On WM_PAINT) +// For VT, this doesn't mean anything. So do nothing. +// Arguments: +// - prcDirtyClient - Pointer to pixel area (RECT) of client region the system +// believes is dirty +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::InvalidateSystem(const RECT* const /*prcDirtyClient*/) noexcept +{ + return S_OK; +} + +// Routine Description: +// - Notifies us that the console has changed the selection region and would +// like it updated +// Arguments: +// - rectangles - Vector of rectangles to draw, line by line +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::InvalidateSelection(const std::vector& /*rectangles*/) noexcept +{ + // Selection shouldn't be handled bt the VT Renderer Host, it should be + // handled by the client. + + return S_OK; +} + +// Routine Description: +// - Notifies us that the console has changed the character region specified. +// - NOTE: This typically triggers on cursor or text buffer changes +// Arguments: +// - psrRegion - Character region (SMALL_RECT) that has been changed +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept +{ + Viewport newInvalid = Viewport::FromExclusive(*psrRegion); + _trace.TraceInvalidate(newInvalid); + + return this->_InvalidCombine(newInvalid); +} + +// Routine Description: +// - Notifies us that the console has changed the position of the cursor. +// Arguments: +// - pcoordCursor - the new position of the cursor +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +{ + // If we just inherited the cursor, we're going to get an InvalidateCursor + // for both where the old cursor was, and where the new cursor is + // (the inherited location). (See Cursor.cpp:Cursor::SetPosition) + // We should ignore the first one, but after that, if the client application + // is moving the cursor around in the viewport, move our virtual top + // up to meet their changes. + if (!_skipCursor && _virtualTop > pcoordCursor->Y) + { + _virtualTop = pcoordCursor->Y; + } + _skipCursor = false; + + _cursorMoved = true; + return S_OK; +} + +// Routine Description: +// - Notifies to repaint everything. +// - NOTE: Use sparingly. Only use when something that could affect the entire +// frame simultaneously occurs. +// Arguments: +// - +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::InvalidateAll() noexcept +{ + _trace.TraceInvalidateAll(_lastViewport.ToOrigin()); + return this->_InvalidCombine(_lastViewport.ToOrigin()); +} + +// Method Description: +// - Notifies us that we're about to circle the buffer, giving us a chance to +// force a repaint before the buffer contents are lost. The VT renderer +// needs to be able to render all text before it's lost, so we return true. +// Arguments: +// - Recieves a bool indicating if we should force the repaint. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = true; + + // Keep track of the fact that we circled, we'll need to do some work on + // end paint to specifically handle this. + _circled = true; + + return S_OK; +} + +// Method Description: +// - Notifies us that we're about to be torn down. This gives us a last chance +// to force a repaint before the buffer contents are lost. The VT renderer +// needs to be able to render all text before it's lost, so we return true. +// Arguments: +// - Recieves a bool indicating if we should force the repaint. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = true; + return S_OK; +} + +// Routine Description: +// - Helper to combine the given rectangle into the invalid region to be +// updated on the next paint +// Expects EXCLUSIVE rectangles. +// Arguments: +// - invalid - A viewport containing the character region that should be +// repainted on the next frame +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_InvalidCombine(const Viewport invalid) noexcept +{ + if (!_fInvalidRectUsed) + { + _invalidRect = invalid; + _fInvalidRectUsed = true; + } + else + { + _invalidRect = Viewport::Union(_invalidRect, invalid); + } + + // Ensure invalid areas remain within bounds of window. + RETURN_IF_FAILED(_InvalidRestrict()); + + return S_OK; +} + +// Routine Description: +// - Helper to adjust the invalid region by the given offset such as when a +// scroll operation occurs. +// Arguments: +// - ppt - Distances by which we should move the invalid region in response to a scroll +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_InvalidOffset(const COORD* const pCoord) noexcept +{ + if (_fInvalidRectUsed) + { + try + { + Viewport newInvalid = Viewport::Offset(_invalidRect, *pCoord); + + // Add the scrolled invalid rectangle to what was left behind to get the new invalid area. + // This is the equivalent of adding in the "update rectangle" that we would get out of ScrollWindowEx/ScrollDC. + _invalidRect = Viewport::Union(_invalidRect, newInvalid); + + // Ensure invalid areas remain within bounds of window. + RETURN_IF_FAILED(_InvalidRestrict()); + } + CATCH_RETURN(); + } + + return S_OK; +} + +// Routine Description: +// - Helper to ensure the invalid region remains within the bounds of the viewport. +// Arguments: +// - +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or safemath failure. +[[nodiscard]] +HRESULT VtEngine::_InvalidRestrict() noexcept +{ + SMALL_RECT oldInvalid = _invalidRect.ToExclusive(); + + _lastViewport.ToOrigin().TrimToViewport(&oldInvalid); + + _invalidRect = Viewport::FromExclusive(oldInvalid); + + return S_OK; +} diff --git a/src/renderer/vt/lib/sources b/src/renderer/vt/lib/sources new file mode 100644 index 000000000..b2783896d --- /dev/null +++ b/src/renderer/vt/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderVt +TARGETTYPE = LIBRARY diff --git a/src/renderer/vt/lib/vt.vcxproj b/src/renderer/vt/lib/vt.vcxproj new file mode 100644 index 000000000..468d236e6 --- /dev/null +++ b/src/renderer/vt/lib/vt.vcxproj @@ -0,0 +1,16 @@ + + + + + + + {990F2657-8580-4828-943F-5DD657D11842} + Win32Proj + vt + RendererVt + ConRenderVt + + + + + diff --git a/src/renderer/vt/lib/vt.vcxproj.filters b/src/renderer/vt/lib/vt.vcxproj.filters new file mode 100644 index 000000000..e65ed3729 --- /dev/null +++ b/src/renderer/vt/lib/vt.vcxproj.filters @@ -0,0 +1,42 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/renderer/vt/math.cpp b/src/renderer/vt/math.cpp new file mode 100644 index 000000000..342cdd4d2 --- /dev/null +++ b/src/renderer/vt/math.cpp @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "vtrenderer.hpp" + +#pragma hdrstop + +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Gets the size in characters of the current dirty portion of the frame. +// Arguments: +// - +// Return Value: +// - The character dimensions of the current dirty area of the frame. +// This is an Inclusive rect. +SMALL_RECT VtEngine::GetDirtyRectInChars() +{ + SMALL_RECT dirty = _invalidRect.ToInclusive(); + if (dirty.Top < _virtualTop) + { + dirty.Top = _virtualTop; + } + return dirty; +} + +// Routine Description: +// - Uses the currently selected font to determine how wide the given character will be when renderered. +// - NOTE: Only supports determining half-width/full-width status for CJK-type languages (e.g. is it 1 character wide or 2. a.k.a. is it a rectangle or square.) +// Arguments: +// - glyph - utf16 encoded codepoint to check +// - pResult - recieves return value, True if it is full-width (2 wide). False if it is half-width (1 wide). +// Return Value: +// - S_FALSE: This is unsupported by the VT Renderer and should use another engine's value. +[[nodiscard]] +HRESULT VtEngine::IsGlyphWideByFont(const std::wstring_view /*glyph*/, _Out_ bool* const pResult) noexcept +{ + *pResult = false; + return S_FALSE; +} + +// Routine Description: +// - Performs a "CombineRect" with the "OR" operation. +// - Basically extends the existing rect outward to also encompass the passed-in region. +// Arguments: +// - pRectExisting - Expand this rectangle to encompass the add rect. +// - pRectToOr - Add this rectangle to the existing one. +// Return Value: +// - +void VtEngine::_OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT* const pRectToOr) const +{ + pRectExisting->Left = std::min(pRectExisting->Left, pRectToOr->Left); + pRectExisting->Top = std::min(pRectExisting->Top, pRectToOr->Top); + pRectExisting->Right = std::max(pRectExisting->Right, pRectToOr->Right); + pRectExisting->Bottom = std::max(pRectExisting->Bottom, pRectToOr->Bottom); +} + +// Method Description: +// - Returns true if the invalidated region indicates that we only need to +// simply print text from the current cursor position. This will prevent us +// from sending extra VT set-up/tear down sequences (?12h/l) when all we +// need to do is print more text at the current cursor position. +// Arguments: +// - +// Return Value: +// - true iff only the next character is invalid +bool VtEngine::_WillWriteSingleChar() const +{ + COORD currentCursor = _lastText; + SMALL_RECT _srcInvalid = _invalidRect.ToExclusive(); + bool noScrollDelta = (_scrollDelta.X == 0 && _scrollDelta.Y == 0); + + bool invalidIsOneChar = (_invalidRect.Width() == 1) && + (_invalidRect.Height() == 1); + // Either the next character to the right or the immediately previous + // character should follow this code path + // (The immediate previous character would suggest a backspace) + bool invalidIsNext = (_srcInvalid.Top == _lastText.Y) + && (_srcInvalid.Left == _lastText.X); + bool invalidIsLast = (_srcInvalid.Top == _lastText.Y) + && (_srcInvalid.Left == (_lastText.X-1)); + + return noScrollDelta && invalidIsOneChar && (invalidIsNext || invalidIsLast); +} diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp new file mode 100644 index 000000000..13c64fc42 --- /dev/null +++ b/src/renderer/vt/paint.cpp @@ -0,0 +1,538 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "vtrenderer.hpp" +#include "../../inc/conattrs.hpp" +#include "../../types/inc/convert.hpp" + +#pragma hdrstop +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +// Routine Description: +// - Prepares internal structures for a painting operation. +// Arguments: +// - +// Return Value: +// - S_OK if we started to paint. S_FALSE if we didn't need to paint. +// HRESULT error code if painting didn't start successfully. +[[nodiscard]] +HRESULT VtEngine::StartPaint() noexcept +{ + if (_pipeBroken) + { + return S_FALSE; + } + + // If there's nothing to do, quick return + bool somethingToDo = _fInvalidRectUsed || + (_scrollDelta.X != 0 || _scrollDelta.Y != 0) || + _cursorMoved || + _titleChanged; + + _quickReturn = !somethingToDo; + _trace.TraceStartPaint(_quickReturn, _fInvalidRectUsed, _invalidRect, _lastViewport, _scrollDelta, _cursorMoved); + + return _quickReturn ? S_FALSE : S_OK; +} + +// Routine Description: +// - EndPaint helper to perform the final cleanup after painting. If we +// returned S_FALSE from StartPaint, there's no guarantee this was called. +// That's okay however, EndPaint only zeros structs that would be zero if +// StartPaint returns S_FALSE. +// Arguments: +// - +// Return Value: +// - S_OK, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::EndPaint() noexcept +{ + _trace.TraceEndPaint(); + + _invalidRect = Viewport::Empty(); + _fInvalidRectUsed = false; + _scrollDelta = {0}; + _clearedAllThisFrame = false; + _cursorMoved = false; + _firstPaint = false; + _skipCursor = false; + _resized = false; + // If we've circled the buffer this frame, move our virtual top upwards. + // We do this at the END of the frame, so that during the paint, we still + // use the original virtual top. + if (_circled) + { + if (_virtualTop > 0) + { + _virtualTop--; + } + } + _circled = false; + + // If we deferred a cursor movement during the frame, make sure we put the + // cursor in the right place before we end the frame. + if (_deferredCursorPos != INVALID_COORDS) + { + RETURN_IF_FAILED(_MoveCursor(_deferredCursorPos)); + } + + RETURN_IF_FAILED(_Flush()); + + return S_OK; +} + +// Routine Description: +// - Used to perform longer running presentation steps outside the lock so the +// other threads can continue. +// - Not currently used by VtEngine. +// Arguments: +// - +// Return Value: +// - S_FALSE since we do nothing. +[[nodiscard]] +HRESULT VtEngine::Present() noexcept +{ + return S_FALSE; +} + +// Routine Description: +// - Paints the background of the invalid area of the frame. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::PaintBackground() noexcept +{ + return S_OK; +} + +// Routine Description: +// - Draws one line of the buffer to the screen. Writes the characters to the +// pipe. If the characters are outside the ASCII range (0-0x7f), then +// instead writes a '?' +// Arguments: +// - clusters - text and column count data to be written +// - trimLeft - This specifies whether to trim one character width off the left +// side of the output. Used for drawing the right-half only of a +// double-wide character. +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool /*trimLeft*/) noexcept +{ + return VtEngine::_PaintAsciiBufferLine(clusters, coord); +} + +// Method Description: +// - Draws up to one line worth of grid lines on top of characters. +// Arguments: +// - lines - Enum defining which edges of the rectangle to draw +// - color - The color to use for drawing the edges. +// - cchLine - How many characters we should draw the grid lines along (left to right in a row) +// - coordTarget - The starting X/Y position of the first character to draw on. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::PaintBufferGridLines(const GridLines /*lines*/, + const COLORREF /*color*/, + const size_t /*cchLine*/, + const COORD /*coordTarget*/) noexcept +{ + return S_OK; +} + +// Routine Description: +// - Draws the cursor on the screen +// Arguments: +// - options - Options that affect the presentation of the cursor +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept +{ + // MSFT:15933349 - Send the terminal the updated cursor information, if it's changed. + LOG_IF_FAILED(_MoveCursor(options.coordCursor)); + + return S_OK; +} + +// Routine Description: +// - Inverts the selected region on the current screen buffer. +// - Reads the selected area, selection mode, and active screen buffer +// from the global properties and dispatches a GDI invert on the selected text area. +// Because the selection is the responsibility of the terminal, and not the +// host, render nothing. +// Arguments: +// - rect - Rectangle to invert or highlight to make the selection area +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept +{ + return S_OK; +} + +// Routine Description: +// - Write a VT sequence to change the current colors of text. Writes true RGB +// color sequences. +// Arguments: +// - colorForeground: The RGB Color to use to paint the foreground text. +// - colorBackground: The RGB Color to use to paint the background of the text. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_RgbUpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const bool isBold, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) noexcept +{ + const bool fgChanged = colorForeground != _LastFG; + const bool bgChanged = colorBackground != _LastBG; + const bool fgIsDefault = colorForeground == _colorProvider.GetDefaultForeground(); + const bool bgIsDefault = colorBackground == _colorProvider.GetDefaultBackground(); + + // If both the FG and BG should be the defaults, emit a SGR reset. + if ((fgChanged || bgChanged) && fgIsDefault && bgIsDefault) + { + // SGR Reset will also clear out the boldness of the text. + RETURN_IF_FAILED(_SetGraphicsDefault()); + _LastFG = colorForeground; + _LastBG = colorBackground; + _lastWasBold = false; + + // I'm not sure this is possible currently, but if the text is bold, but + // default colors, make sure we bold it. + if (isBold) + { + RETURN_IF_FAILED(_SetGraphicsBoldness(isBold)); + _lastWasBold = isBold; + } + } + else + { + if (_lastWasBold != isBold) + { + RETURN_IF_FAILED(_SetGraphicsBoldness(isBold)); + _lastWasBold = isBold; + } + + WORD wFoundColor = 0; + if (fgChanged) + { + if (fgIsDefault) + { + RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(true)); + } + else if (::FindTableIndex(colorForeground, ColorTable, cColorTable, &wFoundColor)) + { + RETURN_IF_FAILED(_SetGraphicsRendition16Color(wFoundColor, true)); + } + else + { + RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(colorForeground, true)); + } + _LastFG = colorForeground; + } + + if (bgChanged) + { + if (bgIsDefault) + { + RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(false)); + } + else if (::FindTableIndex(colorBackground, ColorTable, cColorTable, &wFoundColor)) + { + RETURN_IF_FAILED(_SetGraphicsRendition16Color(wFoundColor, false)); + } + else + { + RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(colorBackground, false)); + } + _LastBG = colorBackground; + } + } + + return S_OK; +} + +// Routine Description: +// - Write a VT sequence to change the current colors of text. It will try to +// find the colors in the color table that are nearest to the input colors, +// and write those indicies to the pipe. +// Arguments: +// - colorForeground: The RGB Color to use to paint the foreground text. +// - colorBackground: The RGB Color to use to paint the background of the text. +// - ColorTable: An array of colors to find the closest match to. +// - cColorTable: size of the color table. +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +[[nodiscard]] +HRESULT VtEngine::_16ColorUpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const bool isBold, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) noexcept +{ + + const bool fgChanged = colorForeground != _LastFG; + const bool bgChanged = colorBackground != _LastBG; + const bool fgIsDefault = colorForeground == _colorProvider.GetDefaultForeground(); + const bool bgIsDefault = colorBackground == _colorProvider.GetDefaultBackground(); + + // If both the FG and BG should be the defaults, emit a SGR reset. + if ((fgChanged || bgChanged) && fgIsDefault && bgIsDefault) + { + // SGR Reset will also clear out the boldness of the text. + RETURN_IF_FAILED(_SetGraphicsDefault()); + _LastFG = colorForeground; + _LastBG = colorBackground; + _lastWasBold = false; + // I'm not sure this is possible currently, but if the text is bold, but + // default colors, make sure we bold it. + if (isBold) + { + RETURN_IF_FAILED(_SetGraphicsBoldness(isBold)); + _lastWasBold = isBold; + } + } + else + { + if (_lastWasBold != isBold) + { + RETURN_IF_FAILED(_SetGraphicsBoldness(isBold)); + _lastWasBold = isBold; + } + + if (fgChanged) + { + const WORD wNearestFg = ::FindNearestTableIndex(colorForeground, ColorTable, cColorTable); + RETURN_IF_FAILED(_SetGraphicsRendition16Color(wNearestFg, true)); + + _LastFG = colorForeground; + } + + if (bgChanged) + { + const WORD wNearestBg = ::FindNearestTableIndex(colorBackground, ColorTable, cColorTable); + RETURN_IF_FAILED(_SetGraphicsRendition16Color(wNearestBg, false)); + + _LastBG = colorBackground; + } + + } + + return S_OK; +} + +// Routine Description: +// - Draws one line of the buffer to the screen. Writes the characters to the +// pipe. If the characters are outside the ASCII range (0-0x7f), then +// instead writes a '?'. +// This is needed because the Windows internal telnet client implementation +// doesn't know how to handle >ASCII characters. The old telnetd would +// just replace them with '?' characters. If we render the >ASCII +// characters to telnet, it will likely end up drawing them wrong, which +// will make the client appear buggy and broken. +// Arguments: +// - clusters - text and column width data to be written +// - coord - character coordinate target to render within viewport +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::_PaintAsciiBufferLine(std::basic_string_view const clusters, + const COORD coord) noexcept +{ + try + { + RETURN_IF_FAILED(_MoveCursor(coord)); + + std::wstring wstr; + wstr.reserve(clusters.size()); + + short totalWidth = 0; + for (const auto& cluster : clusters) + { + wstr.append(cluster.GetText()); + RETURN_IF_FAILED(ShortAdd(totalWidth, gsl::narrow(cluster.GetColumns()), &totalWidth)); + } + + RETURN_IF_FAILED(VtEngine::_WriteTerminalAscii(wstr)); + + // Update our internal tracker of the cursor's position + _lastText.X += totalWidth; + + return S_OK; + } + CATCH_RETURN(); +} + +// Routine Description: +// - Draws one line of the buffer to the screen. Writes the characters to the +// pipe, encoded in UTF-8. +// Arguments: +// - clusters - text and column widths to be written +// - coord - character coordinate target to render within viewport +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::_PaintUtf8BufferLine(std::basic_string_view const clusters, + const COORD coord) noexcept +{ + if (coord.Y < _virtualTop) + { + return S_OK; + } + + RETURN_IF_FAILED(_MoveCursor(coord)); + + std::wstring unclusteredString; + unclusteredString.reserve(clusters.size()); + short totalWidth = 0; + for (const auto& cluster : clusters) + { + unclusteredString.append(cluster.GetText()); + RETURN_IF_FAILED(ShortAdd(totalWidth, static_cast(cluster.GetColumns()), &totalWidth)); + } + const size_t cchLine = unclusteredString.size(); + + bool foundNonspace = false; + size_t lastNonSpace = 0; + for (size_t i = 0; i < cchLine; i++) + { + if (unclusteredString.at(i) != L'\x20') + { + lastNonSpace = i; + foundNonspace = true; + } + } + // Examples: + // - " ": + // cch = 2, lastNonSpace = 0, foundNonSpace = false + // cch-lastNonSpace = 2 -> good + // cch-lastNonSpace-(0) = 2 -> good + // - "A " + // cch = 2, lastNonSpace = 0, foundNonSpace = true + // cch-lastNonSpace = 2 -> bad + // cch-lastNonSpace-(1) = 1 -> good + // - "AA" + // cch = 2, lastNonSpace = 1, foundNonSpace = true + // cch-lastNonSpace = 1 -> bad + // cch-lastNonSpace-(1) = 0 -> good + const size_t numSpaces = cchLine - lastNonSpace - (foundNonspace ? 1 : 0); + + // Optimizations: + // If there are lots of spaces at the end of the line, we can try to Erase + // Character that number of spaces, then move the cursor forward (to + // where it would be if we had written the spaces) + // An erase character and move right sequence is 8 chars, and possibly 10 + // (if there are at least 10 spaces, 2 digits to print) + // ESC [ %d X ESC [ %d C + // ESC [ %d %d X ESC [ %d %d C + // So we need at least 9 spaces for the optimized sequence to make sense. + // Also, if we already erased the entire display this frame, then + // don't do ANYTHING with erasing at all. + + // Note: We're only doing these optimizations along the UTF-8 path, because + // the inbox telnet client doesn't understand the Erase Character sequence, + // and it uses xterm-ascii. This ensures that xterm and -256color consumers + // get the enhancements, and telnet isn't broken. + const bool optimalToUseECH = numSpaces > ERASE_CHARACTER_STRING_LENGTH; + const bool useEraseChar = (optimalToUseECH) && + (!_newBottomLine) && + (!_clearedAllThisFrame); + + // If we're not using erase char, but we did erase all at the start of the + // frame, don't add spaces at the end. + const bool removeSpaces = (useEraseChar || (_clearedAllThisFrame) || (_newBottomLine)); + const size_t cchActual = removeSpaces ? + (cchLine - numSpaces) : + cchLine; + + const size_t columnsActual = removeSpaces ? + (totalWidth - numSpaces) : + totalWidth; + + // Write the actual text string + std::wstring wstr = std::wstring(unclusteredString.data(), cchActual); + RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(wstr)); + + // Update our internal tracker of the cursor's position. + // See MSFT:20266233 + // If the cursor is at the rightmost column of the terminal, and we write a + // space, the cursor won't actually move to the next cell (which would + // be {0, _lastText.Y++}). The cursor will stay visibly in that last + // cell until then next character is output. + // If in that case, we increment the cursor position here (such that the X + // position would be one past the right of the terminal), when we come + // back through to MoveCursor in the last PaintCursor of the frame, + // we'll determine that we need to emit a \b to put the cursor in the + // right position. This is wrong, and will cause us to move the cursor + // back one character more than we wanted. + if (_lastText.X < _lastViewport.RightInclusive()) + { + _lastText.X += static_cast(columnsActual); + } + + short sNumSpaces; + try + { + sNumSpaces = gsl::narrow(numSpaces); + } + CATCH_RETURN(); + + if (useEraseChar) + { + RETURN_IF_FAILED(_EraseCharacter(sNumSpaces)); + // ECH doesn't actually move the cursor itself. However, we think that + // the cursor *should* be at the end of the area we just erased. Stash + // that position as our new deferred position. If we don't move the + // cursor somewhere else before the end of the frame, we'll move the + // cursor to the deferred position at the end of the frame, or right + // before we need to print new text. + _deferredCursorPos = { _lastText.X + sNumSpaces, _lastText.Y }; + } + else if (_newBottomLine) + { + // If we're on a new line, then we don't need to erase the line. The + // line is already empty. + if (optimalToUseECH) + { + _deferredCursorPos = { _lastText.X + sNumSpaces, _lastText.Y }; + } + else + { + std::wstring spaces = std::wstring(numSpaces, L' '); + RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(spaces)); + + _lastText.X += static_cast(numSpaces); + } + } + + // If we previously though that this was a new bottom line, it certainly + // isn't new any longer. + _newBottomLine = false; + + return S_OK; +} + +// Method Description: +// - Updates the window's title string. Emits the VT sequence to SetWindowTitle. +// Because wintelnet does not understand these sequences by default, we +// don't do anything by default. Other modes can implement if they support +// the sequence. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::_DoUpdateTitle(const std::wstring& /*newTitle*/) noexcept +{ + return S_OK; +} diff --git a/src/renderer/vt/precomp.cpp b/src/renderer/vt/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/renderer/vt/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/renderer/vt/precomp.h b/src/renderer/vt/precomp.h new file mode 100644 index 000000000..d315838c7 --- /dev/null +++ b/src/renderer/vt/precomp.h @@ -0,0 +1,49 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#include + +#ifndef _NTSTATUS_DEFINED +#define _NTSTATUS_DEFINED +typedef _Return_type_success_(return >= 0) long NTSTATUS; +#endif + +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) + +//#include +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth +#define FACILITY_NTWIN32 0x7 +__inline int NTSTATUS_FROM_WIN32(long x) { return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); } + +#define NT_TESTNULL(var) (((var) == nullptr) ? STATUS_NO_MEMORY : STATUS_SUCCESS) +#define NT_TESTNULL_GLE(var) (((var) == nullptr) ? NTSTATUS_FROM_WIN32(GetLastError()) : STATUS_SUCCESS); + +#if defined(DEBUG) || defined(_DEBUG) || defined(DBG) +#define WHEN_DBG(x) x +#else +#define WHEN_DBG(x) +#endif + +// SafeMath +#pragma prefast(push) +#pragma prefast(disable:26071, "Range violation in Intsafe. Not ours.") +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def +#include +#pragma prefast(pop) + diff --git a/src/renderer/vt/sources.inc b/src/renderer/vt/sources.inc new file mode 100644 index 000000000..a4eb8e628 --- /dev/null +++ b/src/renderer/vt/sources.inc @@ -0,0 +1,38 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Renderer for VT +# ------------------------------------- + +# This module provides a rendering engine implementation that +# renders the display to an outgoing VT stream. + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\invalidate.cpp \ + ..\math.cpp \ + ..\paint.cpp \ + ..\state.cpp \ + ..\tracing.cpp \ + ..\WinTelnetEngine.cpp \ + ..\XtermEngine.cpp \ + ..\Xterm256Engine.cpp \ + ..\VtSequences.cpp \ + +INCLUDES = \ + ..; \ + ..\..\..\inc; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp new file mode 100644 index 000000000..2fa50643d --- /dev/null +++ b/src/renderer/vt/state.cpp @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "vtrenderer.hpp" +#include "../../inc/conattrs.hpp" +#include "../../types/inc/convert.hpp" + +// For _vcprintf +#include +#include + +#pragma hdrstop + +using namespace Microsoft::Console; +using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; + +const COORD VtEngine::INVALID_COORDS = {-1, -1}; + +// Routine Description: +// - Creates a new VT-based rendering engine +// - NOTE: Will throw if initialization failure. Caller must catch. +// Arguments: +// - +// Return Value: +// - An instance of a Renderer. +VtEngine::VtEngine(_In_ wil::unique_hfile pipe, + const IDefaultColorProvider& colorProvider, + const Viewport initialViewport) : + RenderEngineBase(), + _hFile(std::move(pipe)), + _colorProvider(colorProvider), + _LastFG(INVALID_COLOR), + _LastBG(INVALID_COLOR), + _lastWasBold(false), + _lastViewport(initialViewport), + _invalidRect(Viewport::Empty()), + _fInvalidRectUsed(false), + _lastRealCursor({0}), + _lastText({0}), + _scrollDelta({0}), + _quickReturn(false), + _clearedAllThisFrame(false), + _cursorMoved(false), + _resized(false), + _suppressResizeRepaint(true), + _virtualTop(0), + _circled(false), + _firstPaint(true), + _skipCursor(false), + _pipeBroken(false), + _exitResult{ S_OK }, + _terminalOwner{ nullptr }, + _newBottomLine{ false }, + _deferredCursorPos{ INVALID_COORDS }, + _trace {} +{ +#ifndef UNIT_TESTING + // When unit testing, we can instantiate a VtEngine without a pipe. + THROW_HR_IF(E_HANDLE, _hFile.get() == INVALID_HANDLE_VALUE); +#else + // member is only defined when UNIT_TESTING is. + _usingTestCallback = false; +#endif +} + +// Method Description: +// - Writes the characters to our file handle. If we're building the unit tests, +// we can instead write to the test callback, in order to avoid needing to +// set up pipes and threads for unit tests. +// Arguments: +// - str: The buffer to write to the pipe. Might have nulls in it. +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::_Write(std::string_view const str) noexcept +{ + _trace.TraceString(str); +#ifdef UNIT_TESTING + if (_usingTestCallback) + { + RETURN_LAST_ERROR_IF(!_pfnTestCallback(str.data(), str.size())); + return S_OK; + } +#endif + + try + { + _buffer.append(str); + + return S_OK; + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT VtEngine::_Flush() noexcept +{ +#ifdef UNIT_TESTING + if (_hFile.get() == INVALID_HANDLE_VALUE) + { + // Do not flush during Unit Testing because we won't have a valid file. + return S_OK; + } +#endif + + if (!_pipeBroken) + { + bool fSuccess = !!WriteFile(_hFile.get(), _buffer.data(), static_cast(_buffer.size()), nullptr, nullptr); + _buffer.clear(); + if (!fSuccess) + { + _exitResult = HRESULT_FROM_WIN32(GetLastError()); + _pipeBroken = true; + if (_terminalOwner) + { + _terminalOwner->CloseOutput(); + } + return _exitResult; + } + } + + return S_OK; +} + +// Method Description: +// - Wrapper for ITerminalOutputConnection. See _Write. +[[nodiscard]] +HRESULT VtEngine::WriteTerminalUtf8(const std::string& str) noexcept +{ + return _Write(str); +} + +// Method Description: +// - Writes a wstring to the tty, encoded as full utf-8. This is one +// implementation of the WriteTerminalW method. +// Arguments: +// - wstr - wstring of text to be written +// Return Value: +// - S_OK or suitable HRESULT error from either conversion or writing pipe. +[[nodiscard]] +HRESULT VtEngine::_WriteTerminalUtf8(const std::wstring& wstr) noexcept +{ + try + { + const auto converted = ConvertToA(CP_UTF8, wstr); + return _Write(converted); + } + CATCH_RETURN(); +} + +// Method Description: +// - Writes a wstring to the tty, encoded as "utf-8" where characters that are +// outside the ASCII range are encoded as '?' +// This mainly exists to maintain compatability with the inbox telnet client. +// This is one implementation of the WriteTerminalW method. +// Arguments: +// - wstr - wstring of text to be written +// Return Value: +// - S_OK or suitable HRESULT error from writing pipe. +[[nodiscard]] +HRESULT VtEngine::_WriteTerminalAscii(const std::wstring& wstr) noexcept +{ + const size_t cchActual = wstr.length(); + + std::string needed; + needed.reserve(wstr.size()); + + for (const auto& wch : wstr) + { + // We're explicitly replacing characters outside ASCII with a ? because + // that's what telnet wants. + needed.push_back((wch > L'\x7f') ? '?' : static_cast(wch)); + } + + return _Write(needed); +} + +// Method Description: +// - Helper for calling _Write with a string for formatting a sequence. Used +// extensively by VtSequences.cpp +// Arguments: +// - pFormat: the pointer to the string to write to the pipe. +// - ...: a va_list of args to format the string with. +// Return Value: +// - S_OK, E_INVALIDARG for a invalid format string, or suitable HRESULT error +// from writing pipe. +[[nodiscard]] +HRESULT VtEngine::_WriteFormattedString(const std::string* const pFormat, ...) noexcept +{ + + HRESULT hr = E_FAIL; + va_list argList; + va_start(argList, pFormat); + + int cchNeeded = _scprintf(pFormat->c_str(), argList); + // -1 is the _scprintf error case https://msdn.microsoft.com/en-us/library/t32cf9tb.aspx + if (cchNeeded > -1) + { + wistd::unique_ptr psz = wil::make_unique_nothrow(cchNeeded + 1); + RETURN_IF_NULL_ALLOC(psz); + + int cchWritten = _vsnprintf_s(psz.get(), cchNeeded + 1, cchNeeded, pFormat->c_str(), argList); + hr = _Write({ psz.get(), gsl::narrow(cchWritten) }); + } + else + { + hr = E_INVALIDARG; + } + + va_end(argList); + return hr; +} + +// Method Description: +// - This method will update the active font on the current device context +// Does nothing for vt, the font is handed by the terminal. +// Arguments: +// - FontDesired - reference to font information we should use while instantiating a font. +// - Font - reference to font information where the chosen font information will be populated. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT VtEngine::UpdateFont(const FontInfoDesired& /*pfiFontDesired*/, + _Out_ FontInfo& /*pfiFont*/) noexcept +{ + return S_OK; +} + +// Method Description: +// - This method will modify the DPI we're using for scaling calculations. +// Does nothing for vt, the dpi is handed by the terminal. +// Arguments: +// - iDpi - The Dots Per Inch to use for scaling. We will use this relative to +// the system default DPI defined in Windows headers as a constant. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT VtEngine::UpdateDpi(const int /*iDpi*/) noexcept +{ + return S_OK; +} + +// Method Description: +// - This method will update our internal reference for how big the viewport is. +// If the viewport has changed size, then we'll need to send an update to +// the terminal. +// Arguments: +// - srNewViewport - The bounds of the new viewport. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT VtEngine::UpdateViewport(const SMALL_RECT srNewViewport) noexcept +{ + HRESULT hr = S_OK; + const Viewport oldView = _lastViewport; + const Viewport newView = Viewport::FromInclusive(srNewViewport); + + _lastViewport = newView; + + if ((oldView.Height() != newView.Height()) || (oldView.Width() != newView.Width())) + { + // Don't emit a resize event if we've requested it be suppressed + if (!_suppressResizeRepaint) + { + hr = _ResizeWindow(newView.Width(), newView.Height()); + } + } + + // See MSFT:19408543 + // Always clear the suppression request, even if the new size was the same + // as the last size. We're always going to get a UpdateViewport call + // for our first frame. However, we start with _suppressResizeRepaint set, + // to prevent that first UpdateViewport call from emitting our size. + // If we only clear the flag when the new viewport is different, this can + // lead to the first _actual_ resize being suppressed. + _suppressResizeRepaint = false; + + if (SUCCEEDED(hr)) + { + // Viewport is smaller now - just update it all. + if ( oldView.Height() > newView.Height() || oldView.Width() > newView.Width() ) + { + hr = InvalidateAll(); + } + else + { + // At least one of the directions grew. + // First try and add everything to the right of the old viewport, + // then everything below where the old viewport ended. + if (oldView.Width() < newView.Width()) + { + short left = oldView.RightExclusive(); + short top = 0; + short right = newView.RightInclusive(); + short bottom = oldView.BottomInclusive(); + Viewport rightOfOldViewport = Viewport::FromInclusive({left, top, right, bottom}); + hr = _InvalidCombine(rightOfOldViewport); + } + if (SUCCEEDED(hr) && oldView.Height() < newView.Height()) + { + short left = 0; + short top = oldView.BottomExclusive(); + short right = newView.RightInclusive(); + short bottom = newView.BottomInclusive(); + Viewport belowOldViewport = Viewport::FromInclusive({left, top, right, bottom}); + hr = _InvalidCombine(belowOldViewport); + + } + } + } + _resized = true; + return hr; +} + +// Method Description: +// - This method will figure out what the new font should be given the starting font information and a DPI. +// - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match. +// - NOTE: It is left up to the underling rendering system to choose the nearest font. Please ask for the font dimensions if they are required using the interface. Do not use the size you requested with this structure. +// - If the intent is to immediately turn around and use this font, pass the optional handle parameter and use it immediately. +// Does nothing for vt, the font is handed by the terminal. +// Arguments: +// - FontDesired - reference to font information we should use while instantiating a font. +// - Font - reference to font information where the chosen font information will be populated. +// - iDpi - The DPI we will have when rendering +// Return Value: +// - S_FALSE: This is unsupported by the VT Renderer and should use another engine's value. +[[nodiscard]] +HRESULT VtEngine::GetProposedFont(const FontInfoDesired& /*pfiFontDesired*/, + _Out_ FontInfo& /*pfiFont*/, + const int /*iDpi*/) noexcept +{ + return S_FALSE; +} + +// Method Description: +// - Retrieves the current pixel size of the font we have selected for drawing. +// Arguments: +// - pFontSize - recieves the current X by Y size of the font. +// Return Value: +// - S_FALSE: This is unsupported by the VT Renderer and should use another engine's value. +[[nodiscard]] +HRESULT VtEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +{ + *pFontSize = COORD({1, 1}); + return S_FALSE; +} + +// Method Description: +// - Sets the test callback for this instance. Instead of rendering to a pipe, +// this instance will instead render to a callback for testing. +// Arguments: +// - pfn: a callback to call instead of writing to the pipe. +// Return Value: +// - +void VtEngine::SetTestCallback(_In_ std::function pfn) +{ + +#ifdef UNIT_TESTING + + _pfnTestCallback = pfn; + _usingTestCallback = true; + +#else + THROW_HR(E_FAIL); +#endif + +} + +// Method Description: +// - Returns true if the entire viewport has been invalidated. That signals we +// should use a VT Clear Screen sequence as an optimization. +// Arguments: +// - +// Return Value: +// - true if the entire viewport has been invalidated +bool VtEngine::_AllIsInvalid() const +{ + return _lastViewport == _invalidRect; +} + +// Method Description: +// - Prevent the renderer from emitting output on the next resize. This prevents +// the host from echoing a resize to the terminal that requested it. +// Arguments: +// - +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::SuppressResizeRepaint() noexcept +{ + _suppressResizeRepaint = true; + return S_OK; +} + +// Method Description: +// - "Inherit" the cursor at the given position. We won't need to move it +// anywhere, so update where we last thought the cursor was. +// Also update our "virtual top", indicating where should clip all updates to +// (we don't want to paint the empty region above the inherited cursor). +// Also ignore the next InvalidateCursor call. +// Arguments: +// - coordCursor: The cursor position to inherit from. +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT VtEngine::InheritCursor(const COORD coordCursor) noexcept +{ + _virtualTop = coordCursor.Y; + _lastText = coordCursor; + _skipCursor = true; + // Prevent us from clearing the entire viewport on the first paint + _firstPaint = false; + return S_OK; +} + +void VtEngine::SetTerminalOwner(Microsoft::Console::ITerminalOwner* const terminalOwner) +{ + _terminalOwner = terminalOwner; +} + +// Method Description: +// - sends a sequence to request the end terminal to tell us the +// cursor position. The terminal will reply back on the vt input handle. +// Flushes the buffer as well, to make sure the request is sent to the terminal. +// Arguments: +// - +// Return Value: +// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write. +HRESULT VtEngine::RequestCursor() noexcept +{ + RETURN_IF_FAILED(_RequestCursor()); + RETURN_IF_FAILED(_Flush()); + return S_OK; +} diff --git a/src/renderer/vt/tracing.cpp b/src/renderer/vt/tracing.cpp new file mode 100644 index 000000000..cfe6deb68 --- /dev/null +++ b/src/renderer/vt/tracing.cpp @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "tracing.hpp" +#include + +TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVtRendererTraceProvider, + "Microsoft.Windows.Console.Render.VtEngine", + // tl:{c9ba2a95-d3ca-5e19-2bd6-776a0910cb9d} + (0xc9ba2a95, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d), + TraceLoggingOptionMicrosoftTelemetry()); + +using namespace Microsoft::Console::VirtualTerminal; +using namespace Microsoft::Console::Types; + +RenderTracing::RenderTracing() +{ + #ifndef UNIT_TESTING + TraceLoggingRegister(g_hConsoleVtRendererTraceProvider); + #endif UNIT_TESTING +} + +RenderTracing::~RenderTracing() +{ + #ifndef UNIT_TESTING + TraceLoggingUnregister(g_hConsoleVtRendererTraceProvider); + #endif UNIT_TESTING +} + +// Function Description: +// - Convert the string to only have printable characters in it. Control +// characters are converted to hat notation, spaces are converted to "SPC" +// (to be able to see them at the end of a string), and DEL is written as +// "\x7f". +// Arguments: +// - inString: The string to convert +// Return Value: +// - a string with only printable characters in it. +std::string toPrintableString(const std::string_view& inString) +{ + std::string retval = ""; + for (size_t i = 0; i < inString.length(); i++) + { + unsigned char c = inString[i]; + if (c < '\x20' && c < '\x7f') + { + retval += "^"; + char actual = (c + 0x40); + retval += std::string(1, actual); + } + else if (c == '\x7f') + { + retval += "\\x7f"; + } + else if (c == '\x20') + { + retval += "SPC"; + } + else + { + retval += std::string(1, c); + } + } + return retval; +} +void RenderTracing::TraceString(const std::string_view& instr) const +{ + #ifndef UNIT_TESTING + const std::string _seq = toPrintableString(instr); + const char* const seq = _seq.c_str(); + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceString", + TraceLoggingString(seq), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(instr); + #endif UNIT_TESTING +} + +std::string _ViewportToString(const Viewport& view) +{ + std::stringstream ss; + ss << "{LT:("; + ss << view.Left(); + ss << ","; + ss << view.Top(); + ss << ") RB:("; + ss << view.RightInclusive(); + ss << ","; + ss << view.BottomInclusive(); + ss << ") ["; + ss << view.Width(); + ss << "x"; + ss << view.Height(); + ss << "]}"; + std::string myString = ""; + const auto s = ss.str(); + myString += s; + return myString; +} + +std::string _CoordToString(const COORD& c) +{ + std::stringstream ss; + ss << "{"; + ss << c.X; + ss << ","; + ss << c.Y; + ss << "}"; + const auto s = ss.str(); + // Make sure you actually place this value in a local after calling, don't + // just inline it to _CoordToString().c_str() + return s; +} + +void RenderTracing::TraceInvalidate(const Viewport invalidRect) const +{ + #ifndef UNIT_TESTING + const auto invalidatedStr = _ViewportToString(invalidRect); + const auto invalidated = invalidatedStr.c_str(); + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceInvalidate", + TraceLoggingString(invalidated), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(invalidRect); + #endif UNIT_TESTING +} + +void RenderTracing::TraceInvalidateAll(const Viewport viewport) const +{ + #ifndef UNIT_TESTING + const auto invalidatedStr = _ViewportToString(viewport); + const auto invalidatedAll = invalidatedStr.c_str(); + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceInvalidateAll", + TraceLoggingString(invalidatedAll), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(viewport); + #endif UNIT_TESTING +} + +void RenderTracing::TraceTriggerCircling(const bool newFrame) const +{ + #ifndef UNIT_TESTING + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceTriggerCircling", + TraceLoggingBool(newFrame), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(newFrame); + #endif UNIT_TESTING +} + +void RenderTracing::TraceStartPaint(const bool quickReturn, + const bool invalidRectUsed, + const Microsoft::Console::Types::Viewport invalidRect, + const Microsoft::Console::Types::Viewport lastViewport, + const COORD scrollDelt, + const bool cursorMoved) const +{ + #ifndef UNIT_TESTING + const auto invalidatedStr = _ViewportToString(invalidRect); + const auto invalidated = invalidatedStr.c_str(); + const auto lastViewStr = _ViewportToString(lastViewport); + const auto lastView = lastViewStr.c_str(); + const auto scrollDeltaStr = _CoordToString(scrollDelt); + const auto scrollDelta = scrollDeltaStr.c_str(); + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceStartPaint", + TraceLoggingBool(quickReturn), + TraceLoggingBool(invalidRectUsed), + TraceLoggingString(invalidated), + TraceLoggingString(lastView), + TraceLoggingString(scrollDelta), + TraceLoggingBool(cursorMoved), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(quickReturn); + UNREFERENCED_PARAMETER(invalidRectUsed); + UNREFERENCED_PARAMETER(invalidRect); + UNREFERENCED_PARAMETER(lastViewport); + UNREFERENCED_PARAMETER(scrollDelt); + UNREFERENCED_PARAMETER(cursorMoved); + #endif UNIT_TESTING +} + +void RenderTracing::TraceEndPaint() const +{ + #ifndef UNIT_TESTING + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceEndPaint", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + #endif UNIT_TESTING +} + + +void RenderTracing::TraceLastText(const COORD lastTextPos) const +{ + #ifndef UNIT_TESTING + const auto lastTextStr = _CoordToString(lastTextPos); + const auto lastText = lastTextStr.c_str(); + TraceLoggingWrite(g_hConsoleVtRendererTraceProvider, + "VtEngine_TraceLastText", + TraceLoggingString(lastText), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE)); + #else + UNREFERENCED_PARAMETER(lastTextPos); + #endif UNIT_TESTING +} diff --git a/src/renderer/vt/tracing.hpp b/src/renderer/vt/tracing.hpp new file mode 100644 index 000000000..af1594d99 --- /dev/null +++ b/src/renderer/vt/tracing.hpp @@ -0,0 +1,43 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- tracing.hpp + +Abstract: +- This module is used for recording tracing/debugging information to the telemetry ETW channel +--*/ + +#pragma once +#include +#include +#include +#include +#include +#include "../../types/inc/Viewport.hpp" + +TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVtRendererTraceProvider); + +namespace Microsoft::Console::VirtualTerminal +{ + class RenderTracing final + { + public: + + RenderTracing(); + ~RenderTracing(); + void TraceString(const std::string_view& str) const; + void TraceInvalidate(const Microsoft::Console::Types::Viewport view) const; + void TraceLastText(const COORD lastText) const; + void TraceInvalidateAll(const Microsoft::Console::Types::Viewport view) const; + void TraceTriggerCircling(const bool newFrame) const; + void TraceStartPaint(const bool quickReturn, + const bool invalidRectUsed, + const Microsoft::Console::Types::Viewport invalidRect, + const Microsoft::Console::Types::Viewport lastViewport, + const COORD scrollDelta, + const bool cursorMoved) const; + void TraceEndPaint() const; + }; +} diff --git a/src/renderer/vt/ut_lib/sources b/src/renderer/vt/ut_lib/sources new file mode 100644 index 000000000..583035792 --- /dev/null +++ b/src/renderer/vt/ut_lib/sources @@ -0,0 +1,16 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderVt.Unittest +TARGETTYPE = LIBRARY + +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING diff --git a/src/renderer/vt/ut_lib/vt.unittest.vcxproj b/src/renderer/vt/ut_lib/vt.unittest.vcxproj new file mode 100644 index 000000000..8892d23d8 --- /dev/null +++ b/src/renderer/vt/ut_lib/vt.unittest.vcxproj @@ -0,0 +1,25 @@ + + + + + + + INLINE_TEST_METHOD_MARKUP;UNIT_TESTING;%(PreprocessorDefinitions) + + + + StaticLibrary + + + + {990F2657-8580-4828-943F-5DD657D11843} + Win32Proj + vt.unittest + RendererVt.unittest + ConRenderVt.unittest + + + + + + \ No newline at end of file diff --git a/src/renderer/vt/vt-renderer-common.vcxproj b/src/renderer/vt/vt-renderer-common.vcxproj new file mode 100644 index 000000000..d06c88894 --- /dev/null +++ b/src/renderer/vt/vt-renderer-common.vcxproj @@ -0,0 +1,31 @@ + + + + + + + + + Create + + + + + + + + + + + + + + + + + diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp new file mode 100644 index 000000000..986905104 --- /dev/null +++ b/src/renderer/vt/vtrenderer.hpp @@ -0,0 +1,280 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- VtRenderer.hpp + +Abstract: +- This is the definition of the VT specific implementation of the renderer. + +Author(s): +- Michael Niksa (MiNiksa) 24-Jul-2017 +- Mike Griese (migrie) 01-Sept-2017 +--*/ + +#pragma once + +#include "../inc/RenderEngineBase.hpp" +#include "../../inc/IDefaultColorProvider.hpp" +#include "../../inc/ITerminalOutputConnection.hpp" +#include "../../inc/ITerminalOwner.hpp" +#include "../../types/inc/Viewport.hpp" +#include "tracing.hpp" +#include +#include + +namespace Microsoft::Console::Render +{ + class VtEngine : public RenderEngineBase, public Microsoft::Console::ITerminalOutputConnection + { + public: + // See _PaintUtf8BufferLine for explanation of this value. + static const size_t ERASE_CHARACTER_STRING_LENGTH = 8; + static const COORD INVALID_COORDS; + + VtEngine(_In_ wil::unique_hfile hPipe, + const Microsoft::Console::IDefaultColorProvider& colorProvider, + const Microsoft::Console::Types::Viewport initialViewport); + + virtual ~VtEngine() override = default; + + [[nodiscard]] + HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] + virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0; + [[nodiscard]] + HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; + [[nodiscard]] + HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; + [[nodiscard]] + HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override; + [[nodiscard]] + HRESULT InvalidateAll() noexcept override; + [[nodiscard]] + HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] + HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; + + [[nodiscard]] + virtual HRESULT StartPaint() noexcept override; + [[nodiscard]] + virtual HRESULT EndPaint() noexcept override; + [[nodiscard]] + virtual HRESULT Present() noexcept override; + + [[nodiscard]] + virtual HRESULT ScrollFrame() noexcept = 0; + + [[nodiscard]] + HRESULT PaintBackground() noexcept override; + [[nodiscard]] + virtual HRESULT PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool trimLeft) noexcept override; + [[nodiscard]] + HRESULT PaintBufferGridLines(const GridLines lines, + const COLORREF color, + const size_t cchLine, + const COORD coordTarget) noexcept override; + [[nodiscard]] + HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + + [[nodiscard]] + HRESULT PaintCursor(const CursorOptions& options) noexcept override; + + [[nodiscard]] + virtual HRESULT UpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + const bool isSettingDefaultBrushes) noexcept = 0; + [[nodiscard]] + HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, + _Out_ FontInfo& pfiFontInfo) noexcept override; + [[nodiscard]] + HRESULT UpdateDpi(const int iDpi) noexcept override; + [[nodiscard]] + HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + + [[nodiscard]] + HRESULT GetProposedFont(const FontInfoDesired& FontDesired, + _Out_ FontInfo& Font, + const int iDpi) noexcept override; + + SMALL_RECT GetDirtyRectInChars() override; + [[nodiscard]] + HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; + [[nodiscard]] + HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; + + [[nodiscard]] + HRESULT SuppressResizeRepaint() noexcept; + + [[nodiscard]] + HRESULT RequestCursor() noexcept; + [[nodiscard]] + HRESULT InheritCursor(const COORD coordCursor) noexcept; + + [[nodiscard]] + HRESULT WriteTerminalUtf8(const std::string& str) noexcept; + + [[nodiscard]] + virtual HRESULT WriteTerminalW(const std::wstring& str) noexcept = 0; + + void SetTerminalOwner(Microsoft::Console::ITerminalOwner* const terminalOwner); + + protected: + wil::unique_hfile _hFile; + std::string _buffer; + + const Microsoft::Console::IDefaultColorProvider& _colorProvider; + + COLORREF _LastFG; + COLORREF _LastBG; + bool _lastWasBold; + + Microsoft::Console::Types::Viewport _lastViewport; + Microsoft::Console::Types::Viewport _invalidRect; + + bool _fInvalidRectUsed; + COORD _lastRealCursor; + COORD _lastText; + COORD _scrollDelta; + + bool _quickReturn; + bool _clearedAllThisFrame; + bool _cursorMoved; + bool _resized; + + bool _suppressResizeRepaint; + + SHORT _virtualTop; + bool _circled; + bool _firstPaint; + bool _skipCursor; + bool _newBottomLine; + COORD _deferredCursorPos; + + bool _pipeBroken; + HRESULT _exitResult; + Microsoft::Console::ITerminalOwner* _terminalOwner; + + Microsoft::Console::VirtualTerminal::RenderTracing _trace; + + [[nodiscard]] + HRESULT _Write(std::string_view const str) noexcept; + [[nodiscard]] + HRESULT _WriteFormattedString(const std::string* const pFormat, ...) noexcept; + [[nodiscard]] + HRESULT _Flush() noexcept; + + void _OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT* const pRectToOr) const; + [[nodiscard]] + HRESULT _InvalidCombine(const Microsoft::Console::Types::Viewport invalid) noexcept; + [[nodiscard]] + HRESULT _InvalidOffset(const COORD* const ppt) noexcept; + [[nodiscard]] + HRESULT _InvalidRestrict() noexcept; + bool _AllIsInvalid() const; + + [[nodiscard]] + HRESULT _StopCursorBlinking() noexcept; + [[nodiscard]] + HRESULT _StartCursorBlinking() noexcept; + [[nodiscard]] + HRESULT _HideCursor() noexcept; + [[nodiscard]] + HRESULT _ShowCursor() noexcept; + [[nodiscard]] + HRESULT _EraseLine() noexcept; + [[nodiscard]] + HRESULT _InsertDeleteLine(const short sLines, const bool fInsertLine) noexcept; + [[nodiscard]] + HRESULT _DeleteLine(const short sLines) noexcept; + [[nodiscard]] + HRESULT _InsertLine(const short sLines) noexcept; + [[nodiscard]] + HRESULT _CursorForward(const short chars) noexcept; + [[nodiscard]] + HRESULT _EraseCharacter(const short chars) noexcept; + [[nodiscard]] + HRESULT _CursorPosition(const COORD coord) noexcept; + [[nodiscard]] + HRESULT _CursorHome() noexcept; + [[nodiscard]] + HRESULT _ClearScreen() noexcept; + [[nodiscard]] + HRESULT _ChangeTitle(const std::string& title) noexcept; + [[nodiscard]] + HRESULT _SetGraphicsRendition16Color(const WORD wAttr, + const bool fIsForeground) noexcept; + [[nodiscard]] + HRESULT _SetGraphicsRenditionRGBColor(const COLORREF color, + const bool fIsForeground) noexcept; + [[nodiscard]] + HRESULT _SetGraphicsRenditionDefaultColor(const bool fIsForeground) noexcept; + + [[nodiscard]] + HRESULT _SetGraphicsBoldness(const bool isBold) noexcept; + + [[nodiscard]] + HRESULT _SetGraphicsDefault() noexcept; + + [[nodiscard]] + HRESULT _ResizeWindow(const short sWidth, const short sHeight) noexcept; + + [[nodiscard]] + HRESULT _BeginUnderline() noexcept; + + [[nodiscard]] + HRESULT _EndUnderline() noexcept; + + [[nodiscard]] + HRESULT _RequestCursor() noexcept; + + [[nodiscard]] + virtual HRESULT _MoveCursor(const COORD coord) noexcept = 0; + [[nodiscard]] + HRESULT _RgbUpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const bool isBold, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) noexcept; + [[nodiscard]] + HRESULT _16ColorUpdateDrawingBrushes(const COLORREF colorForeground, + const COLORREF colorBackground, + const bool isBold, + _In_reads_(cColorTable) const COLORREF* const ColorTable, + const WORD cColorTable) noexcept; + + bool _WillWriteSingleChar() const; + + [[nodiscard]] + HRESULT _PaintUtf8BufferLine(std::basic_string_view const clusters, + const COORD coord) noexcept; + + [[nodiscard]] + HRESULT _PaintAsciiBufferLine(std::basic_string_view const clusters, + const COORD coord) noexcept; + + [[nodiscard]] + HRESULT _WriteTerminalUtf8(const std::wstring& str) noexcept; + [[nodiscard]] + HRESULT _WriteTerminalAscii(const std::wstring& str) noexcept; + + [[nodiscard]] + virtual HRESULT _DoUpdateTitle(const std::wstring& newTitle) noexcept override; + + /////////////////////////// Unit Testing Helpers /////////////////////////// + #ifdef UNIT_TESTING + std::function _pfnTestCallback; + bool _usingTestCallback; + + friend class VtRendererTest; + #endif + + void SetTestCallback(_In_ std::function pfn); + + }; +} diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp new file mode 100644 index 000000000..e7d8870b9 --- /dev/null +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "WddmConRenderer.hpp" + +#include "main.h" + +#pragma hdrstop + +// +// Default non-bright white. +// + +#define DEFAULT_COLOR_ATTRIBUTE (0xC) + +#define DEFAULT_FONT_WIDTH (8) +#define DEFAULT_FONT_HEIGHT (12) + +using namespace Microsoft::Console::Render; + +WddmConEngine::WddmConEngine() : + RenderEngineBase(), + _hWddmConCtx(INVALID_HANDLE_VALUE), + _displayHeight(0), + _displayWidth(0), + _displayState(nullptr), + _currentLegacyColorAttribute(DEFAULT_COLOR_ATTRIBUTE) +{ + +} + +void WddmConEngine::FreeResources(ULONG displayHeight) +{ + if (_displayState) + { + for (ULONG i = 0; i < displayHeight; i++) + { + if (_displayState[i]) + { + if (_displayState[i]->Old) + { + free(_displayState[i]->Old); + } + if (_displayState[i]->New) + { + free(_displayState[i]->New); + } + + free(_displayState[i]); + } + } + + free(_displayState); + } + + if (_hWddmConCtx != INVALID_HANDLE_VALUE) + { + WDDMConDestroy(_hWddmConCtx); + _hWddmConCtx = INVALID_HANDLE_VALUE; + } +} + +WddmConEngine::~WddmConEngine() +{ + FreeResources(_displayHeight); +} + +[[nodiscard]] +HRESULT WddmConEngine::Initialize() noexcept +{ + HRESULT hr; + RECT DisplaySize; + CD_IO_DISPLAY_SIZE DisplaySizeIoctl; + + if (_hWddmConCtx == INVALID_HANDLE_VALUE) + { + hr = WDDMConCreate(&_hWddmConCtx); + + if (SUCCEEDED(hr)) + { + hr = WDDMConGetDisplaySize(_hWddmConCtx, &DisplaySizeIoctl); + + if (SUCCEEDED(hr)) + { + DisplaySize.top = 0; + DisplaySize.left = 0; + DisplaySize.bottom = (LONG)DisplaySizeIoctl.Height; + DisplaySize.right = (LONG)DisplaySizeIoctl.Width; + + _displayState = (PCD_IO_ROW_INFORMATION *)calloc(DisplaySize.bottom, sizeof(PCD_IO_ROW_INFORMATION)); + + if (_displayState != nullptr) + { + for (LONG i = 0; i < DisplaySize.bottom; i++) + { + _displayState[i] = (PCD_IO_ROW_INFORMATION)calloc(1, sizeof(CD_IO_ROW_INFORMATION)); + if (_displayState[i] == nullptr) + { + hr = E_OUTOFMEMORY; + + break; + } + + _displayState[i]->Index = (SHORT)i; + _displayState[i]->Old = (PCD_IO_CHARACTER)calloc(DisplaySize.right, sizeof(CD_IO_CHARACTER)); + _displayState[i]->New = (PCD_IO_CHARACTER)calloc(DisplaySize.right, sizeof(CD_IO_CHARACTER)); + + if (_displayState[i]->Old == nullptr || _displayState[i]->New == nullptr) + { + hr = E_OUTOFMEMORY; + + break; + } + } + + if (SUCCEEDED(hr)) + { + _displayHeight = DisplaySize.bottom; + _displayWidth = DisplaySize.right; + } + else + { + FreeResources(DisplaySize.bottom); + } + } + else + { + WDDMConDestroy(_hWddmConCtx); + + hr = E_OUTOFMEMORY; + } + } + else + { + WDDMConDestroy(_hWddmConCtx); + } + } + } + else + { + hr = E_HANDLE; + } + + return hr; +} + +bool WddmConEngine::IsInitialized() +{ + return _hWddmConCtx != INVALID_HANDLE_VALUE; +} + +[[nodiscard]] +HRESULT WddmConEngine::Enable() noexcept +{ + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + return WDDMConEnableDisplayAccess((PHANDLE)_hWddmConCtx, TRUE); +} + +[[nodiscard]] +HRESULT WddmConEngine::Disable() noexcept +{ + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + return WDDMConEnableDisplayAccess((PHANDLE)_hWddmConCtx, FALSE); +} + +[[nodiscard]] +HRESULT WddmConEngine::Invalidate(const SMALL_RECT* const /*psrRegion*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateSystem(const RECT* const /*prcDirtyClient*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateSelection(const std::vector& /*rectangles*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateScroll(const COORD* const /*pcoordDelta*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateAll() noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::InvalidateCircling(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +[[nodiscard]] +HRESULT WddmConEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept +{ + *pForcePaint = false; + return S_FALSE; +} + +[[nodiscard]] +HRESULT WddmConEngine::StartPaint() noexcept +{ + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + return WDDMConBeginUpdateDisplayBatch(_hWddmConCtx); +} + +[[nodiscard]] +HRESULT WddmConEngine::EndPaint() noexcept +{ + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + return WDDMConEndUpdateDisplayBatch(_hWddmConCtx); +} + +// Routine Description: +// - Used to perform longer running presentation steps outside the lock so the other threads can continue. +// - Not currently used by WddmConEngine. +// Arguments: +// - +// Return Value: +// - S_FALSE since we do nothing. + +[[nodiscard]] +HRESULT WddmConEngine::Present() noexcept +{ + return S_FALSE; +} + +[[nodiscard]] +HRESULT WddmConEngine::ScrollFrame() noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::PaintBackground() noexcept +{ + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + + PCD_IO_CHARACTER OldChar; + PCD_IO_CHARACTER NewChar; + + for (LONG rowIndex = 0; rowIndex < _displayHeight; rowIndex++) + { + for (LONG colIndex = 0; colIndex < _displayWidth; colIndex++) + { + OldChar = &_displayState[rowIndex]->Old[colIndex]; + NewChar = &_displayState[rowIndex]->New[colIndex]; + + OldChar->Character = NewChar->Character; + OldChar->Atribute = NewChar->Atribute; + + NewChar->Character = L' '; + NewChar->Atribute = 0x0; + } + } + + + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool /*trimLeft*/) noexcept +{ + try + { + RETURN_IF_HANDLE_INVALID(_hWddmConCtx); + + PCD_IO_CHARACTER OldChar; + PCD_IO_CHARACTER NewChar; + + for (size_t i = 0; i < clusters.size() && i < (size_t)_displayWidth; i++) + { + OldChar = &_displayState[coord.Y]->Old[coord.X + i]; + NewChar = &_displayState[coord.Y]->New[coord.X + i]; + + OldChar->Character = NewChar->Character; + OldChar->Atribute = NewChar->Atribute; + + NewChar->Character = clusters.at(i).GetTextAsSingle(); + NewChar->Atribute = _currentLegacyColorAttribute; + } + + return WDDMConUpdateDisplay(_hWddmConCtx, _displayState[coord.Y], FALSE); + } + CATCH_RETURN(); +} + +[[nodiscard]] +HRESULT WddmConEngine::PaintBufferGridLines(GridLines const /*lines*/, + COLORREF const /*color*/, + size_t const /*cchLine*/, + COORD const /*coordTarget*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::PaintCursor(const IRenderEngine::CursorOptions& /*options*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::UpdateDrawingBrushes(COLORREF const /*colorForeground*/, + COLORREF const /*colorBackground*/, + const WORD legacyColorAttribute, + const bool /*isBold*/, + bool const /*isSettingDefaultBrushes*/) noexcept +{ + _currentLegacyColorAttribute = legacyColorAttribute; + + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::UpdateFont(const FontInfoDesired& /*pfiFontInfoDesired*/, FontInfo& fiFontInfo) noexcept +{ + COORD coordSize = {0}; + LOG_IF_FAILED(GetFontSize(&coordSize)); + + fiFontInfo.SetFromEngine(fiFontInfo.GetFaceName(), + fiFontInfo.GetFamily(), + fiFontInfo.GetWeight(), + fiFontInfo.IsTrueTypeFont(), + coordSize, + coordSize); + + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::UpdateDpi(int const /*iDpi*/) noexcept +{ + return S_OK; +} + +// Method Description: +// - This method will update our internal reference for how big the viewport is. +// Does nothing for WDDMCon. +// Arguments: +// - srNewViewport - The bounds of the new viewport. +// Return Value: +// - HRESULT S_OK +[[nodiscard]] +HRESULT WddmConEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept +{ + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::GetProposedFont(const FontInfoDesired& /*pfiFontInfoDesired*/, + FontInfo& /*pfiFontInfo*/, + int const /*iDpi*/) noexcept +{ + return S_OK; +} + +SMALL_RECT WddmConEngine::GetDirtyRectInChars() +{ + SMALL_RECT r; + r.Bottom = _displayHeight > 0 ? (SHORT)(_displayHeight - 1) : 0; + r.Top = 0; + r.Left = 0; + r.Right = _displayWidth > 0 ? (SHORT)(_displayWidth - 1) : 0; + + return r; +} + +RECT WddmConEngine::GetDisplaySize() +{ + RECT r; + r.top = 0; + r.left = 0; + r.bottom = _displayHeight; + r.right = _displayWidth; + + return r; +} + +[[nodiscard]] +HRESULT WddmConEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept +{ + // In order to retrieve the font size being used by DirectX, it is necessary + // to modify the API set that defines the contract for WddmCon. However, the + // intention is to subsume WddmCon into ConhostV2 directly once the issue of + // building in the OneCore 'depot' including DirectX headers and libs is + // resolved. The font size has no bearing on the behavior of the console + // since it is used to determine the invalid rectangle whenever the console + // buffer changes. However, given that no invalidation logic exists for this + // renderer, the value returned by this function is irrelevant. + // + // TODO: MSFT 11851921 - Subsume WddmCon into ConhostV2 and remove the API + // set extension. + COORD c; + c.X = DEFAULT_FONT_WIDTH; + c.Y = DEFAULT_FONT_HEIGHT; + + *pFontSize = c; + return S_OK; +} + +[[nodiscard]] +HRESULT WddmConEngine::IsGlyphWideByFont(const std::wstring_view /*glyph*/, _Out_ bool* const pResult) noexcept +{ + *pResult = false; + return S_OK; +} + +// Method Description: +// - Updates the window's title string. +// Does nothing for WddmCon. +// Arguments: +// - newTitle: the new string to use for the title of the window +// Return Value: +// - S_OK +[[nodiscard]] +HRESULT WddmConEngine::_DoUpdateTitle(_In_ const std::wstring& /*newTitle*/) noexcept +{ + return S_OK; +} diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp new file mode 100644 index 000000000..eecdfdea1 --- /dev/null +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "..\..\renderer\inc\RenderEngineBase.hpp" + +namespace Microsoft::Console::Render +{ + class WddmConEngine final : public RenderEngineBase + { + public: + WddmConEngine(); + ~WddmConEngine() override; + + [[nodiscard]] + HRESULT Initialize() noexcept; + bool IsInitialized(); + + // Used to release device resources so that another instance of + // conhost can render to the screen (i.e. only one DirectX + // application may control the screen at a time.) + [[nodiscard]] + HRESULT Enable() noexcept; + [[nodiscard]] + HRESULT Disable() noexcept; + + RECT GetDisplaySize(); + + // IRenderEngine Members + [[nodiscard]] + HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; + [[nodiscard]] + HRESULT InvalidateCursor(const COORD* const pcoordCursor) noexcept override; + [[nodiscard]] + HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; + [[nodiscard]] + HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; + [[nodiscard]] + HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] + HRESULT InvalidateAll() noexcept override; + [[nodiscard]] + HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; + [[nodiscard]] + HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; + + [[nodiscard]] + HRESULT StartPaint() noexcept override; + [[nodiscard]] + HRESULT EndPaint() noexcept override; + [[nodiscard]] + HRESULT Present() noexcept override; + + + [[nodiscard]] + HRESULT ScrollFrame() noexcept override; + + [[nodiscard]] + HRESULT PaintBackground() noexcept override; + [[nodiscard]] + HRESULT PaintBufferLine(std::basic_string_view const clusters, + const COORD coord, + const bool trimLeft) noexcept override; + [[nodiscard]] + HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; + [[nodiscard]] + HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + + [[nodiscard]] + HRESULT PaintCursor(const CursorOptions& options) noexcept override; + + [[nodiscard]] + HRESULT UpdateDrawingBrushes(COLORREF const colorForeground, + COLORREF const colorBackground, + const WORD legacyColorAttribute, + const bool isBold, + bool const isSettingDefaultBrushes) noexcept override; + [[nodiscard]] + HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; + [[nodiscard]] + HRESULT UpdateDpi(int const iDpi) noexcept override; + [[nodiscard]] + HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + + [[nodiscard]] + HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; + + SMALL_RECT GetDirtyRectInChars() override; + [[nodiscard]] + HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; + [[nodiscard]] + HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; + + protected: + [[nodiscard]] + HRESULT _DoUpdateTitle(_In_ const std::wstring& newTitle) noexcept override; + + private: + HANDLE _hWddmConCtx; + + // Helpers + void FreeResources(ULONG displayHeight); + + // Variables + LONG _displayHeight; + LONG _displayWidth; + + PCD_IO_ROW_INFORMATION *_displayState; + + WORD _currentLegacyColorAttribute; + }; +} diff --git a/src/renderer/wddmcon/dirs b/src/renderer/wddmcon/dirs new file mode 100644 index 000000000..46c4fdd5a --- /dev/null +++ b/src/renderer/wddmcon/dirs @@ -0,0 +1,2 @@ +DIRS= \ + lib diff --git a/src/renderer/wddmcon/lib/sources b/src/renderer/wddmcon/lib/sources new file mode 100644 index 000000000..b2a289f2b --- /dev/null +++ b/src/renderer/wddmcon/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConRenderWddmCon +TARGETTYPE = LIBRARY diff --git a/src/renderer/wddmcon/main.cxx b/src/renderer/wddmcon/main.cxx new file mode 100644 index 000000000..11ca30730 --- /dev/null +++ b/src/renderer/wddmcon/main.cxx @@ -0,0 +1,767 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "main.h" + +HINSTANCE g_hInstance; + +#define FONT_SIZE 20.0f +#define FONT_FACE L"Courier New" +#define CONSOLE_MARGIN 2 +#define MAX_RENDER_ATTEMPTS 3ul + +#define REGSTR_PATH_CONKBD L"SYSTEM\\CurrentControlSet\\Control\\ConKbd" +#define REGSTR_VALUE_DISPLAY_INIT_DELAY L"DisplayInitDelay" +#define REGSTR_VALUE_FONT_SIZE L"FontSize" + +#define SafeRelease(p) if (p) { (p)->Release(); (p) = NULL;} + +typedef struct tagWDDMCONSOLECONTEXT { + // Console state + BOOLEAN fOutputEnabled; + BOOLEAN fInD2DBatch; + DXGI_MODE_DESC DisplayMode; + DWORD DisplayInitDelay; + CD_IO_DISPLAY_SIZE DisplaySize; + float FontSize; + float LineHeight; + float GlyphWidth; + float DpiX; + float DpiY; + WCHAR *pwszGlyphRunAccel; + + // Device-Independent Resources + ID2D1Factory *pD2DFactory; + IDWriteFactory *pDWriteFactory; + IDWriteTextFormat *pDWriteTextFormat; + + // Device-Dependent Resources + BOOLEAN fHaveDeviceResources; + ID3D11Device *pD3DDevice; + ID3D11DeviceContext *pD3DDeviceContext; + IDXGIAdapter1 *pDXGIAdapter1; + IDXGIFactory2 *pDXGIFactory2; + IDXGIFactoryDWM *pDXGIFactoryDWM; + IDXGIOutput *pDXGIOutput; + IDXGISwapChainDWM* pDXGISwapChainDWM; + IDXGISurface *pDXGISurface; + ID2D1RenderTarget *pD2DSwapChainRT; + ID2D1SolidColorBrush *pD2DColorBrush; +} WDDMCONSOLECONTEXT, *PWDDMCONSOLECONTEXT; + +void +ReleaseDeviceResources( + _In_ PWDDMCONSOLECONTEXT pCtx + ) +{ + pCtx->fHaveDeviceResources = FALSE; + SafeRelease(pCtx->pD2DColorBrush); + + if (pCtx->pD2DSwapChainRT && pCtx->fInD2DBatch) + { + pCtx->pD2DSwapChainRT->EndDraw(); + } + SafeRelease(pCtx->pD2DSwapChainRT); + + SafeRelease(pCtx->pDXGISurface); + SafeRelease(pCtx->pDXGISwapChainDWM); + SafeRelease(pCtx->pDXGIOutput); + + if (pCtx->pD3DDeviceContext) + { + // To ensure the swap chain goes away we must unbind any views from the + // D3D pipeline + pCtx->pD3DDeviceContext->OMSetRenderTargets(0, NULL, NULL); + } + SafeRelease(pCtx->pD3DDeviceContext); + + SafeRelease(pCtx->pD3DDevice); + + SafeRelease(pCtx->pDXGIAdapter1); + SafeRelease(pCtx->pDXGIFactoryDWM); + SafeRelease(pCtx->pDXGIFactory2); +} + +VOID +WINAPI +WDDMConDestroy( + _In_ HANDLE hDisplay + ) +{ + if (hDisplay != NULL) { + PWDDMCONSOLECONTEXT pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + ReleaseDeviceResources(pCtx); + SafeRelease(pCtx->pDWriteTextFormat); + SafeRelease(pCtx->pDWriteFactory); + SafeRelease(pCtx->pD2DFactory); + + RtlFreeHeap(RtlProcessHeap(), 0, (PVOID)pCtx->pwszGlyphRunAccel); + RtlFreeHeap(RtlProcessHeap(), 0, (PVOID)pCtx); + } +} + +HRESULT +ReadSettings( + _Inout_ PWDDMCONSOLECONTEXT pCtx + ) +{ + HRESULT hr = S_OK; + DWORD Error = ERROR_SUCCESS; + HKEY hKey = NULL; + DWORD ValueType = REG_NONE; + DWORD ValueSize = 0; + DWORD ValueData = 0; + + if (pCtx == NULL) { + hr = E_INVALIDARG; + } + + if (SUCCEEDED(hr)) { + Error = RegOpenKeyEx(HKEY_LOCAL_MACHINE, + REGSTR_PATH_CONKBD, + 0, + KEY_READ, + &hKey); + + if (Error != ERROR_SUCCESS) { + hr = HRESULT_FROM_WIN32(Error); + } + } + + if (SUCCEEDED(hr)) { + ValueSize = sizeof(ValueData); + + Error = RegQueryValueEx(hKey, + REGSTR_VALUE_DISPLAY_INIT_DELAY, + NULL, + &ValueType, + (PBYTE)&ValueData, + &ValueSize); + + if ((Error == ERROR_SUCCESS) && + (ValueType == REG_DWORD) && + (ValueSize == sizeof(ValueData))) { + pCtx->DisplayInitDelay = ValueData; + } + + ValueSize = sizeof(ValueData); + + Error = RegQueryValueEx(hKey, + REGSTR_VALUE_FONT_SIZE, + NULL, + &ValueType, + (PBYTE)&ValueData, + &ValueSize); + + if ((Error == ERROR_SUCCESS) && + (ValueType == REG_DWORD) && + (ValueSize == sizeof(ValueData)) && + (ValueData > 0)) { + pCtx->FontSize = (float)ValueData; + } + } + + if (hKey != NULL) { + RegCloseKey(hKey); + } + + return hr; +} + +HRESULT +CreateTextLayout( + _In_ PWDDMCONSOLECONTEXT pCtx, + _In_reads_(StringLength) WCHAR *String, + _In_ size_t StringLength, + _Out_ IDWriteTextLayout **ppTextLayout + ) +{ + HRESULT hr = S_OK; + + if (pCtx == NULL) { + hr = E_INVALIDARG; + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDWriteFactory->CreateTextLayout(String, + static_cast(StringLength), + pCtx->pDWriteTextFormat, + (float)pCtx->DisplayMode.Width, + pCtx->LineHeight != 0 ? pCtx->LineHeight : (float)pCtx->DisplayMode.Height, + ppTextLayout); + } + + return hr; +} + +HRESULT +CopyFrontToBack( + _In_ PWDDMCONSOLECONTEXT pCtx + ) +{ + HRESULT hr; + ID3D11Resource *pBackBuffer = nullptr; + ID3D11Resource *pFrontBuffer = nullptr; + + hr = pCtx->pDXGISwapChainDWM->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); + + if (SUCCEEDED(hr)) + { + hr = pCtx->pDXGISwapChainDWM->GetBuffer(1, IID_PPV_ARGS(&pFrontBuffer)); + } + + if (SUCCEEDED(hr)) + { + pCtx->pD3DDeviceContext->CopyResource(pBackBuffer, pFrontBuffer); + } + + SafeRelease(pFrontBuffer); + SafeRelease(pBackBuffer); + + return hr; +} + +HRESULT +PresentSwapChain( + _In_ PWDDMCONSOLECONTEXT pCtx + ) +{ + HRESULT hr; + + hr = pCtx->pDXGISwapChainDWM->Present(1, 0); + + if (SUCCEEDED(hr)) + { + hr = CopyFrontToBack(pCtx); + } + + return hr; +} + +HRESULT +CreateDeviceResources( + _In_ PWDDMCONSOLECONTEXT pCtx, + _In_ BOOLEAN fCreateSwapChain + ) +{ + if (pCtx->fHaveDeviceResources) { + ReleaseDeviceResources(pCtx); + } + + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(&pCtx->pDXGIFactory2)); + + if (SUCCEEDED(hr)) { + hr = pCtx->pDXGIFactory2->QueryInterface(IID_PPV_ARGS(&pCtx->pDXGIFactoryDWM)); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDXGIFactory2->EnumAdapters1(0, &pCtx->pDXGIAdapter1); + } + + if (SUCCEEDED(hr)) { + DWORD DeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | + D3D11_CREATE_DEVICE_SINGLETHREADED; + + D3D_FEATURE_LEVEL FeatureLevels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_1, + }; + + hr = D3D11CreateDevice(pCtx->pDXGIAdapter1, + D3D_DRIVER_TYPE_UNKNOWN, + NULL, + DeviceFlags, + FeatureLevels, + ARRAYSIZE(FeatureLevels), + D3D11_SDK_VERSION, + &pCtx->pD3DDevice, + NULL, + &pCtx->pD3DDeviceContext); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDXGIAdapter1->EnumOutputs(0, &pCtx->pDXGIOutput); + } + + if (SUCCEEDED(hr)) { + DXGI_MODE_DESC currentmode = {0}; + hr = pCtx->pDXGIOutput->FindClosestMatchingMode(¤tmode, + &pCtx->DisplayMode, + pCtx->pD3DDevice); + } + + if (fCreateSwapChain) { + if (SUCCEEDED(hr)) { + D3D11_VIEWPORT viewport; + viewport.Width = (FLOAT)pCtx->DisplayMode.Width; + viewport.Height = (FLOAT)pCtx->DisplayMode.Height; + viewport.TopLeftX = 0; + viewport.TopLeftY = 0; + viewport.MinDepth = 0; + viewport.MaxDepth = 1; + pCtx->pD3DDeviceContext->RSSetViewports(1, &viewport); + } + + if (SUCCEEDED(hr)) { + DXGI_SWAP_CHAIN_DESC SwapChainDesc = { 0 }; + DXGI_SAMPLE_DESC LocalSampleDesc = { 1, 0 }; + + SwapChainDesc.BufferDesc.Width = 0; + SwapChainDesc.BufferDesc.Height = 0; + SwapChainDesc.BufferDesc.RefreshRate.Numerator = 0; + SwapChainDesc.BufferDesc.RefreshRate.Denominator = 1; + SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + SwapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE; + SwapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED; + + SwapChainDesc.SampleDesc = LocalSampleDesc; + SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_BACK_BUFFER; + SwapChainDesc.BufferCount = 2; + SwapChainDesc.OutputWindow = NULL; + SwapChainDesc.Windowed = FALSE; + SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_SEQUENTIAL; + SwapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_NONPREROTATED; + + hr = pCtx->pDXGIFactoryDWM->CreateSwapChain(pCtx->pD3DDevice, + &SwapChainDesc, + pCtx->pDXGIOutput, + &pCtx->pDXGISwapChainDWM); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDXGISwapChainDWM->GetBuffer(0, IID_PPV_ARGS(&pCtx->pDXGISurface)); + } + + if (SUCCEEDED(hr)) { + + D2D1_RENDER_TARGET_PROPERTIES props = + D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED), + 0.0f, + 0.0f); + + hr = pCtx->pD2DFactory->CreateDxgiSurfaceRenderTarget(pCtx->pDXGISurface, + &props, + &pCtx->pD2DSwapChainRT); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pD2DSwapChainRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), + &pCtx->pD2DColorBrush); + } + } + + if (SUCCEEDED(hr)) { + pCtx->fHaveDeviceResources = TRUE; + if (pCtx->fInD2DBatch) { + pCtx->pD2DSwapChainRT->BeginDraw(); + } + } else { + ReleaseDeviceResources(pCtx); + } + + return hr; +} + +HRESULT +WINAPI +WDDMConCreate( + _In_ HANDLE *phDisplay + ) +{ + HRESULT hr = S_OK; + IDWriteTextLayout *pTextLayout = NULL; + DWRITE_TEXT_METRICS TextMetrics = {}; + PWDDMCONSOLECONTEXT pCtx = + (PWDDMCONSOLECONTEXT)RtlAllocateHeap(RtlProcessHeap(), + 0, + sizeof(WDDMCONSOLECONTEXT)); + + if (pCtx == NULL) { + hr = E_OUTOFMEMORY; + } else { + RtlZeroMemory(pCtx, sizeof(WDDMCONSOLECONTEXT)); + pCtx->fOutputEnabled = FALSE; + pCtx->FontSize = FONT_SIZE; + + ReadSettings(pCtx); + } + + if (SUCCEEDED(hr)) { + if (pCtx->DisplayInitDelay != 0) { + Sleep(pCtx->DisplayInitDelay); + } + + hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pCtx->pD2DFactory); + } + + if (SUCCEEDED(hr)) { + hr = DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(pCtx->pDWriteFactory), + reinterpret_cast(&pCtx->pDWriteFactory) + ); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDWriteFactory->CreateTextFormat(FONT_FACE, + NULL, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + pCtx->FontSize, + L"en-us", + &pCtx->pDWriteTextFormat); + } + + if (SUCCEEDED(hr)) { + hr = pCtx->pDWriteTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER); + } + + if (SUCCEEDED(hr)) { + hr = CreateDeviceResources(pCtx, FALSE); + } + + if (SUCCEEDED(hr)) { + ReleaseDeviceResources(pCtx); + + hr = CreateTextLayout(pCtx, + L"M", + 1, + &pTextLayout); + } + + if (SUCCEEDED(hr)) { + hr = pTextLayout->GetMetrics(&TextMetrics); + pCtx->GlyphWidth = TextMetrics.width; + pCtx->LineHeight = TextMetrics.height; + } + + if (SUCCEEDED(hr)) { +#pragma warning(push) +#pragma warning(disable : 4996) // GetDesktopDpi is deprecated. + pCtx->pD2DFactory->GetDesktopDpi(&pCtx->DpiX, &pCtx->DpiY); +#pragma warning(pop) + float MaxWidth = pTextLayout->GetMaxWidth(); + float MaxHeight = pTextLayout->GetMaxHeight(); + pCtx->GlyphWidth = (float)(ULONG)(pCtx->GlyphWidth); + pCtx->LineHeight = (float)(ULONG)(pCtx->LineHeight); + pCtx->DisplaySize.Width = (ULONG)(MaxWidth / pCtx->GlyphWidth); + pCtx->DisplaySize.Height = (ULONG)(MaxHeight / pCtx->LineHeight) + 1; + pCtx->DisplaySize.Width -= CONSOLE_MARGIN * 2; + pCtx->DisplaySize.Height -= CONSOLE_MARGIN * 2; + } + + if (SUCCEEDED(hr)) { + pCtx->pwszGlyphRunAccel = (WCHAR*)RtlAllocateHeap(RtlProcessHeap(), + 0, + sizeof(WCHAR) * (pCtx->DisplaySize.Width + 1)); + if (pCtx->pwszGlyphRunAccel == NULL) { + hr = E_OUTOFMEMORY; + } + } + + SafeRelease(pTextLayout); + + if (SUCCEEDED(hr)) { + *phDisplay = (HANDLE)pCtx; + } else if (pCtx != NULL) { + WDDMConDestroy(pCtx); + } + + return hr; +} + +D2D1::ColorF ConsoleColors[] = { D2D1::ColorF(D2D1::ColorF::Black), + D2D1::ColorF(D2D1::ColorF::DarkBlue), + D2D1::ColorF(D2D1::ColorF::DarkGreen), + D2D1::ColorF(D2D1::ColorF::DarkCyan), + D2D1::ColorF(D2D1::ColorF::DarkRed), + D2D1::ColorF(D2D1::ColorF::DarkMagenta), + D2D1::ColorF(D2D1::ColorF::Olive), + D2D1::ColorF(D2D1::ColorF::DarkGray), + D2D1::ColorF(D2D1::ColorF::LightGray), + D2D1::ColorF(D2D1::ColorF::Blue), + D2D1::ColorF(D2D1::ColorF::Lime), + D2D1::ColorF(D2D1::ColorF::Cyan), + D2D1::ColorF(D2D1::ColorF::Red), + D2D1::ColorF(D2D1::ColorF::Magenta), + D2D1::ColorF(D2D1::ColorF::Yellow), + D2D1::ColorF(D2D1::ColorF::White) }; + +HRESULT +WINAPI +WDDMConBeginUpdateDisplayBatch( + _In_ HANDLE hDisplay + ) +{ + HRESULT hr = S_OK; + PWDDMCONSOLECONTEXT pCtx = NULL; + + if (hDisplay == NULL) { + hr = E_INVALIDARG; + } else { + pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + } + + if (SUCCEEDED(hr) && pCtx->fInD2DBatch) { + hr = E_INVALIDARG; + } + + if (SUCCEEDED(hr) && pCtx->fOutputEnabled) { + if (!pCtx->fHaveDeviceResources) { + hr = CreateDeviceResources(pCtx, TRUE); + } + + if (SUCCEEDED(hr)) { + pCtx->pD2DSwapChainRT->BeginDraw(); + pCtx->fInD2DBatch = TRUE; + } + } + + return hr; +} + +HRESULT +WINAPI +WDDMConEndUpdateDisplayBatch( + _In_ HANDLE hDisplay + ) +{ + HRESULT hr = S_OK; + PWDDMCONSOLECONTEXT pCtx = NULL; + + if (hDisplay == NULL) { + hr = E_INVALIDARG; + } else { + pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + } + + if (SUCCEEDED(hr) && !pCtx->fInD2DBatch) { + hr = E_INVALIDARG; + } + + if (SUCCEEDED(hr) && pCtx->fHaveDeviceResources) { + pCtx->fInD2DBatch = FALSE; + + hr = pCtx->pD2DSwapChainRT->EndDraw(); + + if (SUCCEEDED(hr)) { + hr = PresentSwapChain(pCtx); + } + + if (FAILED(hr)) { + ReleaseDeviceResources(pCtx); + } + } + + return hr; +} + +HRESULT +WINAPI +WDDMConUpdateDisplay( + _In_ HANDLE hDisplay, + _In_ CD_IO_ROW_INFORMATION *pRowInformation, + _In_ BOOLEAN fInvalidate + ) +{ + HRESULT hr = S_OK; + PWDDMCONSOLECONTEXT pCtx = NULL; + + if (hDisplay == NULL || pRowInformation == NULL) { + hr = E_INVALIDARG; + } else { + pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + } + + // To prevent an infinite loop, we need to limit the number of times we try to render. + // WDDMCon is used typically in bring-up scenarios, especially ones with unstable graphics drivers. + // As such without the limit, an unstable graphics device can cause us to get stuck here + // and hang console subsystem activities indefinitely. + ULONG RenderAttempts = 0; + +ReRender: + ULONG ColumnIndex = 0; + float LineY = 0.0f; + if (SUCCEEDED(hr) && pCtx->fOutputEnabled) { + if (SUCCEEDED(hr) && !pCtx->fHaveDeviceResources) { + hr = CreateDeviceResources(pCtx, TRUE); + } + + if (SUCCEEDED(hr)) { + LineY = pRowInformation->Index * pCtx->LineHeight; + LineY += CONSOLE_MARGIN * pCtx->LineHeight; + if (!pCtx->fInD2DBatch) { + pCtx->pD2DSwapChainRT->BeginDraw(); + } + } + + while (SUCCEEDED(hr)) { + IDWriteTextLayout *pTextLayout = NULL; + if (fInvalidate || + (memcmp(&pRowInformation->New[ColumnIndex], + &pRowInformation->Old[ColumnIndex], + sizeof(CD_IO_CHARACTER)) != 0)) { + PCD_IO_CHARACTER pCharacter = &pRowInformation->New[ColumnIndex]; + float CharacterOrigin = (ColumnIndex + CONSOLE_MARGIN) * pCtx->GlyphWidth; + ULONG ColumnIndexStart = ColumnIndex; + ULONG ColumnIndexReadAhead = ColumnIndex + 1; + ULONG GlyphRunLength; + + pCtx->pwszGlyphRunAccel[ColumnIndex] = pRowInformation->New[ColumnIndex].Character; + if (ColumnIndexReadAhead != pCtx->DisplaySize.Width) { + while (pRowInformation->New[ColumnIndex].Atribute == pRowInformation->New[ColumnIndexReadAhead].Atribute) { + if (memcmp(&pRowInformation->New[ColumnIndexReadAhead], + &pRowInformation->Old[ColumnIndexReadAhead], + sizeof(CD_IO_CHARACTER)) == 0) { + break; + } + + pCtx->pwszGlyphRunAccel[ColumnIndexReadAhead] = pRowInformation->New[ColumnIndexReadAhead].Character; + + if (++ColumnIndexReadAhead == pCtx->DisplaySize.Width) { + break; + } + } + } + pCtx->pwszGlyphRunAccel[ColumnIndexReadAhead] = '\0'; + GlyphRunLength = ColumnIndexReadAhead - ColumnIndex; + + if (SUCCEEDED(hr)) { + hr = CreateTextLayout(pCtx, + &pCtx->pwszGlyphRunAccel[ColumnIndex], + GlyphRunLength, + &pTextLayout); + } + + ColumnIndex = ColumnIndexReadAhead - 1; + + if (SUCCEEDED(hr)) { + D2D1_RECT_F GlyphRectangle = D2D1::RectF(CharacterOrigin, + LineY, + CharacterOrigin + pCtx->GlyphWidth * GlyphRunLength, + LineY + pCtx->LineHeight); + D2D1_POINT_2F Origin = D2D1::Point2F(CharacterOrigin, LineY); + + if (ColumnIndexStart == 0) { + GlyphRectangle.left = 0.0f; + } + + if ((UINT)pRowInformation->Index == 0) { + GlyphRectangle.top = 0.0f; + } + + if (ColumnIndex == pCtx->DisplaySize.Width - 1) { + GlyphRectangle.right = (float)pCtx->DisplayMode.Width; + } + + if ((UINT)pRowInformation->Index == pCtx->DisplaySize.Height - 1) { + GlyphRectangle.bottom = (float)pCtx->DisplayMode.Height; + } + + pCtx->pD2DColorBrush->SetColor( + ConsoleColors[(pCharacter->Atribute >> 4) & 0xF]); + pCtx->pD2DSwapChainRT->FillRectangle(&GlyphRectangle, + pCtx->pD2DColorBrush); + + pCtx->pD2DColorBrush->SetColor(ConsoleColors[pCharacter->Atribute & 0xF]); + pCtx->pD2DSwapChainRT->DrawTextLayout(Origin, + pTextLayout, + pCtx->pD2DColorBrush, + D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); + } + + SafeRelease(pTextLayout); + } + + if (++ColumnIndex == pCtx->DisplaySize.Width) { + break; + } + } + + if (SUCCEEDED(hr)) { + if (!pCtx->fInD2DBatch) { + hr = pCtx->pD2DSwapChainRT->EndDraw(); + + if (SUCCEEDED(hr)) { + hr = PresentSwapChain(pCtx); + } + } + } + + if (FAILED(hr) && pCtx != NULL && pCtx->fHaveDeviceResources) { + ReleaseDeviceResources(pCtx); + RenderAttempts++; + + if (RenderAttempts < MAX_RENDER_ATTEMPTS) + { + hr = S_OK; + goto ReRender; + } + } + } + + return hr; +} + + +HRESULT +WINAPI +WDDMConGetDisplaySize( + _In_ HANDLE hDisplay, + _In_ CD_IO_DISPLAY_SIZE *pDisplaySize + ) +{ + HRESULT hr = S_OK; + PWDDMCONSOLECONTEXT pCtx = NULL; + + if (hDisplay == NULL) { + hr = E_INVALIDARG; + } else { + pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + } + + if (SUCCEEDED(hr)) { + *pDisplaySize = pCtx->DisplaySize; + } + + return hr; +} + +HRESULT +WINAPI +WDDMConEnableDisplayAccess( + _In_ HANDLE hDisplay, + _In_ BOOLEAN fOutputEnabled + ) +{ + HRESULT hr = S_OK; + PWDDMCONSOLECONTEXT pCtx = NULL; + + if (hDisplay == NULL) { + hr = E_INVALIDARG; + } else { + pCtx = (PWDDMCONSOLECONTEXT)hDisplay; + } + + if (SUCCEEDED(hr) && + fOutputEnabled == pCtx->fOutputEnabled) { + hr = E_NOT_VALID_STATE; + } + + if (SUCCEEDED(hr)) { + pCtx->fOutputEnabled = fOutputEnabled; + if (!fOutputEnabled) { + ReleaseDeviceResources(pCtx); + } + } + + return hr; +} diff --git a/src/renderer/wddmcon/main.h b/src/renderer/wddmcon/main.h new file mode 100644 index 000000000..4a2078e2e --- /dev/null +++ b/src/renderer/wddmcon/main.h @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +HRESULT +WINAPI +WDDMConBeginUpdateDisplayBatch( + _In_ HANDLE hDisplay + ); + +HRESULT +WINAPI +WDDMConCreate( + _In_ HANDLE* phDisplay + ); + +VOID +WINAPI +WDDMConDestroy( + _In_ HANDLE hDisplay + ); + +HRESULT +WINAPI +WDDMConEnableDisplayAccess( + _In_ HANDLE phDisplay, + _In_ BOOLEAN fEnabled + ); + +HRESULT +WINAPI +WDDMConEndUpdateDisplayBatch( + _In_ HANDLE hDisplay + ); + +HRESULT +WINAPI +WDDMConGetDisplaySize( + _In_ HANDLE hDisplay, + _In_ CD_IO_DISPLAY_SIZE* pDisplaySize + ); + +HRESULT +WINAPI +WDDMConUpdateDisplay( + _In_ HANDLE hDisplay, + _In_ CD_IO_ROW_INFORMATION* pRowInformation, + _In_ BOOLEAN fInvalidate + ); diff --git a/src/renderer/wddmcon/precomp.cpp b/src/renderer/wddmcon/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/renderer/wddmcon/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/renderer/wddmcon/precomp.h b/src/renderer/wddmcon/precomp.h new file mode 100644 index 000000000..1d1d4cc0b --- /dev/null +++ b/src/renderer/wddmcon/precomp.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include + +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#pragma hdrstop diff --git a/src/renderer/wddmcon/sources.inc b/src/renderer/wddmcon/sources.inc new file mode 100644 index 000000000..ef158a3d2 --- /dev/null +++ b/src/renderer/wddmcon/sources.inc @@ -0,0 +1,36 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Renderer for WDDMCON +# ------------------------------------- + +# This module provides a rendering engine implementation that +# utilizes the WDDMCON library for drawing the console to a fullscreen +# DirectX surface. + +# ------------------------------------- +# CRT Configuration +# ------------------------------------- + +BUILD_FOR_CORESYSTEM = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\..\inc; \ + ..\..\..\inc; \ + ..\..\..\host; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + +SOURCES = \ + $(SOURCES) \ + ..\main.cxx \ + ..\WddmConRenderer.cpp \ diff --git a/src/server/ApiDispatchers.cpp b/src/server/ApiDispatchers.cpp new file mode 100644 index 000000000..58f4aced5 --- /dev/null +++ b/src/server/ApiDispatchers.cpp @@ -0,0 +1,1681 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiDispatchers.h" + +#include "../host/directio.h" +#include "../host/getset.h" +#include "../host/stream.h" +#include "../host/srvinit.h" +#include "../host/telemetry.hpp" +#include "../host/cmdline.h" + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleCP(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_GETCP_MSG* const a = &m->u.consoleMsgL1.GetConsoleCP; + + if (a->Output) + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleOutputCP); + m->_pApiRoutines->GetConsoleOutputCodePageImpl(a->CodePage); + } + else + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleCP); + m->_pApiRoutines->GetConsoleInputCodePageImpl(a->CodePage); + } + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleMode(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleMode); + CONSOLE_MODE_MSG* const a = &m->u.consoleMsgL1.GetConsoleMode; + std::wstring handleType = L"unknown"; + + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetConsoleMode", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingOpcode(WINEVENT_OPCODE_START) + ); + + auto tracing = wil::scope_exit([&]() + { + Tracing::s_TraceApi(a, handleType); + TraceLoggingWrite(g_hConhostV2EventTraceProvider, "API_GetConsoleMode", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingOpcode(WINEVENT_OPCODE_STOP) + ); + }); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + if (pObjectHandle->IsInputHandle()) + { + handleType = L"input handle"; + InputBuffer* pObj; + RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_READ, &pObj)); + m->_pApiRoutines->GetConsoleInputModeImpl(*pObj, a->Mode); + } + else + { + handleType = L"output handle"; + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj)); + m->_pApiRoutines->GetConsoleOutputModeImpl(*pObj, a->Mode); + } + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleMode(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleMode); + CONSOLE_MODE_MSG* const a = &m->u.consoleMsgL1.SetConsoleMode; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + if (pObjectHandle->IsInputHandle()) + { + InputBuffer* pObj; + RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_WRITE, &pObj)); + return m->_pApiRoutines->SetConsoleInputModeImpl(*pObj, a->Mode); + } + else + { + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + return m->_pApiRoutines->SetConsoleOutputModeImpl(*pObj, a->Mode); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetNumberOfInputEvents(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetNumberOfConsoleInputEvents); + CONSOLE_GETNUMBEROFINPUTEVENTS_MSG* const a = &m->u.consoleMsgL1.GetNumberOfConsoleInputEvents; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + InputBuffer* pObj; + RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_READ, &pObj)); + + return m->_pApiRoutines->GetNumberOfConsoleInputEventsImpl(*pObj, a->ReadyEvents); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleInput(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const pbReplyPending) +{ + *pbReplyPending = FALSE; + + CONSOLE_GETCONSOLEINPUT_MSG* const a = &m->u.consoleMsgL1.GetConsoleInput; + if (WI_IsFlagSet(a->Flags, CONSOLE_READ_NOREMOVE)) + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::PeekConsoleInput, a->Unicode); + } + else + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsoleInput, a->Unicode); + } + + a->NumRecords = 0; + + // If any flags are set that are not within our enum, it's invalid. + if (WI_IsAnyFlagSet(a->Flags, ~CONSOLE_READ_VALID)) + { + return E_INVALIDARG; + } + + // Make sure we have a valid input buffer. + ConsoleHandleData* const pHandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pHandleData); + InputBuffer* pInputBuffer; + RETURN_IF_FAILED(pHandleData->GetInputBuffer(GENERIC_READ, &pInputBuffer)); + + // Get output buffer. + PVOID pvBuffer; + ULONG cbBufferSize; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBufferSize)); + + INPUT_RECORD* const rgRecords = reinterpret_cast(pvBuffer); + size_t const cRecords = cbBufferSize / sizeof(INPUT_RECORD); + + bool const fIsPeek = WI_IsFlagSet(a->Flags, CONSOLE_READ_NOREMOVE); + bool const fIsWaitAllowed = WI_IsFlagClear(a->Flags, CONSOLE_READ_NOWAIT); + + INPUT_READ_HANDLE_DATA* const pInputReadHandleData = pHandleData->GetClientInput(); + + std::unique_ptr waiter; + HRESULT hr; + std::deque> outEvents; + size_t const 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); + } + } + + // We must return the number of records in the message payload (to alert the client) + // as well as in the message headers (below in SetReplyInfomration) to alert the driver. + LOG_IF_FAILED(SizeTToULong(outEvents.size(), &a->NumRecords)); + + size_t cbWritten; + LOG_IF_FAILED(SizeTMult(outEvents.size(), sizeof(INPUT_RECORD), &cbWritten)); + + if (nullptr != waiter.get()) + { + // In some circumstances, the read may have told us to wait because it didn't have data, + // but the client explicitly asked us to return immediate. In that case, we'll convert the + // wait request into a "0 bytes found, OK". + + if (fIsWaitAllowed) + { + hr = ConsoleWaitQueue::s_CreateWait(m, waiter.release()); + if (SUCCEEDED(hr)) + { + *pbReplyPending = TRUE; + hr = CONSOLE_STATUS_WAIT; + } + } + else + { + // If wait isn't allowed and the routine generated a + // waiter, say there was nothing to be + // retrieved right now. + // The waiter will be auto-freed in the smart pointer. + + cbWritten = 0; + hr = S_OK; + } + } + else + { + try + { + for (size_t i = 0; i < cRecords; ++i) + { + if (outEvents.empty()) + { + break; + } + rgRecords[i] = outEvents.front()->ToInputRecord(); + outEvents.pop_front(); + } + } + CATCH_RETURN(); + } + + if (SUCCEEDED(hr)) + { + m->SetReplyInformation(cbWritten); + } + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerReadConsole(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const pbReplyPending) +{ + *pbReplyPending = FALSE; + + CONSOLE_READCONSOLE_MSG* const a = &m->u.consoleMsgL1.ReadConsole; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsole, a->Unicode); + + a->NumBytes = 0; // we return 0 until proven otherwise. + + // Make sure we have a valid input buffer. + ConsoleHandleData* const HandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, HandleData); + InputBuffer* pInputBuffer; + RETURN_IF_FAILED(HandleData->GetInputBuffer(GENERIC_READ, &pInputBuffer)); + + // Get output parameter buffer. + PVOID pvBuffer; + ULONG cbBufferSize; + // TODO: This is dumb. We should find out how much we need, not guess. + // If the request is not in Unicode mode, we must allocate an output buffer that is twice as big as the actual caller buffer. + RETURN_IF_FAILED(m->GetAugmentedOutputBuffer((a->Unicode != FALSE) ? 1 : 2, + &pvBuffer, + &cbBufferSize)); + + // TODO: This is also rather strange and will also probably make more sense if we stop guessing that we need 2x buffer to convert. + // This might need to go on the other side of the fence (inside host) because the server doesn't know what we're going to do with initial num bytes. + // (This restriction exists because it's going to copy initial into the final buffer, but we don't know that.) + RETURN_HR_IF(E_INVALIDARG, a->InitialNumBytes > cbBufferSize); + + // Retrieve input parameters. + // 1. Exe name making the request + ULONG const cchExeName = a->ExeNameLength; + ULONG cbExeName; + RETURN_IF_FAILED(ULongMult(cchExeName, sizeof(wchar_t), &cbExeName)); + wistd::unique_ptr pwsExeName; + + if (cchExeName > 0) + { + pwsExeName = wil::make_unique_nothrow(cchExeName); + RETURN_IF_NULL_ALLOC(pwsExeName); + RETURN_IF_FAILED(m->ReadMessageInput(0, pwsExeName.get(), cbExeName)); + } + const std::wstring_view exeView(pwsExeName.get(), cchExeName); + + // 2. Existing data in the buffer that was passed in. + ULONG const cbInitialData = a->InitialNumBytes; + std::unique_ptr pbInitialData; + + try + { + if (cbInitialData > 0) + { + pbInitialData = std::make_unique(cbInitialData); + + // This parameter starts immediately after the exe name so skip by that many bytes. + RETURN_IF_FAILED(m->ReadMessageInput(cbExeName, pbInitialData.get(), cbInitialData)); + } + } + CATCH_RETURN(); + + // ReadConsole needs this to get the command history list associated with an attached process, but it can be an opaque value. + HANDLE const hConsoleClient = (HANDLE)m->GetProcessHandle(); + + // ReadConsole needs this to store context information across "processed reads" e.g. reads on the same handle + // across multiple calls when we are simulating a command prompt input line for the client application. + INPUT_READ_HANDLE_DATA* const pInputReadHandleData = HandleData->GetClientInput(); + + std::unique_ptr waiter; + size_t cbWritten; + + HRESULT hr; + if (a->Unicode) + { + const std::string_view initialData(pbInitialData.get(), cbInitialData); + const gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); + hr = m->_pApiRoutines->ReadConsoleWImpl(*pInputBuffer, + outputBuffer, + cbWritten, // We must set the reply length in bytes. + waiter, + initialData, + exeView, + *pInputReadHandleData, + hConsoleClient, + a->CtrlWakeupMask, + a->ControlKeyState); + + + } + else + { + const std::string_view initialData(pbInitialData.get(), cbInitialData); + const gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbBufferSize); + hr = m->_pApiRoutines->ReadConsoleAImpl(*pInputBuffer, + outputBuffer, + cbWritten, // We must set the reply length in bytes. + waiter, + initialData, + exeView, + *pInputReadHandleData, + hConsoleClient, + a->CtrlWakeupMask, + a->ControlKeyState); + + } + + LOG_IF_FAILED(SizeTToULong(cbWritten, &a->NumBytes)); + + if (nullptr != waiter.get()) + { + // If we received a waiter, we need to queue the wait and not reply. + hr = ConsoleWaitQueue::s_CreateWait(m, waiter.release()); + + if (SUCCEEDED(hr)) + { + *pbReplyPending = TRUE; + } + } + else + { + // - This routine is called when a ReadConsole or ReadFile request is about to be completed. + // - It sets the number of bytes written as the information to be written with the completion status and, + // if CTRL+Z processing is enabled and a CTRL+Z is detected, switches the number of bytes read to zero. + if (a->ProcessControlZ != FALSE && + a->NumBytes > 0 && + m->State.OutputBuffer != nullptr && + *(PUCHAR)m->State.OutputBuffer == 0x1a) + { + a->NumBytes = 0; + } + + m->SetReplyInformation(a->NumBytes); + } + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerWriteConsole(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const pbReplyPending) +{ + *pbReplyPending = FALSE; + + CONSOLE_WRITECONSOLE_MSG* const a = &m->u.consoleMsgL1.WriteConsole; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsole, a->Unicode); + + // Make sure we have a valid screen buffer. + ConsoleHandleData* HandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, HandleData); + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(HandleData->GetScreenBuffer(GENERIC_WRITE, &pScreenInfo)); + + // Get input parameter buffer + PVOID pvBuffer; + ULONG cbBufferSize; + auto tracing = wil::scope_exit([&]() + { + Tracing::s_TraceApi(pvBuffer, a); + }); + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize)); + + std::unique_ptr waiter; + size_t cbRead; + + // We have to hold onto the HR from the call and return it. + // We can't return some other error after the actual API call. + // This is because the write console function is allowed to write part of the string and then return an error. + // It then must report back how far it got before it failed. + HRESULT hr; + if (a->Unicode) + { + const std::wstring_view buffer(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(wchar_t)); + size_t cchInputRead; + + hr = m->_pApiRoutines->WriteConsoleWImpl(*pScreenInfo, buffer, cchInputRead, waiter); + + // We must set the reply length in bytes. Convert back from characters. + LOG_IF_FAILED(SizeTMult(cchInputRead, sizeof(wchar_t), &cbRead)); + } + else + { + const std::string_view buffer(reinterpret_cast(pvBuffer), cbBufferSize); + size_t cchInputRead; + + hr = m->_pApiRoutines->WriteConsoleAImpl(*pScreenInfo, buffer, cchInputRead, waiter); + + // Reply length is already in bytes (chars), don't need to convert. + cbRead = cchInputRead; + } + + // We must return the byte length of the read data in the message. + LOG_IF_FAILED(SizeTToULong(cbRead, &a->NumBytes)); + + if (nullptr != waiter.get()) + { + // If we received a waiter, we need to queue the wait and not reply. + hr = ConsoleWaitQueue::s_CreateWait(m, waiter.release()); + if (SUCCEEDED(hr)) + { + *pbReplyPending = TRUE; + } + } + else + { + // If no waiter, fill the response data and return. + m->SetReplyInformation(a->NumBytes); + } + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerFillConsoleOutput(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_FILLCONSOLEOUTPUT_MSG const a = &m->u.consoleMsgL2.FillConsoleOutput; + + switch (a->ElementType) + { + case CONSOLE_ATTRIBUTE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::FillConsoleOutputAttribute); + break; + case CONSOLE_ASCII: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::FillConsoleOutputCharacter, false); + break; + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::FillConsoleOutputCharacter, true); + break; + } + + // Capture length of initial fill. + size_t fill = a->Length; + + // Set written length to 0 in case we early return. + a->Length = 0; + + // Make sure we have a valid screen buffer. + ConsoleHandleData* HandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, HandleData); + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(HandleData->GetScreenBuffer(GENERIC_WRITE, &pScreenInfo)); + + + HRESULT hr; + size_t amountWritten; + switch (a->ElementType) + { + case CONSOLE_ATTRIBUTE: + { + hr = m->_pApiRoutines->FillConsoleOutputAttributeImpl(*pScreenInfo, + a->Element, + fill, + a->WriteCoord, + amountWritten); + break; + } + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + { + hr = m->_pApiRoutines->FillConsoleOutputCharacterWImpl(*pScreenInfo, + a->Element, + fill, + a->WriteCoord, + amountWritten); + break; + } + case CONSOLE_ASCII: + { + hr = m->_pApiRoutines->FillConsoleOutputCharacterAImpl(*pScreenInfo, + static_cast(a->Element), + fill, + a->WriteCoord, + amountWritten); + break; + } + default: + return E_INVALIDARG; + } + + LOG_IF_FAILED(SizeTToDWord(amountWritten, &a->Length)); + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleActiveScreenBuffer(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleActiveScreenBuffer); + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + m->_pApiRoutines->SetConsoleActiveScreenBufferImpl(*pObj); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerFlushConsoleInputBuffer(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::FlushConsoleInputBuffer); + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + InputBuffer* pObj; + RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_WRITE, &pObj)); + + m->_pApiRoutines->FlushConsoleInputBuffer(*pObj); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleCP(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_SETCP_MSG* const a = &m->u.consoleMsgL2.SetConsoleCP; + + if (a->Output) + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleOutputCP); + return m->_pApiRoutines->SetConsoleOutputCodePageImpl(a->CodePage); + } + else + { + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleCP); + return m->_pApiRoutines->SetConsoleInputCodePageImpl(a->CodePage); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleCursorInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleCursorInfo); + CONSOLE_GETCURSORINFO_MSG* const a = &m->u.consoleMsgL2.GetConsoleCursorInfo; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + bool visible = false; + m->_pApiRoutines->GetConsoleCursorInfoImpl(*pObj, a->CursorSize, visible); + a->Visible = !!visible; + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleCursorInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleCursorInfo); + CONSOLE_SETCURSORINFO_MSG* const a = &m->u.consoleMsgL2.SetConsoleCursorInfo; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + return m->_pApiRoutines->SetConsoleCursorInfoImpl(*pObj, a->CursorSize, a->Visible); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleScreenBufferInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleScreenBufferInfoEx); + CONSOLE_SCREENBUFFERINFO_MSG* const a = &m->u.consoleMsgL2.GetConsoleScreenBufferInfo; + + auto tracing = wil::scope_exit([&]() + { + Tracing::s_TraceApi(a); + }); + + CONSOLE_SCREEN_BUFFER_INFOEX ex = { 0 }; + ex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj)); + + m->_pApiRoutines->GetConsoleScreenBufferInfoExImpl(*pObj, ex); + + a->FullscreenSupported = !!ex.bFullscreenSupported; + size_t const ColorTableSizeInBytes = RTL_NUMBER_OF_V2(ex.ColorTable) * sizeof(*ex.ColorTable); + CopyMemory(a->ColorTable, ex.ColorTable, ColorTableSizeInBytes); + a->CursorPosition = ex.dwCursorPosition; + a->MaximumWindowSize = ex.dwMaximumWindowSize; + a->Size = ex.dwSize; + a->ScrollPosition.X = ex.srWindow.Left; + a->ScrollPosition.Y = ex.srWindow.Top; + a->CurrentWindowSize.X = ex.srWindow.Right - ex.srWindow.Left; + a->CurrentWindowSize.Y = ex.srWindow.Bottom - ex.srWindow.Top; + a->Attributes = ex.wAttributes; + a->PopupAttributes = ex.wPopupAttributes; + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleScreenBufferInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleScreenBufferInfoEx); + CONSOLE_SCREENBUFFERINFO_MSG* const a = &m->u.consoleMsgL2.SetConsoleScreenBufferInfo; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + CONSOLE_SCREEN_BUFFER_INFOEX ex = { 0 }; + ex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + ex.bFullscreenSupported = a->FullscreenSupported; + size_t const ColorTableSizeInBytes = RTL_NUMBER_OF_V2(ex.ColorTable) * sizeof(*ex.ColorTable); + CopyMemory(ex.ColorTable, a->ColorTable, ColorTableSizeInBytes); + ex.dwCursorPosition = a->CursorPosition; + ex.dwMaximumWindowSize = a->MaximumWindowSize; + ex.dwSize = a->Size; + ex.srWindow = { 0 }; + ex.srWindow.Left = a->ScrollPosition.X; + ex.srWindow.Top = a->ScrollPosition.Y; + ex.srWindow.Right = ex.srWindow.Left + a->CurrentWindowSize.X; + ex.srWindow.Bottom = ex.srWindow.Top + a->CurrentWindowSize.Y; + ex.wAttributes = a->Attributes; + ex.wPopupAttributes = a->PopupAttributes; + + return m->_pApiRoutines->SetConsoleScreenBufferInfoExImpl(*pObj, ex); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleScreenBufferSize(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleScreenBufferSize); + CONSOLE_SETSCREENBUFFERSIZE_MSG* const a = &m->u.consoleMsgL2.SetConsoleScreenBufferSize; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + return m->_pApiRoutines->SetConsoleScreenBufferSizeImpl(*pObj, a->Size); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleCursorPosition(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleCursorPosition); + CONSOLE_SETCURSORPOSITION_MSG* const a = &m->u.consoleMsgL2.SetConsoleCursorPosition; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + return m->_pApiRoutines->SetConsoleCursorPositionImpl(*pObj, a->CursorPosition); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetLargestConsoleWindowSize(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetLargestConsoleWindowSize); + CONSOLE_GETLARGESTWINDOWSIZE_MSG* const a = &m->u.consoleMsgL2.GetLargestConsoleWindowSize; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + m->_pApiRoutines->GetLargestConsoleWindowSizeImpl(*pObj, a->Size); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerScrollConsoleScreenBuffer(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_SCROLLSCREENBUFFER_MSG* const a = &m->u.consoleMsgL2.ScrollConsoleScreenBuffer; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ScrollConsoleScreenBuffer, a->Unicode); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + if (a->Unicode) + { + return m->_pApiRoutines->ScrollConsoleScreenBufferWImpl(*pObj, + a->ScrollRectangle, + a->DestinationOrigin, + a->Clip ? std::optional(a->ClipRectangle) : std::nullopt, + a->Fill.Char.UnicodeChar, + a->Fill.Attributes); + } + else + { + return m->_pApiRoutines->ScrollConsoleScreenBufferAImpl(*pObj, + a->ScrollRectangle, + a->DestinationOrigin, + a->Clip ? std::optional(a->ClipRectangle) : std::nullopt, + a->Fill.Char.AsciiChar, + a->Fill.Attributes); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleTextAttribute(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleTextAttribute); + CONSOLE_SETTEXTATTRIBUTE_MSG* const a = &m->u.consoleMsgL2.SetConsoleTextAttribute; + + auto tracing = wil::scope_exit([&]() + { + Tracing::s_TraceApi(a); + }); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + RETURN_HR(m->_pApiRoutines->SetConsoleTextAttributeImpl(*pObj, a->Attributes)); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleWindowInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleWindowInfo); + CONSOLE_SETWINDOWINFO_MSG* const a = &m->u.consoleMsgL2.SetConsoleWindowInfo; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + return m->_pApiRoutines->SetConsoleWindowInfoImpl(*pObj, a->Absolute, a->Window); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerReadConsoleOutputString(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + RETURN_HR_IF(E_ACCESSDENIED, !m->GetProcessHandle()->GetPolicy().CanReadOutputBuffer()); + + CONSOLE_READCONSOLEOUTPUTSTRING_MSG* const a = &m->u.consoleMsgL2.ReadConsoleOutputString; + + switch (a->StringType) + { + case CONSOLE_ATTRIBUTE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsoleOutputAttribute); + break; + case CONSOLE_ASCII: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsoleOutputCharacter, false); + break; + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsoleOutputCharacter, true); + break; + } + + a->NumRecords = 0; // Set to 0 records returned in case we have failures. + + PVOID pvBuffer; + ULONG cbBuffer; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBuffer)); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pScreenInfo)); + + size_t written; + switch (a->StringType) + { + case CONSOLE_ATTRIBUTE: + { + const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(WORD)); + RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputAttributeImpl(*pScreenInfo, a->ReadCoord, buffer, written)); + break; + } + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + { + const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); + RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterWImpl(*pScreenInfo, a->ReadCoord, buffer, written)); + break; + } + case CONSOLE_ASCII: + { + const gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer); + RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputCharacterAImpl(*pScreenInfo, a->ReadCoord, buffer, written)); + break; + } + default: + return E_INVALIDARG; + } + + // Report count of records now in the buffer (varies based on type) + RETURN_IF_FAILED(SizeTToULong(written, &a->NumRecords)); + + m->SetReplyInformation(cbBuffer); // Set the reply buffer size to what we were originally told the buffer size was (on the way in) + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerWriteConsoleInput(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_WRITECONSOLEINPUT_MSG const a = &m->u.consoleMsgL2.WriteConsoleInput; + + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsoleInput, a->Unicode); + + a->NumRecords = 0; + + RETURN_HR_IF(E_ACCESSDENIED, !m->GetProcessHandle()->GetPolicy().CanWriteInputBuffer()); + + PVOID pvBuffer; + ULONG cbSize; + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbSize)); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + InputBuffer* pInputBuffer; + RETURN_IF_FAILED(pObjectHandle->GetInputBuffer(GENERIC_WRITE, &pInputBuffer)); + + size_t written; + std::basic_string_view buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(INPUT_RECORD)); + if (!a->Unicode) + { + RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleInputAImpl(*pInputBuffer, buffer, written, !!a->Append)); + } + else + { + RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleInputWImpl(*pInputBuffer, buffer, written, !!a->Append)); + } + + RETURN_IF_FAILED(SizeTToULong(written, &a->NumRecords)); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerWriteConsoleOutput(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_WRITECONSOLEOUTPUT_MSG const a = &m->u.consoleMsgL2.WriteConsoleOutput; + + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsoleOutput, a->Unicode); + + // Backup originalRegion and set the written area to a 0 size rectangle in case of failures. + const auto originalRegion = Microsoft::Console::Types::Viewport::FromInclusive(a->CharRegion); + auto writtenRegion = Microsoft::Console::Types::Viewport::FromDimensions(originalRegion.Origin(), { 0, 0 }); + a->CharRegion = writtenRegion.ToInclusive(); + + // Get input parameter buffer + PVOID pvBuffer; + ULONG cbSize; + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbSize)); + + // Make sure we have a valid screen buffer. + ConsoleHandleData* HandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, HandleData); + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(HandleData->GetScreenBuffer(GENERIC_WRITE, &pScreenInfo)); + + // Validate parameters + size_t regionArea; + RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().X, originalRegion.Dimensions().Y, ®ionArea)); + size_t regionBytes; + RETURN_IF_FAILED(SizeTMult(regionArea, sizeof(CHAR_INFO), ®ionBytes)); + RETURN_HR_IF(E_INVALIDARG, cbSize < regionBytes); // If given fewer bytes on input than we need to do this write, it's invalid. + + const gsl::span buffer(reinterpret_cast(pvBuffer), cbSize / sizeof(CHAR_INFO)); + if (!a->Unicode) + { + RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleOutputAImpl(*pScreenInfo, buffer, originalRegion, writtenRegion)); + } + else + { + RETURN_IF_FAILED(m->_pApiRoutines->WriteConsoleOutputWImpl(*pScreenInfo, buffer, originalRegion, writtenRegion)); + } + + // Update the written region if we were successful + a->CharRegion = writtenRegion.ToInclusive(); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerWriteConsoleOutputString(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_WRITECONSOLEOUTPUTSTRING_MSG const a = &m->u.consoleMsgL2.WriteConsoleOutputString; + + auto tracing = wil::scope_exit([&]() + { + Tracing::s_TraceApi(a); + }); + + switch (a->StringType) + { + case CONSOLE_ATTRIBUTE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsoleOutputAttribute); + break; + case CONSOLE_ASCII: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsoleOutputCharacter, false); + break; + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::WriteConsoleOutputCharacter, true); + break; + } + + // Set written records to 0 in case we early return. + a->NumRecords = 0; + + // Make sure we have a valid screen buffer. + ConsoleHandleData* HandleData = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, HandleData); + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(HandleData->GetScreenBuffer(GENERIC_WRITE, &pScreenInfo)); + + // Get input parameter buffer + PVOID pvBuffer; + ULONG cbBufferSize; + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize)); + + HRESULT hr; + size_t used; + switch (a->StringType) + { + case CONSOLE_ASCII: + { + const std::string_view text(reinterpret_cast(pvBuffer), cbBufferSize); + + hr = m->_pApiRoutines->WriteConsoleOutputCharacterAImpl(*pScreenInfo, + text, + a->WriteCoord, + used); + + break; + } + case CONSOLE_REAL_UNICODE: + case CONSOLE_FALSE_UNICODE: + { + const std::wstring_view text(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(wchar_t)); + + hr = m->_pApiRoutines->WriteConsoleOutputCharacterWImpl(*pScreenInfo, + text, + a->WriteCoord, + used); + + break; + } + case CONSOLE_ATTRIBUTE: + { + const std::basic_string_view text(reinterpret_cast(pvBuffer), cbBufferSize / sizeof(WORD)); + + hr = m->_pApiRoutines->WriteConsoleOutputAttributeImpl(*pScreenInfo, + text, + a->WriteCoord, + used); + + break; + } + default: + return E_INVALIDARG; + } + + // We need to return how many records were consumed off of the string + LOG_IF_FAILED(SizeTToULong(used, &a->NumRecords)); + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerReadConsoleOutput(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + RETURN_HR_IF(E_ACCESSDENIED, !m->GetProcessHandle()->GetPolicy().CanReadOutputBuffer()); + + CONSOLE_READCONSOLEOUTPUT_MSG* const a = &m->u.consoleMsgL2.ReadConsoleOutput; + + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::ReadConsoleOutput, a->Unicode); + + // Backup data region passed and set it to a zero size region in case we exit early for failures. + const auto originalRegion = Microsoft::Console::Types::Viewport::FromInclusive(a->CharRegion); + const auto zeroRegion = Microsoft::Console::Types::Viewport::FromDimensions(originalRegion.Origin(), { 0, 0 }); + a->CharRegion = zeroRegion.ToInclusive(); + + PVOID pvBuffer; + ULONG cbBuffer; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBuffer)); + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pScreenInfo; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pScreenInfo)); + + // Validate parameters + size_t regionArea; + RETURN_IF_FAILED(SizeTMult(originalRegion.Dimensions().X, originalRegion.Dimensions().Y, ®ionArea)); + size_t regionBytes; + RETURN_IF_FAILED(SizeTMult(regionArea, sizeof(CHAR_INFO), ®ionBytes)); + RETURN_HR_IF(E_INVALIDARG, regionArea > 0 && ((regionArea > ULONG_MAX / sizeof(CHAR_INFO)) || (cbBuffer < regionBytes))); + + gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(CHAR_INFO)); + auto finalRegion = Microsoft::Console::Types::Viewport::Empty(); // the actual region read out of the buffer + if (!a->Unicode) + { + RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputAImpl(*pScreenInfo, + buffer, + originalRegion, + finalRegion)); + } + else + { + RETURN_IF_FAILED(m->_pApiRoutines->ReadConsoleOutputWImpl(*pScreenInfo, + buffer, + originalRegion, + finalRegion)); + } + + a->CharRegion = finalRegion.ToInclusive(); + + // We have to reply back with the entire buffer length. The client side in kernelbase will trim out + // the correct region of the buffer for return to the original caller. + m->SetReplyInformation(cbBuffer); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleTitle(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_GETTITLE_MSG const a = &m->u.consoleMsgL2.GetConsoleTitle; + Telemetry::Instance().LogApiCall(a->Original ? Telemetry::ApiCall::GetConsoleOriginalTitle : Telemetry::ApiCall::GetConsoleTitle, a->Unicode); + + PVOID pvBuffer; + ULONG cbBuffer; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbBuffer)); + + HRESULT hr = S_OK; + if (a->Unicode) + { + gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer / sizeof(wchar_t)); + size_t written; + size_t needed; + if (a->Original) + { + // This API traditionally doesn't return an HRESULT. Log and discard. + LOG_IF_FAILED(m->_pApiRoutines->GetConsoleOriginalTitleWImpl(buffer, written, needed)); + } + else + { + // This API traditionally doesn't return an HRESULT. Log and discard. + LOG_IF_FAILED(m->_pApiRoutines->GetConsoleTitleWImpl(buffer, written, needed)); + } + + // We must return the needed length of the title string in the TitleLength. + LOG_IF_FAILED(SizeTToULong(needed, &a->TitleLength)); + + // We must return the actually written length of the title string in the reply. + m->SetReplyInformation(written * sizeof(wchar_t)); + } + else + { + gsl::span buffer(reinterpret_cast(pvBuffer), cbBuffer); + size_t written; + size_t needed; + if (a->Original) + { + hr = m->_pApiRoutines->GetConsoleOriginalTitleAImpl(buffer, written, needed); + } + else + { + hr = m->_pApiRoutines->GetConsoleTitleAImpl(buffer, written, needed); + } + + // We must return the needed length of the title string in the TitleLength. + LOG_IF_FAILED(SizeTToULong(needed, &a->TitleLength)); + + // We must return the actually written length of the title string in the reply. + m->SetReplyInformation(written * sizeof(char)); + } + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleTitle(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_SETTITLE_MSG* const a = &m->u.consoleMsgL2.SetConsoleTitle; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleTitle, a->Unicode); + + PVOID pvBuffer; + ULONG cbOriginalLength; + + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbOriginalLength)); + + if (a->Unicode) + { + const std::wstring_view title(reinterpret_cast(pvBuffer), cbOriginalLength / sizeof(wchar_t)); + return m->_pApiRoutines->SetConsoleTitleWImpl(title); + } + else + { + const std::string_view title(reinterpret_cast(pvBuffer), cbOriginalLength); + return m->_pApiRoutines->SetConsoleTitleAImpl(title); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleMouseInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetNumberOfConsoleMouseButtons); + CONSOLE_GETMOUSEINFO_MSG* const a = &m->u.consoleMsgL3.GetConsoleMouseInfo; + + m->_pApiRoutines->GetNumberOfConsoleMouseButtonsImpl(a->NumButtons); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleFontSize(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleFontSize); + CONSOLE_GETFONTSIZE_MSG* const a = &m->u.consoleMsgL3.GetConsoleFontSize; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj)); + + return m->_pApiRoutines->GetConsoleFontSizeImpl(*pObj, a->FontIndex, a->FontSize); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleCurrentFont(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetCurrentConsoleFontEx); + CONSOLE_CURRENTFONT_MSG* const a = &m->u.consoleMsgL3.GetCurrentConsoleFont; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj)); + + CONSOLE_FONT_INFOEX FontInfo = { 0 }; + FontInfo.cbSize = sizeof(FontInfo); + + RETURN_IF_FAILED(m->_pApiRoutines->GetCurrentConsoleFontExImpl(*pObj, a->MaximumWindow, FontInfo)); + + CopyMemory(a->FaceName, FontInfo.FaceName, RTL_NUMBER_OF_V2(a->FaceName) * sizeof(a->FaceName[0])); + a->FontFamily = FontInfo.FontFamily; + a->FontIndex = FontInfo.nFont; + a->FontSize = FontInfo.dwFontSize; + a->FontWeight = FontInfo.FontWeight; + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleDisplayMode(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleDisplayMode); + CONSOLE_SETDISPLAYMODE_MSG* const a = &m->u.consoleMsgL3.SetConsoleDisplayMode; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + return m->_pApiRoutines->SetConsoleDisplayModeImpl(*pObj, a->dwFlags, a->ScreenBufferDimensions); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleDisplayMode(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleDisplayMode); + CONSOLE_GETDISPLAYMODE_MSG* const a = &m->u.consoleMsgL3.GetConsoleDisplayMode; + + // Historically this has never checked the handles. It just returns global state. + + m->_pApiRoutines->GetConsoleDisplayModeImpl(a->ModeFlags); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerAddConsoleAlias(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_ADDALIAS_MSG* const a = &m->u.consoleMsgL3.AddConsoleAliasW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::AddConsoleAlias, a->Unicode); + + // Read the input buffer and validate the strings. + PVOID pvBuffer; + ULONG cbBufferSize; + RETURN_IF_FAILED(m->GetInputBuffer(&pvBuffer, &cbBufferSize)); + + PVOID pvInputTarget; + ULONG const cbInputTarget = a->TargetLength; + PVOID pvInputExeName; + ULONG const cbInputExeName = a->ExeLength; + PVOID pvInputSource; + ULONG const cbInputSource = a->SourceLength; + RETURN_HR_IF(E_INVALIDARG, !IsValidStringBuffer(a->Unicode, + pvBuffer, + cbBufferSize, + 3, + cbInputExeName, + &pvInputExeName, + cbInputSource, + &pvInputSource, + cbInputTarget, + &pvInputTarget)); + + if (a->Unicode) + { + const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); + const std::wstring_view inputTarget(reinterpret_cast(pvInputTarget), cbInputTarget / sizeof(wchar_t)); + const std::wstring_view inputExeName(reinterpret_cast(pvInputExeName), cbInputExeName / sizeof(wchar_t)); + + return m->_pApiRoutines->AddConsoleAliasWImpl(inputSource, inputTarget, inputExeName); + } + else + { + const std::string_view inputSource(reinterpret_cast(pvInputSource), cbInputSource); + const std::string_view inputTarget(reinterpret_cast(pvInputTarget), cbInputTarget); + const std::string_view inputExeName(reinterpret_cast(pvInputExeName), cbInputExeName); + + return m->_pApiRoutines->AddConsoleAliasAImpl(inputSource, inputTarget, inputExeName); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleAlias(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_GETALIAS_MSG* const a = &m->u.consoleMsgL3.GetConsoleAliasW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleAlias, a->Unicode); + + PVOID pvInputBuffer; + ULONG cbInputBufferSize; + RETURN_IF_FAILED(m->GetInputBuffer(&pvInputBuffer, &cbInputBufferSize)); + + PVOID pvInputExe; + ULONG const cbInputExe = a->ExeLength; + PVOID pvInputSource; + ULONG const cbInputSource = a->SourceLength; + RETURN_HR_IF(E_INVALIDARG, !IsValidStringBuffer(a->Unicode, + pvInputBuffer, + cbInputBufferSize, + 2, + cbInputExe, + &pvInputExe, + cbInputSource, + &pvInputSource)); + + PVOID pvOutputBuffer; + ULONG cbOutputBufferSize; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvOutputBuffer, &cbOutputBufferSize)); + + HRESULT hr; + size_t cbWritten; + if (a->Unicode) + { + const std::wstring_view inputSource(reinterpret_cast(pvInputSource), cbInputSource / sizeof(wchar_t)); + const std::wstring_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe / sizeof(wchar_t)); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize / sizeof(wchar_t)); + size_t cchWritten; + + hr = m->_pApiRoutines->GetConsoleAliasWImpl(inputSource, outputBuffer, cchWritten, inputExeName); + + // We must set the reply length in bytes. Convert back from characters. + RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); + } + else + { + const std::string_view inputSource(reinterpret_cast(pvInputSource), cbInputSource); + const std::string_view inputExeName(reinterpret_cast(pvInputExe), cbInputExe); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBufferSize); + size_t cchWritten; + + hr = m->_pApiRoutines->GetConsoleAliasAImpl(inputSource, outputBuffer, cchWritten, inputExeName); + + cbWritten = cchWritten; + } + + // We must return the byte length of the written data in the message + RETURN_IF_FAILED(SizeTToUShort(cbWritten, &a->TargetLength)); + + m->SetReplyInformation(a->TargetLength); + + // See conlibk.lib. For any "buffer too small condition", we must send the exact status code + // NTSTATUS = STATUS_BUFFER_TOO_SMALL. If we send Win32 or HRESULT equivalents, the client library + // will zero out our DWORD return value set in a->TargetLength on our behalf. + if (ERROR_INSUFFICIENT_BUFFER == hr || + HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) == hr) + { + hr = STATUS_BUFFER_TOO_SMALL; + } + + return hr; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleAliasesLength(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_GETALIASESLENGTH_MSG const a = &m->u.consoleMsgL3.GetConsoleAliasesLengthW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleAliasesLength, a->Unicode); + + ULONG cbExeNameLength; + PVOID pvExeName; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + size_t cbAliasesLength; + if (a->Unicode) + { + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + size_t cchAliasesLength; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesLengthWImpl(inputExeName, cchAliasesLength)); + + RETURN_IF_FAILED(SizeTMult(cchAliasesLength, sizeof(wchar_t), &cbAliasesLength)); + } + else + { + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + size_t cchAliasesLength; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesLengthAImpl(inputExeName, cchAliasesLength)); + + cbAliasesLength = cchAliasesLength; + } + + RETURN_IF_FAILED(SizeTToULong(cbAliasesLength, &a->AliasesLength)); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleAliasExesLength(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_GETALIASEXESLENGTH_MSG const a = &m->u.consoleMsgL3.GetConsoleAliasExesLengthW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleAliasExesLength, a->Unicode); + + size_t cbAliasExesLength; + if (a->Unicode) + { + size_t cchAliasExesLength; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesLengthWImpl(cchAliasExesLength)); + cbAliasExesLength = cchAliasExesLength * sizeof(wchar_t); + } + else + { + size_t cchAliasExesLength; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesLengthAImpl(cchAliasExesLength)); + cbAliasExesLength = cchAliasExesLength; + } + + RETURN_IF_FAILED(SizeTToULong(cbAliasExesLength, &a->AliasExesLength)); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleAliases(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_GETALIASES_MSG* const a = &m->u.consoleMsgL3.GetConsoleAliasesW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleAliases, a->Unicode); + + PVOID pvExeName; + ULONG cbExeNameLength; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + PVOID pvOutputBuffer; + DWORD cbAliasesBufferLength; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvOutputBuffer, &cbAliasesBufferLength)); + + size_t cbWritten; + if (a->Unicode) + { + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength / sizeof(wchar_t)); + size_t cchWritten; + + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesWImpl(inputExeName, outputBuffer, cchWritten)); + + // We must set the reply length in bytes. Convert back from characters. + RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); + } + else + { + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbAliasesBufferLength); + size_t cchWritten; + + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasesAImpl(inputExeName, outputBuffer, cchWritten)); + + cbWritten = cchWritten; + } + + RETURN_IF_FAILED(SizeTToULong(cbWritten, &a->AliasesBufferLength)); + + m->SetReplyInformation(a->AliasesBufferLength); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleAliasExes(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_GETALIASEXES_MSG* const a = &m->u.consoleMsgL3.GetConsoleAliasExesW; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleAliasExes, a->Unicode); + + PVOID pvBuffer; + ULONG cbAliasExesBufferLength; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvBuffer, &cbAliasExesBufferLength)); + + size_t cbWritten; + if (a->Unicode) + { + gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength / sizeof(wchar_t)); + size_t cchWritten; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesWImpl(outputBuffer, cchWritten)); + + RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); + } + else + { + gsl::span outputBuffer(reinterpret_cast(pvBuffer), cbAliasExesBufferLength); + size_t cchWritten; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleAliasExesAImpl(outputBuffer, cchWritten)); + + cbWritten = cchWritten; + } + + // We must return the byte length of the written data in the message + RETURN_IF_FAILED(SizeTToULong(cbWritten, &a->AliasExesBufferLength)); + + m->SetReplyInformation(a->AliasExesBufferLength); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerExpungeConsoleCommandHistory(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_EXPUNGECOMMANDHISTORY_MSG* const a = &m->u.consoleMsgL3.ExpungeConsoleCommandHistoryW; + + PVOID pvExeName; + ULONG cbExeNameLength; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + if (a->Unicode) + { + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + + return m->_pApiRoutines->ExpungeConsoleCommandHistoryWImpl(inputExeName); + } + else + { + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + + return m->_pApiRoutines->ExpungeConsoleCommandHistoryAImpl(inputExeName); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleNumberOfCommands(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_SETNUMBEROFCOMMANDS_MSG* const a = &m->u.consoleMsgL3.SetConsoleNumberOfCommandsW; + PVOID pvExeName; + ULONG cbExeNameLength; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + size_t const NumberOfCommands = a->NumCommands; + if (a->Unicode) + { + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + + return m->_pApiRoutines->SetConsoleNumberOfCommandsWImpl(inputExeName, NumberOfCommands); + } + else + { + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + + return m->_pApiRoutines->SetConsoleNumberOfCommandsAImpl(inputExeName, NumberOfCommands); + } +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleCommandHistoryLength(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_GETCOMMANDHISTORYLENGTH_MSG const a = &m->u.consoleMsgL3.GetConsoleCommandHistoryLengthW; + + PVOID pvExeName; + ULONG cbExeNameLength; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + size_t cbCommandHistoryLength; + if (a->Unicode) + { + size_t cchCommandHistoryLength; + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryLengthWImpl(inputExeName, cchCommandHistoryLength)); + + // We must set the reply length in bytes. Convert back from characters. + RETURN_IF_FAILED(SizeTMult(cchCommandHistoryLength, sizeof(wchar_t), &cbCommandHistoryLength)); + } + else + { + size_t cchCommandHistoryLength; + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryLengthAImpl(inputExeName, cchCommandHistoryLength)); + + cbCommandHistoryLength = cchCommandHistoryLength; + } + + // Fit return value into structure memory size + RETURN_IF_FAILED(SizeTToULong(cbCommandHistoryLength, &a->CommandHistoryLength)); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleCommandHistory(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + PCONSOLE_GETCOMMANDHISTORY_MSG const a = &m->u.consoleMsgL3.GetConsoleCommandHistoryW; + + PVOID pvExeName; + ULONG cbExeNameLength; + RETURN_IF_FAILED(m->GetInputBuffer(&pvExeName, &cbExeNameLength)); + + PVOID pvOutputBuffer; + ULONG cbOutputBuffer; + RETURN_IF_FAILED(m->GetOutputBuffer(&pvOutputBuffer, &cbOutputBuffer)); + + size_t cbWritten; + if (a->Unicode) + { + const std::wstring_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength / sizeof(wchar_t)); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer / sizeof(wchar_t)); + size_t cchWritten; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryWImpl(inputExeName, outputBuffer, cchWritten)); + + // We must set the reply length in bytes. Convert back from characters. + RETURN_IF_FAILED(SizeTMult(cchWritten, sizeof(wchar_t), &cbWritten)); + } + else + { + const std::string_view inputExeName(reinterpret_cast(pvExeName), cbExeNameLength); + gsl::span outputBuffer(reinterpret_cast(pvOutputBuffer), cbOutputBuffer); + size_t cchWritten; + RETURN_IF_FAILED(m->_pApiRoutines->GetConsoleCommandHistoryAImpl(inputExeName, outputBuffer, cchWritten)); + + cbWritten = cchWritten; + } + + // Fit return value into structure memory size. + RETURN_IF_FAILED(SizeTToULong(cbWritten, &a->CommandBufferLength)); + + m->SetReplyInformation(a->CommandBufferLength); + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleWindow(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleWindow); + CONSOLE_GETCONSOLEWINDOW_MSG* const a = &m->u.consoleMsgL3.GetConsoleWindow; + + m->_pApiRoutines->GetConsoleWindowImpl(a->hwnd); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleSelectionInfo(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleSelectionInfo); + CONSOLE_GETSELECTIONINFO_MSG* const a = &m->u.consoleMsgL3.GetConsoleSelectionInfo; + + m->_pApiRoutines->GetConsoleSelectionInfoImpl(a->SelectionInfo); + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleHistory(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_HISTORY_MSG* const a = &m->u.consoleMsgL3.GetConsoleHistory; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleHistoryInfo); + + CONSOLE_HISTORY_INFO info; + info.cbSize = sizeof(info); + + m->_pApiRoutines->GetConsoleHistoryInfoImpl(info); + + a->dwFlags = info.dwFlags; + a->HistoryBufferSize = info.HistoryBufferSize; + a->NumberOfHistoryBuffers = info.NumberOfHistoryBuffers; + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleHistory(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_HISTORY_MSG* const a = &m->u.consoleMsgL3.SetConsoleHistory; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetConsoleHistoryInfo); + + CONSOLE_HISTORY_INFO info; + info.cbSize = sizeof(info); + info.dwFlags = a->dwFlags; + info.HistoryBufferSize = a->HistoryBufferSize; + info.NumberOfHistoryBuffers = a->NumberOfHistoryBuffers; + + return m->_pApiRoutines->SetConsoleHistoryInfoImpl(info); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerSetConsoleCurrentFont(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::SetCurrentConsoleFontEx); + CONSOLE_CURRENTFONT_MSG* const a = &m->u.consoleMsgL3.SetCurrentConsoleFont; + + ConsoleHandleData* const pObjectHandle = m->GetObjectHandle(); + RETURN_HR_IF_NULL(E_HANDLE, pObjectHandle); + + SCREEN_INFORMATION* pObj; + RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_WRITE, &pObj)); + + CONSOLE_FONT_INFOEX Info; + Info.cbSize = sizeof(Info); + Info.dwFontSize = a->FontSize; + CopyMemory(Info.FaceName, a->FaceName, RTL_NUMBER_OF_V2(Info.FaceName) * sizeof(Info.FaceName[0])); + Info.FontFamily = a->FontFamily; + Info.FontWeight = a->FontWeight; + + return m->_pApiRoutines->SetCurrentConsoleFontExImpl(*pObj, a->MaximumWindow, Info); +} diff --git a/src/server/ApiDispatchers.h b/src/server/ApiDispatchers.h new file mode 100644 index 000000000..7238e32fe --- /dev/null +++ b/src/server/ApiDispatchers.h @@ -0,0 +1,91 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApiDispatchers.h + +Abstract: +- This file decodes the client's API request message and dispatches it to the appropriate defined routine in the server. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp +--*/ + +#pragma once + +#include "IApiRoutines.h" + +#include "ApiMessage.h" + +namespace ApiDispatchers +{ + [[nodiscard]] HRESULT ServerDeprecatedApi(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + +#pragma region L1 + [[nodiscard]] HRESULT ServerGetConsoleCP(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleMode(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleMode(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetNumberOfInputEvents(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleInput(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerReadConsole(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerWriteConsole(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleLangId(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); +#pragma endregion + +#pragma region L2 + [[nodiscard]] HRESULT ServerFillConsoleOutput(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGenerateConsoleCtrlEvent(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleActiveScreenBuffer(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerFlushConsoleInputBuffer(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleCP(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleCursorInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleCursorInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleScreenBufferInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleScreenBufferInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleScreenBufferSize(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleCursorPosition(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetLargestConsoleWindowSize(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerScrollConsoleScreenBuffer(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleTextAttribute(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleWindowInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerReadConsoleOutputString(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerWriteConsoleInput(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerWriteConsoleOutput(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerWriteConsoleOutputString(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerReadConsoleOutput(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleTitle(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleTitle(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); +#pragma endregion + +#pragma region L3 + [[nodiscard]] HRESULT ServerGetConsoleMouseInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleFontSize(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleCurrentFont(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleDisplayMode(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleDisplayMode(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerAddConsoleAlias(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleAlias(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleAliasesLength(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleAliasExesLength(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleAliases(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleAliasExes(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + + // CMDEXT functions + [[nodiscard]] HRESULT ServerExpungeConsoleCommandHistory(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleNumberOfCommands(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleCommandHistoryLength(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleCommandHistory(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + // end CMDEXT functions + + [[nodiscard]] HRESULT ServerGetConsoleWindow(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleSelectionInfo(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleProcessList(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerGetConsoleHistory(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleHistory(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); + [[nodiscard]] HRESULT ServerSetConsoleCurrentFont(_Inout_ CONSOLE_API_MSG* const m, _Inout_ BOOL* const pbReplyPending); +#pragma endregion +}; diff --git a/src/server/ApiDispatchersInternal.cpp b/src/server/ApiDispatchersInternal.cpp new file mode 100644 index 000000000..97efc5dd1 --- /dev/null +++ b/src/server/ApiDispatchersInternal.cpp @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiDispatchers.h" + +#include "..\host\globals.h" +#include "..\host\handle.h" +#include "..\host\server.h" +#include "..\host\telemetry.hpp" + +#include "..\host\ntprivapi.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +[[nodiscard]] +HRESULT ApiDispatchers::ServerDeprecatedApi(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + // log if we hit a deprecated API. + RETURN_HR_MSG(E_NOTIMPL, "Deprecated API attempted: 0x%08x", m->Descriptor.Function); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleProcessList(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + PCONSOLE_GETCONSOLEPROCESSLIST_MSG const a = &m->u.consoleMsgL3.GetConsoleProcessList; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleProcessList); + + PVOID Buffer; + ULONG BufferSize; + RETURN_IF_FAILED(m->GetOutputBuffer(&Buffer, &BufferSize)); + + a->dwProcessCount = BufferSize / sizeof(ULONG); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + /* + * If there's not enough space in the array to hold all the pids, we'll + * inform the user of that by returning a number > than a->dwProcessCount + * (but we still return S_OK). + */ + + LPDWORD lpdwProcessList = (PDWORD)Buffer; + size_t cProcessList = a->dwProcessCount; + if (SUCCEEDED(gci.ProcessHandleList.GetProcessList(lpdwProcessList, &cProcessList))) + { + m->SetReplyInformation(cProcessList * sizeof(ULONG)); + } + + a->dwProcessCount = (ULONG)cProcessList; + + return S_OK; +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGetConsoleLangId(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_LANGID_MSG* const a = &m->u.consoleMsgL1.GetConsoleLangId; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GetConsoleLangId); + + // TODO: MSFT: 9115192 - This should probably just ask through GetOutputCP and convert it ourselves on this side. + return m->_pApiRoutines->GetConsoleLangIdImpl(a->LangId); +} + +[[nodiscard]] +HRESULT ApiDispatchers::ServerGenerateConsoleCtrlEvent(_Inout_ CONSOLE_API_MSG * const m, _Inout_ BOOL* const /*pbReplyPending*/) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + CONSOLE_CTRLEVENT_MSG* const a = &m->u.consoleMsgL2.GenerateConsoleCtrlEvent; + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::GenerateConsoleCtrlEvent); + + LockConsole(); + auto Unlock = wil::scope_exit([&] { UnlockConsole(); }); + + // Make sure the process group id is valid. + if (a->ProcessGroupId != 0) + { + ConsoleProcessHandle* ProcessHandle; + ProcessHandle = gci.ProcessHandleList.FindProcessByGroupId(a->ProcessGroupId); + if (ProcessHandle == nullptr) + { + ULONG ProcessId = a->ProcessGroupId; + + // We didn't find a process with that group ID. + // Let's see if the process with that ID exists and has a parent that is a member of this console. + RETURN_IF_NTSTATUS_FAILED((NtPrivApi::s_GetProcessParentId(&ProcessId))); + ProcessHandle = gci.ProcessHandleList.FindProcessInList(ProcessId); + RETURN_HR_IF_NULL(E_INVALIDARG, ProcessHandle); + RETURN_IF_FAILED(gci.ProcessHandleList.AllocProcessData(a->ProcessGroupId, + 0, + a->ProcessGroupId, + ProcessHandle, + nullptr)); + } + } + + gci.LimitingProcessId = a->ProcessGroupId; + HandleCtrlEvent(a->CtrlEvent); + + return S_OK; +} diff --git a/src/server/ApiMessage.cpp b/src/server/ApiMessage.cpp new file mode 100644 index 000000000..b3c940abd --- /dev/null +++ b/src/server/ApiMessage.cpp @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include + +#include "ApiMessage.h" +#include "DeviceComm.h" + +_CONSOLE_API_MSG::_CONSOLE_API_MSG() : + _pDeviceComm(nullptr), + _pApiRoutines(nullptr) +{ + ZeroMemory(this, sizeof(_CONSOLE_API_MSG)); +} + +ConsoleProcessHandle* _CONSOLE_API_MSG::GetProcessHandle() const +{ + return reinterpret_cast(Descriptor.Process); +} + +ConsoleHandleData* _CONSOLE_API_MSG::GetObjectHandle() const +{ + return reinterpret_cast(Descriptor.Object); +} + +// Routine Description: +// - This routine reads some or all of the input payload of the given message (depending on the given offset). +// Arguments: +// - cbOffset - Supplies the offset in bytes from which to start reading the payload. +// - pvBuffer - Receives the payload. +// - cbSize - Supplies the number of bytes to be read into the buffer. +// Return Value: +// - HRESULT indicating if the payload was successfully read. +[[nodiscard]] +HRESULT _CONSOLE_API_MSG::ReadMessageInput(const ULONG cbOffset, + _Out_writes_bytes_(cbSize) PVOID pvBuffer, + const ULONG cbSize) +{ + CD_IO_OPERATION IoOperation; + IoOperation.Identifier = Descriptor.Identifier; + IoOperation.Buffer.Offset = State.ReadOffset + cbOffset; + IoOperation.Buffer.Data = pvBuffer; + IoOperation.Buffer.Size = cbSize; + + return _pDeviceComm->ReadInput(&IoOperation); +} + +// Routine Description: +// - This routine retrieves the input buffer associated with this message. It will allocate one if needed. +// - Before completing the message, ReleaseMessageBuffers must be called to free any allocation performed by this routine. +// Arguments: +// - Message - Supplies the message whose input buffer will be retrieved. +// - Buffer - Receives a pointer to the input buffer. +// - Size - Receives the size, in bytes, of the input buffer. +// Return Value: +// - HRESULT indicating if the input buffer was successfully retrieved. +[[nodiscard]] +HRESULT _CONSOLE_API_MSG::GetInputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, + _Out_ ULONG* const pcbSize) +{ + // Initialize the buffer if it hasn't been initialized yet. + if (State.InputBuffer == nullptr) + { + RETURN_HR_IF(E_FAIL, State.ReadOffset > Descriptor.InputSize); + + ULONG const cbReadSize = Descriptor.InputSize - State.ReadOffset; + + wistd::unique_ptr pPayload = wil::make_unique_nothrow(cbReadSize); + RETURN_IF_NULL_ALLOC(pPayload); + + RETURN_IF_FAILED(ReadMessageInput(0, pPayload.get(), cbReadSize)); + + State.InputBuffer = pPayload.release(); // TODO: MSFT: 9565140 - don't release, maintain as smart pointer. + State.InputBufferSize = cbReadSize; + } + + // Return the buffer. + *ppvBuffer = State.InputBuffer; + *pcbSize = State.InputBufferSize; + + return S_OK; +} + +// Routine Description: +// - This routine retrieves the output buffer associated with this message. It will allocate one if needed. +// The allocated will be bigger than the actual output size by the requested factor. +// - Before completing the message, ReleaseMessageBuffers must be called to free any allocation performed by this routine. +// Arguments: +// - Factor - Supplies the factor to multiply the allocated buffer by. +// - Buffer - Receives a pointer to the output buffer. +// - Size - Receives the size, in bytes, of the output buffer. +// Return Value: +// - HRESULT indicating if the output buffer was successfully retrieved. +[[nodiscard]] +HRESULT _CONSOLE_API_MSG::GetAugmentedOutputBuffer(const ULONG cbFactor, + _Outptr_result_bytebuffer_(*pcbSize) PVOID * const ppvBuffer, + _Out_ PULONG pcbSize) +{ + // Initialize the buffer if it hasn't been initialized yet. + if (State.OutputBuffer == nullptr) + { + RETURN_HR_IF(E_FAIL, State.WriteOffset > Descriptor.OutputSize); + + ULONG cbWriteSize = Descriptor.OutputSize - State.WriteOffset; + RETURN_IF_FAILED(ULongMult(cbWriteSize, cbFactor, &cbWriteSize)); + + BYTE* pPayload = new(std::nothrow) BYTE[cbWriteSize]; + RETURN_IF_NULL_ALLOC(pPayload); + ZeroMemory(pPayload, sizeof(BYTE) * cbWriteSize); + + State.OutputBuffer = pPayload; // TODO: MSFT: 9565140 - maintain as smart pointer. + State.OutputBufferSize = cbWriteSize; + } + + // Return the buffer. + *ppvBuffer = State.OutputBuffer; + *pcbSize = State.OutputBufferSize; + + return S_OK; +} + +// Routine Description: +// - This routine retrieves the output buffer associated with this message. It will allocate one if needed. +// - Before completing the message, ReleaseMessageBuffers must be called to free any allocation performed by this routine. +// Arguments: +// - Message - Supplies the message whose output buffer will be retrieved. +// - Buffer - Receives a pointer to the output buffer. +// - Size - Receives the size, in bytes, of the output buffer. +// Return Value: +// - HRESULT indicating if the output buffer was successfully retrieved. +[[nodiscard]] +HRESULT _CONSOLE_API_MSG::GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, + _Out_ ULONG * const pcbSize) +{ + return GetAugmentedOutputBuffer(1, ppvBuffer, pcbSize); +} + +// Routine Description: +// - This routine releases output or input buffers that might have been allocated +// during the processing of the given message. If the current completion status +// of the message indicates success, this routine also writes the output buffer +// (if any) to the message. +// Arguments: +// - +// Return Value: +// - HRESULT indicating if the payload was successfully written if applicable. +[[nodiscard]] +HRESULT _CONSOLE_API_MSG::ReleaseMessageBuffers() +{ + HRESULT hr = S_OK; + + if (State.InputBuffer != nullptr) + { + delete[] State.InputBuffer; + State.InputBuffer = nullptr; + } + + if (State.OutputBuffer != nullptr) + { + if (NT_SUCCESS(Complete.IoStatus.Status)) + { + CD_IO_OPERATION IoOperation; + IoOperation.Identifier = Descriptor.Identifier; + IoOperation.Buffer.Offset = State.WriteOffset; + IoOperation.Buffer.Data = State.OutputBuffer; + IoOperation.Buffer.Size = (ULONG)Complete.IoStatus.Information; + + LOG_IF_FAILED(_pDeviceComm->WriteOutput(&IoOperation)); + } + + delete[] State.OutputBuffer; + State.OutputBuffer = nullptr; + } + + return hr; +} + +void _CONSOLE_API_MSG::SetReplyStatus(const NTSTATUS Status) +{ + Complete.IoStatus.Status = Status; +} + +void _CONSOLE_API_MSG::SetReplyInformation(const ULONG_PTR pInformation) +{ + Complete.IoStatus.Information = pInformation; +} diff --git a/src/server/ApiMessage.h b/src/server/ApiMessage.h new file mode 100644 index 000000000..e86b3a189 --- /dev/null +++ b/src/server/ApiMessage.h @@ -0,0 +1,81 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApiMessage.h + +Abstract: +- This file extends the published structure of an API message to provide encapsulation and helper methods + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in util.cpp & conapi.h & csrutil.cpp +--*/ + +#pragma once + +#include "ApiMessageState.h" +#include "IApiRoutines.h" + +class ConsoleProcessHandle; +class ConsoleHandleData; + +class DeviceComm; + +typedef struct _CONSOLE_API_MSG +{ + _CONSOLE_API_MSG(); + + CD_IO_COMPLETE Complete; + CONSOLE_API_STATE State; + + DeviceComm* _pDeviceComm; + IApiRoutines* _pApiRoutines; + + // From here down is the actual packet data sent/received. + CD_IO_DESCRIPTOR Descriptor; + union + { + struct + { + CD_CREATE_OBJECT_INFORMATION CreateObject; + CONSOLE_CREATESCREENBUFFER_MSG CreateScreenBuffer; + }; + struct + { + CONSOLE_MSG_HEADER msgHeader; + union + { + CONSOLE_MSG_BODY_L1 consoleMsgL1; + CONSOLE_MSG_BODY_L2 consoleMsgL2; + CONSOLE_MSG_BODY_L3 consoleMsgL3; + } u; + }; + }; + // End packet data + +public: + ConsoleProcessHandle* GetProcessHandle() const; + ConsoleHandleData* GetObjectHandle() const; + + [[nodiscard]] + HRESULT ReadMessageInput(const ULONG cbOffset, _Out_writes_bytes_(cbSize) PVOID pvBuffer, const ULONG cbSize); + [[nodiscard]] + HRESULT GetAugmentedOutputBuffer(const ULONG cbFactor, + _Outptr_result_bytebuffer_(*pcbSize) PVOID * ppvBuffer, + _Out_ PULONG pcbSize); + [[nodiscard]] + HRESULT GetOutputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); + [[nodiscard]] + HRESULT GetInputBuffer(_Outptr_result_bytebuffer_(*pcbSize) void** const ppvBuffer, _Out_ ULONG* const pcbSize); + + [[nodiscard]] + HRESULT ReleaseMessageBuffers(); + + void SetReplyStatus(const NTSTATUS Status); + void SetReplyInformation(const ULONG_PTR pInformation); + +} CONSOLE_API_MSG, *PCONSOLE_API_MSG, *const PCCONSOLE_API_MSG; diff --git a/src/server/ApiMessageState.cpp b/src/server/ApiMessageState.cpp new file mode 100644 index 000000000..13a5dbb1a --- /dev/null +++ b/src/server/ApiMessageState.cpp @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiMessageState.h" + diff --git a/src/server/ApiMessageState.h b/src/server/ApiMessageState.h new file mode 100644 index 000000000..0259ce0ae --- /dev/null +++ b/src/server/ApiMessageState.h @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApiMessageState.h + +Abstract: +- This file extends the published structure of an API message's state to provide encapsulation and helper methods + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in util.h & conapi.h +--*/ + +#pragma once + +typedef struct _CONSOLE_API_STATE +{ + ULONG WriteOffset; + ULONG ReadOffset; + ULONG InputBufferSize; + ULONG OutputBufferSize; + PVOID InputBuffer; + PVOID OutputBuffer; +} CONSOLE_API_STATE, *PCONSOLE_API_STATE, *const PCCONSOLE_API_STATE; diff --git a/src/server/ApiSorter.cpp b/src/server/ApiSorter.cpp new file mode 100644 index 000000000..f04d3dbf4 --- /dev/null +++ b/src/server/ApiSorter.cpp @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ApiSorter.h" + +#include "ApiDispatchers.h" + +#include "../host/tracing.hpp" + +#define CONSOLE_API_STRUCT(Routine, Struct, TraceName) { Routine, sizeof(Struct), TraceName } +#define CONSOLE_API_NO_PARAMETER(Routine, TraceName) { Routine, 0, TraceName } + +#define CONSOLE_API_DEPRECATED(Struct) { ApiDispatchers::ServerDeprecatedApi, sizeof(Struct), "Deprecated"} +#define CONSOLE_API_DEPRECATED_NO_PARAM() {ApiDispatchers::ServerDeprecatedApi, 0, "Deprecated"} + +typedef struct _CONSOLE_API_DESCRIPTOR +{ + PCONSOLE_API_ROUTINE Routine; + ULONG RequiredSize; + PCSTR TraceName; +} CONSOLE_API_DESCRIPTOR, *PCONSOLE_API_DESCRIPTOR; + +typedef struct _CONSOLE_API_LAYER_DESCRIPTOR +{ + const CONSOLE_API_DESCRIPTOR *Descriptor; + ULONG Count; +} CONSOLE_API_LAYER_DESCRIPTOR, *PCONSOLE_API_LAYER_DESCRIPTOR; + +const CONSOLE_API_DESCRIPTOR ConsoleApiLayer1[] = { + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleCP, CONSOLE_GETCP_MSG, "GetConsoleCP"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleMode, CONSOLE_MODE_MSG, "GetConsoleMode"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleMode, CONSOLE_MODE_MSG, "SetConsoleMode"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetNumberOfInputEvents, CONSOLE_GETNUMBEROFINPUTEVENTS_MSG, "GetNumberOfConsoleInputEvents"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleInput, CONSOLE_GETCONSOLEINPUT_MSG, "GetConsoleInput"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerReadConsole, CONSOLE_READCONSOLE_MSG, "ReadConsole"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerWriteConsole, CONSOLE_WRITECONSOLE_MSG, "WriteConsole"), + CONSOLE_API_DEPRECATED_NO_PARAM(), // ApiDispatchers::ServerConsoleNotifyLastClose + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleLangId, CONSOLE_LANGID_MSG, "GetConsoleLangId"), + CONSOLE_API_DEPRECATED(CONSOLE_MAPBITMAP_MSG), +}; + +const CONSOLE_API_DESCRIPTOR ConsoleApiLayer2[] = { + CONSOLE_API_STRUCT(ApiDispatchers::ServerFillConsoleOutput, CONSOLE_FILLCONSOLEOUTPUT_MSG, "FillConsoleOutput"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGenerateConsoleCtrlEvent, CONSOLE_CTRLEVENT_MSG, "GenerateConsoleCtrlEvent"), + CONSOLE_API_NO_PARAMETER(ApiDispatchers::ServerSetConsoleActiveScreenBuffer, "SetConsoleActiveScreenBuffer"), + CONSOLE_API_NO_PARAMETER(ApiDispatchers::ServerFlushConsoleInputBuffer, "FlushConsoleInputBuffer"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleCP, CONSOLE_SETCP_MSG, "SetConsoleCP"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleCursorInfo, CONSOLE_GETCURSORINFO_MSG, "GetConsoleCursorInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleCursorInfo, CONSOLE_SETCURSORINFO_MSG, "SetConsoleCursorInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleScreenBufferInfo, CONSOLE_SCREENBUFFERINFO_MSG, "GetConsoleScreenBufferInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleScreenBufferInfo, CONSOLE_SCREENBUFFERINFO_MSG, "SetConsoleScreenBufferInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleScreenBufferSize, CONSOLE_SETSCREENBUFFERSIZE_MSG, "SetConsoleScreenBufferSize"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleCursorPosition, CONSOLE_SETCURSORPOSITION_MSG, "SetConsoleCursorPosition"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetLargestConsoleWindowSize, CONSOLE_GETLARGESTWINDOWSIZE_MSG, "GetLargestConsoleWindowSize"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerScrollConsoleScreenBuffer, CONSOLE_SCROLLSCREENBUFFER_MSG, "ScrollConsoleScreenBuffer"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleTextAttribute, CONSOLE_SETTEXTATTRIBUTE_MSG, "SetConsoleTextAttribute"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleWindowInfo, CONSOLE_SETWINDOWINFO_MSG, "SetConsoleWindowInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerReadConsoleOutputString, CONSOLE_READCONSOLEOUTPUTSTRING_MSG, "ReadConsoleOutputString"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerWriteConsoleInput, CONSOLE_WRITECONSOLEINPUT_MSG, "WriteConsoleInput"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerWriteConsoleOutput, CONSOLE_WRITECONSOLEOUTPUT_MSG, "WriteConsoleOutput"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerWriteConsoleOutputString, CONSOLE_WRITECONSOLEOUTPUTSTRING_MSG, "WriteConsoleOutputString"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerReadConsoleOutput, CONSOLE_READCONSOLEOUTPUT_MSG, "ReadConsoleOutput"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleTitle, CONSOLE_GETTITLE_MSG, "GetConsoleTitle"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleTitle, CONSOLE_SETTITLE_MSG, "SetConsoleTitle"), +}; + +const CONSOLE_API_DESCRIPTOR ConsoleApiLayer3[] = { + CONSOLE_API_DEPRECATED(CONSOLE_GETNUMBEROFFONTS_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleMouseInfo, CONSOLE_GETMOUSEINFO_MSG, "GetNumberOfConsoleMouseButtons"), + CONSOLE_API_DEPRECATED(CONSOLE_GETFONTINFO_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleFontSize, CONSOLE_GETFONTSIZE_MSG, "GetConsoleFontSize"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleCurrentFont, CONSOLE_CURRENTFONT_MSG, "GetCurrentConsoleFont"), + CONSOLE_API_DEPRECATED(CONSOLE_SETFONT_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETICON_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_INVALIDATERECT_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_VDM_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETCURSOR_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SHOWCURSOR_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_MENUCONTROL_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETPALETTE_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleDisplayMode, CONSOLE_SETDISPLAYMODE_MSG, "SetConsoleDisplayMode"), + CONSOLE_API_DEPRECATED(CONSOLE_REGISTERVDM_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_GETHARDWARESTATE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETHARDWARESTATE_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleDisplayMode, CONSOLE_GETDISPLAYMODE_MSG, "GetConsoleDisplayMode"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerAddConsoleAlias, CONSOLE_ADDALIAS_MSG, "AddConsoleAlias"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleAlias, CONSOLE_GETALIAS_MSG, "GetConsoleAlias"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleAliasesLength, CONSOLE_GETALIASESLENGTH_MSG, "GetConsoleAliasesLength"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleAliasExesLength, CONSOLE_GETALIASEXESLENGTH_MSG, "GetConsoleAliasExesLength"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleAliases, CONSOLE_GETALIASES_MSG, "GetConsoleAliases"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleAliasExes, CONSOLE_GETALIASEXES_MSG, "GetConsoleAliasExes"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerExpungeConsoleCommandHistory, CONSOLE_EXPUNGECOMMANDHISTORY_MSG, "ExpungeConsoleCommandHistory"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleNumberOfCommands, CONSOLE_SETNUMBEROFCOMMANDS_MSG, "SetConsoleNumberOfCommands"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleCommandHistoryLength, CONSOLE_GETCOMMANDHISTORYLENGTH_MSG, "GetConsoleCommandHistoryLength"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleCommandHistory, CONSOLE_GETCOMMANDHISTORY_MSG, "GetConsoleCommandHistory"), + CONSOLE_API_DEPRECATED(CONSOLE_SETKEYSHORTCUTS_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETMENUCLOSE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_GETKEYBOARDLAYOUTNAME_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleWindow, CONSOLE_GETCONSOLEWINDOW_MSG, "GetConsoleWindow"), + CONSOLE_API_DEPRECATED(CONSOLE_CHAR_TYPE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_LOCAL_EUDC_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_CURSOR_MODE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_CURSOR_MODE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_REGISTEROS2_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_SETOS2OEMFORMAT_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_NLS_MODE_MSG), + CONSOLE_API_DEPRECATED(CONSOLE_NLS_MODE_MSG), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleSelectionInfo, CONSOLE_GETSELECTIONINFO_MSG, "GetConsoleSelectionInfo"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleProcessList, CONSOLE_GETCONSOLEPROCESSLIST_MSG, "GetConsoleProcessList"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerGetConsoleHistory, CONSOLE_HISTORY_MSG, "GetConsoleHistory"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleHistory, CONSOLE_HISTORY_MSG, "SetConsoleHistory"), + CONSOLE_API_STRUCT(ApiDispatchers::ServerSetConsoleCurrentFont, CONSOLE_CURRENTFONT_MSG, "SetConsoleCurrentFont") +}; + +const CONSOLE_API_LAYER_DESCRIPTOR ConsoleApiLayerTable[] = { + { ConsoleApiLayer1, RTL_NUMBER_OF(ConsoleApiLayer1) }, + { ConsoleApiLayer2, RTL_NUMBER_OF(ConsoleApiLayer2) }, + { ConsoleApiLayer3, RTL_NUMBER_OF(ConsoleApiLayer3) }, +}; + +// Routine Description: +// - This routine validates a user IO and dispatches it to the appropriate worker routine. +// Arguments: +// - Message - Supplies the message representing the user IO. +// Return Value: +// - A pointer to the reply message, if this message is to be completed inline; nullptr if this message will pend now and complete later. +PCONSOLE_API_MSG ApiSorter::ConsoleDispatchRequest(_Inout_ PCONSOLE_API_MSG Message) +{ + // Make sure the indices are valid and retrieve the API descriptor. + ULONG const LayerNumber = (Message->msgHeader.ApiNumber >> 24) - 1; + ULONG const ApiNumber = Message->msgHeader.ApiNumber & 0xffffff; + + NTSTATUS Status; + if ((LayerNumber >= RTL_NUMBER_OF(ConsoleApiLayerTable)) || (ApiNumber >= ConsoleApiLayerTable[LayerNumber].Count)) + { + Status = STATUS_ILLEGAL_FUNCTION; + goto Complete; + } + + CONSOLE_API_DESCRIPTOR const *Descriptor = &ConsoleApiLayerTable[LayerNumber].Descriptor[ApiNumber]; + + // Validate the argument size and call the API. + if ((Message->Descriptor.InputSize < sizeof(CONSOLE_MSG_HEADER)) || + (Message->msgHeader.ApiDescriptorSize > sizeof(Message->u)) || + (Message->msgHeader.ApiDescriptorSize > Message->Descriptor.InputSize - sizeof(CONSOLE_MSG_HEADER)) || + (Message->msgHeader.ApiDescriptorSize < Descriptor->RequiredSize)) + { + Status = STATUS_ILLEGAL_FUNCTION; + goto Complete; + } + + BOOL ReplyPending = FALSE; + Message->Complete.Write.Data = &Message->u; + Message->Complete.Write.Size = Message->msgHeader.ApiDescriptorSize; + Message->State.WriteOffset = Message->msgHeader.ApiDescriptorSize; + Message->State.ReadOffset = Message->msgHeader.ApiDescriptorSize + sizeof(CONSOLE_MSG_HEADER); + + // Unfortunately, we can't be as clear-cut with our error codes as we'd like since we have some callers that take + // hard dependencies on NTSTATUS codes that aren't readily expressible as an HRESULT. There's currently only one + // such known code -- STATUS_BUFFER_TOO_SMALL. There's a conlibk dependency on this being returned from the console + // alias API. + { + const auto trace = Tracing::s_TraceApiCall(Status, Descriptor->TraceName); + Status = (*Descriptor->Routine)(Message, &ReplyPending); + } + if (Status != STATUS_BUFFER_TOO_SMALL) + { + Status = NTSTATUS_FROM_HRESULT(Status); + } + + if (!ReplyPending) + { + goto Complete; + } + + return nullptr; + +Complete: + + Message->SetReplyStatus(Status); + + return Message; +} diff --git a/src/server/ApiSorter.h b/src/server/ApiSorter.h new file mode 100644 index 000000000..fe7ab947a --- /dev/null +++ b/src/server/ApiSorter.h @@ -0,0 +1,43 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ApiSorter.h + +Abstract: +- This file sorts out the various console host serviceable APIs. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp +--*/ + +#pragma once + +#include "ApiMessage.h" + +typedef HRESULT(*PCONSOLE_API_ROUTINE) (_Inout_ PCONSOLE_API_MSG m, _Inout_ PBOOL ReplyPending); + +// These are required for wait routines to accurately identify which function is waited on and needs to be dispatched later. +// It's stored here so it can be easily aligned with the layer descriptions below. +// 0x01 stands for level 1 API (layers are 1-based) +// 0x000004 stands for the 5th one down in the layer structure (call IDs are 0-based) +#define API_NUMBER_GETCONSOLEINPUT 0x01000004 +#define API_NUMBER_READCONSOLE 0x01000005 +#define API_NUMBER_WRITECONSOLE 0x01000006 + + +class ApiSorter +{ +public: + // Routine Description: + // - This routine validates a user IO and dispatches it to the appropriate worker routine. + // Arguments: + // - Message - Supplies the message representing the user IO. + // Return Value: + // - A pointer to the reply message, if this message is to be completed inline; nullptr if this message will pend now and complete later. + static PCONSOLE_API_MSG ConsoleDispatchRequest(_Inout_ PCONSOLE_API_MSG Message); +}; diff --git a/src/server/DeviceComm.cpp b/src/server/DeviceComm.cpp new file mode 100644 index 000000000..6d37be560 --- /dev/null +++ b/src/server/DeviceComm.cpp @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "DeviceComm.h" + +DeviceComm::DeviceComm(_In_ HANDLE Server) : + _Server(Server) +{ + THROW_HR_IF(E_HANDLE, Server == INVALID_HANDLE_VALUE); +} + + +DeviceComm::~DeviceComm() +{ +} + +// Routine Description: +// - Needs to be called once per server session and typically as the absolute first operation. +// - This sets up the driver with the input event that it will need to coordinate with when client +// applications attempt to read data and need to be blocked. (It will be the signal to unblock those clients.) +// Arguments: +// - pServerInfo - Structure containing information required to initialize driver state for this console connection. +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::SetServerInformation(_In_ CD_IO_SERVER_INFORMATION* const pServerInfo) const +{ + return _CallIoctl(IOCTL_CONDRV_SET_SERVER_INFORMATION, + pServerInfo, + sizeof(*pServerInfo), + nullptr, + 0); +} + +// Routine Description: +// - Retrieves a packet message from the driver representing the next action/activity that should be performed. +// Arguments: +// - pCompletion - Optional completion structure from the previous activity (can be used in lieu of calling CompleteIo seperately.) +// - pMessage - A structure to hold the message data retrieved from the driver. +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::ReadIo(_In_opt_ CD_IO_COMPLETE* const pCompletion, + _Out_ CONSOLE_API_MSG* const pMessage) const +{ + HRESULT hr = _CallIoctl(IOCTL_CONDRV_READ_IO, + pCompletion, + pCompletion == nullptr ? 0 : sizeof(*pCompletion), + &pMessage->Descriptor, + sizeof(CONSOLE_API_MSG) - FIELD_OFFSET(CONSOLE_API_MSG, Descriptor)); + + if (hr == HRESULT_FROM_WIN32(ERROR_IO_PENDING)) + { + WaitForSingleObjectEx(_Server.get(), 0, FALSE); + hr = S_OK; // TODO: MSFT: 9115192 - ??? This isn't really relevant anymore with a switch from NtDeviceIoControlFile to DeviceIoControl... + } + + return hr; +} + +// Routine Description: +// - Marks an action/activity as completed to the driver so control/responses can be returned to the client application. +// Arguments: +// - pCompletion - Completion structure from the previous activity (can be used in lieu of calling CompleteIo seperately.) +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::CompleteIo(_In_ CD_IO_COMPLETE* const pCompletion) const +{ + return _CallIoctl(IOCTL_CONDRV_COMPLETE_IO, + pCompletion, + sizeof(*pCompletion), + nullptr, + 0); +} + +// Routine Description: +// - Used to retrieve any buffered input data related to an action/activity message. +// Arguments: +// - pIoOperation - Structure containing the identifier matching the action/activity message and containing a suitable buffer space +// to hold retrieved buffered input data from the client application. +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::ReadInput(_In_ CD_IO_OPERATION* const pIoOperation) const +{ + return _CallIoctl(IOCTL_CONDRV_READ_INPUT, + pIoOperation, + sizeof(*pIoOperation), + nullptr, + 0); +} + +// Routine Description: +// - Used to return any buffered output data related to an action/activity message. +// Arguments: +// - pIoOperation - Structure containing the identifier matching the action/activity message and containing a suitable buffer space +// to hold buffered output data to be sent to the client application. +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::WriteOutput(_In_ CD_IO_OPERATION* const pIoOperation) const +{ + return _CallIoctl(IOCTL_CONDRV_WRITE_OUTPUT, + pIoOperation, + sizeof(*pIoOperation), + nullptr, + 0); +} + +// Routine Description: +// - To be called when the console instantiates UI to permit low-level UIAccess patterns to be used for retrieval of +// accessibility data from the console session. +// Arguments: +// - +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::AllowUIAccess() const +{ + return _CallIoctl(IOCTL_CONDRV_ALLOW_VIA_UIACCESS, + nullptr, + 0, + nullptr, + 0); +} + +// Routine Description: +// - For internal use. This function will send the appropriate control code verb and buffers to the driver and return a result. +// - Usage of the optional buffers depends on which verb is sent and is specific to the particular driver and its protocol. +// Arguments: +// - dwIoControlCode - The action code to send to the driver +// - pInBuffer - An optional buffer to send as input with the verb. Usage depends on the control code. +// - cbInBufferSize - The length in bytes of the optional input buffer. +// - pOutBuffer - An optional buffer to send as output with the verb. Usage depends on the control code. +// - cbOutBufferSize - The length in bytes of the optional output buffer. +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT DeviceComm::_CallIoctl(_In_ DWORD dwIoControlCode, + _In_reads_bytes_opt_(cbInBufferSize) PVOID pInBuffer, + _In_ DWORD cbInBufferSize, + _Out_writes_bytes_opt_(cbOutBufferSize) PVOID pOutBuffer, + _In_ DWORD cbOutBufferSize) const +{ + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx + // Written is unused but cannot be nullptr because we aren't using overlapped. + DWORD cbWritten = 0; + RETURN_IF_WIN32_BOOL_FALSE(DeviceIoControl(_Server.get(), + dwIoControlCode, + pInBuffer, + cbInBufferSize, + pOutBuffer, + cbOutBufferSize, + &cbWritten, + nullptr)); + + return S_OK; +} diff --git a/src/server/DeviceComm.h b/src/server/DeviceComm.h new file mode 100644 index 000000000..4c90132f4 --- /dev/null +++ b/src/server/DeviceComm.h @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DeviceComm.h + +Abstract: +- This module assists in communicating via IOCTL messages to and from a Device server handle. + +Author: +- Michael Niksa (MiNiksa) 14-Sept-2016 + +Revision History: +--*/ + +#pragma once + +#include "..\host\conapi.h" + +#include + +class DeviceComm +{ +public: + DeviceComm(_In_ HANDLE Server); + ~DeviceComm(); + + [[nodiscard]] + HRESULT SetServerInformation(_In_ CD_IO_SERVER_INFORMATION* const pServerInfo) const; + [[nodiscard]] + HRESULT ReadIo(_In_opt_ CD_IO_COMPLETE* const pCompletion, + _Out_ CONSOLE_API_MSG* const pMessage) const; + [[nodiscard]] + HRESULT CompleteIo(_In_ CD_IO_COMPLETE* const pCompletion) const; + + [[nodiscard]] + HRESULT ReadInput(_In_ CD_IO_OPERATION* const pIoOperation) const; + [[nodiscard]] + HRESULT WriteOutput(_In_ CD_IO_OPERATION* const pIoOperation) const; + + [[nodiscard]] + HRESULT AllowUIAccess() const; + +private: + + [[nodiscard]] + HRESULT _CallIoctl(_In_ DWORD dwIoControlCode, + _In_reads_bytes_opt_(cbInBufferSize) PVOID pInBuffer, + _In_ DWORD cbInBufferSize, + _Out_writes_bytes_opt_(cbOutBufferSize) PVOID pOutBuffer, + _In_ DWORD cbOutBufferSize) const; + + wil::unique_handle _Server; + +}; diff --git a/src/server/DeviceHandle.cpp b/src/server/DeviceHandle.cpp new file mode 100644 index 000000000..c4a11fc03 --- /dev/null +++ b/src/server/DeviceHandle.cpp @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "DeviceHandle.h" + +#include "WinNTControl.h" + +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 + +/*++ +Routine Description: +- This routine creates a handle to an input or output client of the given + server. No control io is sent to the server as this request must be coming + from the server itself. + +Arguments: +- Handle - Receives a handle to the new client. +- ServerHandle - Supplies a handle to the server to which to attach the + newly created client. +- Name - Supplies the name of the client object. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the client was successfully created. +--*/ +[[nodiscard]] +NTSTATUS +DeviceHandle::CreateClientHandle( + _Out_ PHANDLE Handle, + _In_ HANDLE ServerHandle, + _In_ PCWSTR Name, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + Name, + GENERIC_WRITE | GENERIC_READ | SYNCHRONIZE, + ServerHandle, + Inheritable, + FILE_SYNCHRONOUS_IO_NONALERT); +} + +/*++ +Routine Description: +- This routine creates a new server on the driver and returns a handle to it. + +Arguments: +- Handle - Receives a handle to the new server. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the console was successfully created. +--*/ +[[nodiscard]] +NTSTATUS +DeviceHandle::CreateServerHandle( + _Out_ PHANDLE Handle, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + L"\\Device\\ConDrv\\Server", + GENERIC_ALL, + NULL, + Inheritable, + 0); +} + +/*++ +Routine Description: +- This routine opens a handle to the console driver. + +Arguments: +- Handle - Receives the handle. +- DeviceName - Supplies the name to be used to open the console driver. +- DesiredAccess - Supplies the desired access mask. +- Parent - Optionally supplies the parent object. +- Inheritable - Supplies a boolean indicating if the new handle is to be made inheritable. +- OpenOptions - Supplies the open options to be passed to NtOpenFile. A common + option for clients is FILE_SYNCHRONOUS_IO_NONALERT, to make the handle + synchronous. + +Return Value: +- NTSTATUS indicating if the handle was successfully created. +--*/ +[[nodiscard]] +NTSTATUS +DeviceHandle::_CreateHandle( + _Out_ PHANDLE Handle, + _In_ PCWSTR DeviceName, + _In_ ACCESS_MASK DesiredAccess, + _In_opt_ HANDLE Parent, + _In_ BOOLEAN Inheritable, + _In_ ULONG OpenOptions) + +{ + ULONG Flags = OBJ_CASE_INSENSITIVE; + + if (Inheritable) + { + WI_SetFlag(Flags, OBJ_INHERIT); + } + + UNICODE_STRING Name; + Name.Buffer = (wchar_t*)DeviceName; + Name.Length = (USHORT)(wcslen(DeviceName) * sizeof(wchar_t)); + Name.MaximumLength = Name.Length + sizeof(wchar_t); + + OBJECT_ATTRIBUTES ObjectAttributes; + InitializeObjectAttributes(&ObjectAttributes, + &Name, + Flags, + Parent, + NULL); + + IO_STATUS_BLOCK IoStatus; + return WinNTControl::NtOpenFile(Handle, + DesiredAccess, + &ObjectAttributes, + &IoStatus, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + OpenOptions); +} diff --git a/src/server/DeviceHandle.h b/src/server/DeviceHandle.h new file mode 100644 index 000000000..51054a576 --- /dev/null +++ b/src/server/DeviceHandle.h @@ -0,0 +1,46 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DeviceHandle.h + +Abstract: +- This module helps create client and server handles for interprocess communication via the driver. + +Author: +- Michael Niksa (MiNiksa) 14-Sept-2016 + +Revision History: +--*/ + +#pragma once +namespace DeviceHandle +{ + [[nodiscard]] + NTSTATUS + CreateServerHandle( + _Out_ PHANDLE Handle, + _In_ BOOLEAN Inheritable + ); + + [[nodiscard]] + NTSTATUS + CreateClientHandle( + _Out_ PHANDLE Handle, + _In_ HANDLE ServerHandle, + _In_ PCWSTR Name, + _In_ BOOLEAN Inheritable + ); + + [[nodiscard]] + NTSTATUS + _CreateHandle( + _Out_ PHANDLE Handle, + _In_ PCWSTR DeviceName, + _In_ ACCESS_MASK DesiredAccess, + _In_opt_ HANDLE Parent, + _In_ BOOLEAN Inheritable, + _In_ ULONG OpenOptions + ); +}; diff --git a/src/server/Entrypoints.cpp b/src/server/Entrypoints.cpp new file mode 100644 index 000000000..8a7a951b3 --- /dev/null +++ b/src/server/Entrypoints.cpp @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "Entrypoints.h" + +#include "DeviceHandle.h" +#include "IoThread.h" + +#include "winbasep.h" + +[[nodiscard]] +HRESULT Entrypoints::StartConsoleForServerHandle(const HANDLE ServerHandle, const ConsoleArguments* const args) +{ + return ConsoleCreateIoThreadLegacy(ServerHandle, args); +} + +// this function has unreachable code due to its unusual lifetime. We +// disable the warning about it here. +#pragma warning(push) +#pragma warning(disable:4702) + +[[nodiscard]] +HRESULT Entrypoints::StartConsoleForCmdLine(_In_ PCWSTR pwszCmdLine, const ConsoleArguments* const args) +{ + // Create a scope because we're going to exit thread if everything goes well. + // This scope will ensure all C++ objects and smart pointers get a chance to destruct before ExitThread is called. + { + // TODO:MSFT:13271366 use the arguments from the commandline to determine if we need + // to create the server handle or not. + + // Create the server and reference handles and create the console object. + wil::unique_handle ServerHandle; + RETURN_IF_NTSTATUS_FAILED(DeviceHandle::CreateServerHandle(ServerHandle.addressof(), FALSE)); + + wil::unique_handle ReferenceHandle; + RETURN_IF_NTSTATUS_FAILED(DeviceHandle::CreateClientHandle(ReferenceHandle.addressof(), + ServerHandle.get(), + L"\\Reference", + FALSE)); + + RETURN_IF_NTSTATUS_FAILED(Entrypoints::StartConsoleForServerHandle(ServerHandle.get(), args)); + + // If we get to here, we have transferred ownership of the server handle to the console, so release it. + // Keep a copy of the value so we can open the client handles even though we're no longer the owner. + HANDLE const hServer = ServerHandle.release(); + + // Now that the console object was created, we're in a state that lets us + // create the default io objects. + wil::unique_handle ClientHandle[3]; + + // Input + RETURN_IF_NTSTATUS_FAILED(DeviceHandle::CreateClientHandle(ClientHandle[0].addressof(), + hServer, + L"\\Input", + TRUE)); + + // Output + RETURN_IF_NTSTATUS_FAILED(DeviceHandle::CreateClientHandle(ClientHandle[1].addressof(), + hServer, + L"\\Output", + TRUE)); + + // Error is a copy of Output + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), + ClientHandle[1].get(), + GetCurrentProcess(), + ClientHandle[2].addressof(), + 0, + TRUE, + DUPLICATE_SAME_ACCESS)); + + // Create the child process. We will temporarily overwrite the values in the + // PEB to force them to be inherited. + + STARTUPINFOEX StartupInformation = { 0 }; + StartupInformation.StartupInfo.cb = sizeof(STARTUPINFOEX); + StartupInformation.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + StartupInformation.StartupInfo.hStdInput = ClientHandle[0].get(); + StartupInformation.StartupInfo.hStdOutput = ClientHandle[1].get(); + StartupInformation.StartupInfo.hStdError = ClientHandle[2].get(); + + // Get the parent startup info for this process. It might contain LNK data we need to pass to the child. + { + STARTUPINFO HostStartupInfo = { 0 }; + HostStartupInfo.cb = sizeof(STARTUPINFO); + GetStartupInfoW(&HostStartupInfo); + + // If we were started with Title is Link Name, then pass the flag and the link name down to the child. + if (WI_IsFlagSet(HostStartupInfo.dwFlags, STARTF_TITLEISLINKNAME)) + { + StartupInformation.StartupInfo.lpTitle = HostStartupInfo.lpTitle; + StartupInformation.StartupInfo.dwFlags |= STARTF_TITLEISLINKNAME; + } + } + + // Create the extended attributes list that will pass the console server information into the child process. + + // Call first time to find size + SIZE_T AttributeListSize; + InitializeProcThreadAttributeList(NULL, + 2, + 0, + &AttributeListSize); + + // Alloc space + wistd::unique_ptr AttributeList = wil::make_unique_nothrow(AttributeListSize); + RETURN_IF_NULL_ALLOC(AttributeList); + + StartupInformation.lpAttributeList = reinterpret_cast(AttributeList.get()); + + // Call second time to actually initialize space. + RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(StartupInformation.lpAttributeList, + 2, // This represents the length of the list. We will call UpdateProcThreadAttribute twice so this is 2. + 0, + &AttributeListSize)); + // Set cleanup data for ProcThreadAttributeList when successful. + auto CleanupProcThreadAttribute = wil::scope_exit([&] + { + DeleteProcThreadAttributeList(StartupInformation.lpAttributeList); + }); + + RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(StartupInformation.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_CONSOLE_REFERENCE, + ReferenceHandle.addressof(), + sizeof(HANDLE), + NULL, + NULL)); + + // UpdateProcThreadAttributes wants this as a bare array of handles and doesn't like our smart structures, + // so set it up for its use. + HANDLE HandleList[3]; + HandleList[0] = StartupInformation.StartupInfo.hStdInput; + HandleList[1] = StartupInformation.StartupInfo.hStdOutput; + HandleList[2] = StartupInformation.StartupInfo.hStdError; + + RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(StartupInformation.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + &HandleList[0], + sizeof HandleList, + NULL, + NULL)); + + // We have to copy the command line string we're given because CreateProcessW has to be called with mutable data. + if (wcslen(pwszCmdLine) == 0) + { + // If they didn't give us one, just launch cmd.exe. + pwszCmdLine = L"%WINDIR%\\system32\\cmd.exe"; + } + + // Expand any environment variables present in the command line string. + // - Get needed size + DWORD cchCmdLineExpanded = ExpandEnvironmentStringsW(pwszCmdLine, nullptr, 0); + RETURN_LAST_ERROR_IF(0 == cchCmdLineExpanded); + + // - Allocate space to hold result + wistd::unique_ptr CmdLineMutable = wil::make_unique_nothrow(cchCmdLineExpanded); + RETURN_IF_NULL_ALLOC(CmdLineMutable); + + // - Expand string into allocated space + RETURN_LAST_ERROR_IF(0 == ExpandEnvironmentStringsW(pwszCmdLine, CmdLineMutable.get(), cchCmdLineExpanded)); + + // Call create process + wil::unique_process_information ProcessInformation; + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(NULL, + CmdLineMutable.get(), + NULL, + NULL, + TRUE, + EXTENDED_STARTUPINFO_PRESENT, + NULL, + NULL, + &StartupInformation.StartupInfo, + ProcessInformation.addressof())); + } + + // Exit the thread so the CRT won't clean us up and kill. The IO thread owns the lifetime now. + ExitThread(S_OK); + + // We won't hit this. The ExitThread above will kill the caller at this point. + FAIL_FAST_HR(E_UNEXPECTED); + return S_OK; +} +#pragma warning(pop) diff --git a/src/server/Entrypoints.h b/src/server/Entrypoints.h new file mode 100644 index 000000000..a9c5b4e7d --- /dev/null +++ b/src/server/Entrypoints.h @@ -0,0 +1,27 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- Entrypoints.h + +Abstract: +- This module defines methods to get a console session started. + +Author: +- Michael Niksa (MiNiksa) 14-Sept-2016 + +Revision History: +--*/ + +#pragma once + +class ConsoleArguments; + +namespace Entrypoints +{ + [[nodiscard]] + HRESULT StartConsoleForServerHandle(const HANDLE ServerHandle, const ConsoleArguments* const args); + [[nodiscard]] + HRESULT StartConsoleForCmdLine(_In_ PCWSTR pwszCmdLine, const ConsoleArguments* const args); +}; diff --git a/src/server/IApiRoutines.h b/src/server/IApiRoutines.h new file mode 100644 index 000000000..7fe612860 --- /dev/null +++ b/src/server/IApiRoutines.h @@ -0,0 +1,454 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IApiRoutines.h + +Abstract: +- This file specifies the interface that must be defined by a server application to respond to all API calls. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp, getset.cpp, directio.cpp, stream.cpp +--*/ + +#pragma once + +// TODO: 9115192 - Temporarily forward declare the real objects until I create an interface representing a console object +// This will be required so the server doesn't actually need to understand the implementation of a console object, just the few methods it needs to call. +class SCREEN_INFORMATION; +typedef SCREEN_INFORMATION IConsoleOutputObject; + +class InputBuffer; +typedef InputBuffer IConsoleInputObject; + +class INPUT_READ_HANDLE_DATA; + +#include "IWaitRoutine.h" +#include +#include +#include "../types/inc/IInputEvent.hpp" +#include "../types/inc/viewport.hpp" + +class IApiRoutines +{ +public: + +#pragma region ObjectManagement + // TODO: 9115192 - We will need to make the objects via an interface eventually. This represents that idea. + /*virtual HRESULT CreateInitialObjects(_Out_ IConsoleInputObject** const ppInputObject, + _Out_ IConsoleOutputObject** const ppOutputObject); +*/ + +#pragma endregion + +#pragma region L1 + virtual void GetConsoleInputCodePageImpl(ULONG& codepage) noexcept = 0; + + virtual void GetConsoleOutputCodePageImpl(ULONG& codepage) noexcept = 0; + + virtual void GetConsoleInputModeImpl(InputBuffer& context, + ULONG& mode) noexcept = 0; + + virtual void GetConsoleOutputModeImpl(SCREEN_INFORMATION& context, + ULONG& mode) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleInputModeImpl(IConsoleInputObject& context, + const ULONG mode) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleOutputModeImpl(IConsoleOutputObject& context, + const ULONG mode) noexcept = 0; + + [[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 ReadConsoleAImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleWImpl(IConsoleInputObject& context, + gsl::span buffer, + size_t& written, + std::unique_ptr& waiter, + const std::string_view initialData, + const std::wstring_view exeName, + INPUT_READ_HANDLE_DATA& readHandleState, + const HANDLE clientHandle, + const DWORD controlWakeupMask, + DWORD& controlKeyState) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleAImpl(IConsoleOutputObject& context, + const std::string_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleWImpl(IConsoleOutputObject& context, + const std::wstring_view buffer, + size_t& read, + std::unique_ptr& waiter) noexcept = 0; + +#pragma region Thread Creation Info + [[nodiscard]] + virtual HRESULT GetConsoleLangIdImpl(LANGID& langId) noexcept = 0; +#pragma endregion + +#pragma endregion + +#pragma region L2 + + [[nodiscard]] + virtual HRESULT FillConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const WORD attribute, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept = 0; + + [[nodiscard]] + virtual HRESULT FillConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const char character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept = 0; + + [[nodiscard]] + virtual HRESULT FillConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const wchar_t character, + const size_t lengthToWrite, + const COORD startingCoordinate, + size_t& cellsModified) noexcept = 0; + + virtual void SetConsoleActiveScreenBufferImpl(IConsoleOutputObject& newContext) noexcept = 0; + + virtual void FlushConsoleInputBuffer(IConsoleInputObject& context) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleInputCodePageImpl(const ULONG codepage) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleOutputCodePageImpl(const ULONG codepage) noexcept = 0; + + virtual void GetConsoleCursorInfoImpl(const SCREEN_INFORMATION& context, + ULONG& size, + bool& isVisible) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleCursorInfoImpl(IConsoleOutputObject& context, + const ULONG size, + const bool isVisible) noexcept = 0; + + // driver will pare down for non-Ex method + virtual void GetConsoleScreenBufferInfoExImpl(const IConsoleOutputObject& context, + CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleScreenBufferInfoExImpl(IConsoleOutputObject& OutContext, + const CONSOLE_SCREEN_BUFFER_INFOEX& data) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleScreenBufferSizeImpl(IConsoleOutputObject& context, + const COORD size) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleCursorPositionImpl(IConsoleOutputObject& context, + const COORD position) noexcept = 0; + + virtual void GetLargestConsoleWindowSizeImpl(const IConsoleOutputObject& context, + COORD& size) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ScrollConsoleScreenBufferAImpl(IConsoleOutputObject& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const char fillCharacter, + const WORD fillAttribute) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ScrollConsoleScreenBufferWImpl(IConsoleOutputObject& context, + const SMALL_RECT& source, + const COORD target, + std::optional clip, + const wchar_t fillCharacter, + const WORD fillAttribute) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleTextAttributeImpl(IConsoleOutputObject& context, + const WORD attribute) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleWindowInfoImpl(IConsoleOutputObject& context, + const bool isAbsolute, + const SMALL_RECT& windowRect) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleOutputAttributeImpl(const IConsoleOutputObject& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleOutputCharacterAImpl(const IConsoleOutputObject& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleOutputCharacterWImpl(const IConsoleOutputObject& context, + const COORD origin, + gsl::span buffer, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleInputAImpl(IConsoleInputObject& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleInputWImpl(IConsoleInputObject& context, + const std::basic_string_view buffer, + size_t& written, + const bool append) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleOutputAImpl(IConsoleOutputObject& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& requestRectangle, + Microsoft::Console::Types::Viewport& writtenRectangle) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleOutputWImpl(IConsoleOutputObject& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& requestRectangle, + Microsoft::Console::Types::Viewport& writtenRectangle) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleOutputAttributeImpl(IConsoleOutputObject& OutContext, + const std::basic_string_view attrs, + const COORD target, + size_t& used) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleOutputCharacterAImpl(IConsoleOutputObject& OutContext, + const std::string_view text, + const COORD target, + size_t& used) noexcept = 0; + + [[nodiscard]] + virtual HRESULT WriteConsoleOutputCharacterWImpl(IConsoleOutputObject& OutContext, + const std::wstring_view text, + const COORD target, + size_t& used) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleOutputAImpl(const IConsoleOutputObject& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ReadConsoleOutputWImpl(const IConsoleOutputObject& context, + gsl::span buffer, + const Microsoft::Console::Types::Viewport& sourceRectangle, + Microsoft::Console::Types::Viewport& readRectangle) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleOriginalTitleAImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleOriginalTitleWImpl(gsl::span title, + size_t& written, + size_t& needed) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleTitleAImpl(const std::string_view title) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleTitleWImpl(const std::wstring_view title) noexcept = 0; + +#pragma endregion + +#pragma region L3 + virtual void GetNumberOfConsoleMouseButtonsImpl(ULONG& buttons) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleFontSizeImpl(const SCREEN_INFORMATION& context, + const DWORD index, + COORD& size) noexcept = 0; + + // driver will pare down for non-Ex method + [[nodiscard]] + virtual HRESULT GetCurrentConsoleFontExImpl(const SCREEN_INFORMATION& context, + const bool isForMaximumWindowSize, + CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleDisplayModeImpl(SCREEN_INFORMATION& context, + const ULONG flags, + COORD& newSize) noexcept = 0; + + virtual void GetConsoleDisplayModeImpl(ULONG& flags) noexcept = 0; + + [[nodiscard]] + virtual HRESULT AddConsoleAliasAImpl(const std::string_view source, + const std::string_view target, + const std::string_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT AddConsoleAliasWImpl(const std::wstring_view source, + const std::wstring_view target, + const std::wstring_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasAImpl(const std::string_view source, + gsl::span target, + size_t& written, + const std::string_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasWImpl(const std::wstring_view source, + gsl::span target, + size_t& written, + const std::wstring_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasesLengthAImpl(const std::string_view exeName, + size_t& bufferRequired) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasesLengthWImpl(const std::wstring_view exeName, + size_t& bufferRequired) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasExesLengthAImpl(size_t& bufferRequired) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasExesLengthWImpl(size_t& bufferRequired) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasesAImpl(const std::string_view exeName, + gsl::span alias, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasesWImpl(const std::wstring_view exeName, + gsl::span alias, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasExesAImpl(gsl::span aliasExes, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleAliasExesWImpl(gsl::span aliasExes, + size_t& written) noexcept = 0; + +#pragma region CMDext Private API + + [[nodiscard]] + virtual HRESULT ExpungeConsoleCommandHistoryAImpl(const std::string_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT ExpungeConsoleCommandHistoryWImpl(const std::wstring_view exeName) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleNumberOfCommandsAImpl(const std::string_view exeName, + const size_t numberOfCommands) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleNumberOfCommandsWImpl(const std::wstring_view exeName, + const size_t numberOfCommands) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleCommandHistoryLengthAImpl(const std::string_view exeName, + size_t& length) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleCommandHistoryLengthWImpl(const std::wstring_view exeName, + size_t& length) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleCommandHistoryAImpl(const std::string_view exeName, + gsl::span commandHistory, + size_t& written) noexcept = 0; + + [[nodiscard]] + virtual HRESULT GetConsoleCommandHistoryWImpl(const std::wstring_view exeName, + gsl::span commandHistory, + size_t& written) noexcept = 0; + +#pragma endregion + + virtual void GetConsoleWindowImpl(HWND& hwnd) noexcept = 0; + + virtual void GetConsoleSelectionInfoImpl(CONSOLE_SELECTION_INFO& consoleSelectionInfo) noexcept = 0; + + virtual void GetConsoleHistoryInfoImpl(CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetConsoleHistoryInfoImpl(const CONSOLE_HISTORY_INFO& consoleHistoryInfo) noexcept = 0; + + [[nodiscard]] + virtual HRESULT SetCurrentConsoleFontExImpl(IConsoleOutputObject& context, + const bool isForMaximumWindowSize, + const CONSOLE_FONT_INFOEX& consoleFontInfoEx) noexcept = 0; + +#pragma endregion +}; diff --git a/src/server/IWaitRoutine.h b/src/server/IWaitRoutine.h new file mode 100644 index 000000000..708733548 --- /dev/null +++ b/src/server/IWaitRoutine.h @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IWaitRoutine.h + +Abstract: +- This file specifies the interface that must be defined by a host application when queuing an API call to be serviced later. + Specifically, this defines which method will be called back "later" to service the request. + +Author: +- Michael Niksa (miniksa) 01-Mar-2017 + +Revision History: +- Adapted from original items in srvinit.cpp, getset.cpp, directio.cpp, stream.cpp +--*/ + +#pragma once + +#include "WaitTerminationReason.h" + +enum class ReplyDataType +{ + Write = 1, + Read = 2, +}; + +class IWaitRoutine +{ +public: + IWaitRoutine(ReplyDataType type) + : _ReplyType(type) + + { + } + + virtual ~IWaitRoutine() + { + } + + 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; + + ReplyDataType GetReplyType() const + { + return _ReplyType; + } + +private: + ReplyDataType const _ReplyType; +}; diff --git a/src/server/IoDispatchers.cpp b/src/server/IoDispatchers.cpp new file mode 100644 index 000000000..236044a52 --- /dev/null +++ b/src/server/IoDispatchers.cpp @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "IoDispatchers.h" + +#include "ApiSorter.h" + +#include "..\host\conserv.h" +#include "..\host\conwinuserrefs.h" +#include "..\host\directio.h" +#include "..\host\handle.h" +#include "..\host\srvinit.h" +#include "..\host\telemetry.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +using namespace Microsoft::Console::Interactivity; + +// From ntstatus.h, which we cannot include without causing a bunch of other conflicts. So we just include the one code we need. +// +// MessageId: STATUS_OBJECT_NAME_NOT_FOUND +// +// MessageText: +// +// Object Name not found. +// +#define STATUS_OBJECT_NAME_NOT_FOUND ((NTSTATUS)0xC0000034L) + +// Routine Description: +// - This routine handles IO requests to create new objects. It validates the request, creates the object and a "handle" to it. +// Arguments: +// - pMessage - Supplies the message representing the create IO. +// Return Value: +// - A pointer to the reply message, if this message is to be completed inline; nullptr if this message will pend now and complete later. +PCONSOLE_API_MSG IoDispatchers::ConsoleCreateObject(_In_ PCONSOLE_API_MSG pMessage) +{ + NTSTATUS Status; + + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + PCD_CREATE_OBJECT_INFORMATION const CreateInformation = &pMessage->CreateObject; + + LockConsole(); + + // If a generic object was requested, use the desired access to determine which type of object the caller is expecting. + if (CreateInformation->ObjectType == CD_IO_OBJECT_TYPE_GENERIC) + { + if ((CreateInformation->DesiredAccess & (GENERIC_READ | GENERIC_WRITE)) == GENERIC_READ) + { + CreateInformation->ObjectType = CD_IO_OBJECT_TYPE_CURRENT_INPUT; + + } + else if ((CreateInformation->DesiredAccess & (GENERIC_READ | GENERIC_WRITE)) == GENERIC_WRITE) + { + CreateInformation->ObjectType = CD_IO_OBJECT_TYPE_CURRENT_OUTPUT; + } + } + + std::unique_ptr handle; + // Check the requested type. + switch (CreateInformation->ObjectType) + { + case CD_IO_OBJECT_TYPE_CURRENT_INPUT: + Status = NTSTATUS_FROM_HRESULT(gci.pInputBuffer->AllocateIoHandle(ConsoleHandleData::HandleType::Input, + CreateInformation->DesiredAccess, + CreateInformation->ShareMode, + handle)); + break; + + case CD_IO_OBJECT_TYPE_CURRENT_OUTPUT: + { + SCREEN_INFORMATION& ScreenInformation = gci.GetActiveOutputBuffer().GetMainBuffer(); + Status = NTSTATUS_FROM_HRESULT(ScreenInformation.AllocateIoHandle(ConsoleHandleData::HandleType::Output, + CreateInformation->DesiredAccess, + CreateInformation->ShareMode, + handle)); + break; + } + case CD_IO_OBJECT_TYPE_NEW_OUTPUT: + Status = ConsoleCreateScreenBuffer(handle, pMessage, CreateInformation, &pMessage->CreateScreenBuffer); + break; + + default: + Status = STATUS_INVALID_PARAMETER; + } + + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + // Complete the request. + pMessage->SetReplyStatus(STATUS_SUCCESS); + pMessage->SetReplyInformation(reinterpret_cast(handle.get())); + + if (SUCCEEDED(ServiceLocator::LocateGlobals().pDeviceComm->CompleteIo(&pMessage->Complete))) + { + // We've successfully transfered ownership of the handle to the driver. We can release and not free it. + handle.release(); + } + + UnlockConsole(); + + return nullptr; + +Error: + + FAIL_FAST_IF(NT_SUCCESS(Status)); + + UnlockConsole(); + + pMessage->SetReplyStatus(Status); + + return pMessage; +} + +// Routine Description: +// - This routine will handle a request to specifically close one of the console objects./ +// Arguments: +// - pMessage - Supplies the message representing the close object IO. +// Return Value: +// - A pointer to the reply message. +PCONSOLE_API_MSG IoDispatchers::ConsoleCloseObject(_In_ PCONSOLE_API_MSG pMessage) +{ + LockConsole(); + + delete pMessage->GetObjectHandle(); + pMessage->SetReplyStatus(STATUS_SUCCESS); + + UnlockConsole(); + return pMessage; +} + +// Routine Description: +// - Used when a client application establishes an initial connection to this console server. +// - This is supposed to represent accounting for the process, making the appropriate handles, etc. +// Arguments: +// - pReceiveMsg - The packet message received from the driver specifying that a client is connecting +// Return Value: +// - The response data to this request message. +PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API_MSG pReceiveMsg) +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::AttachConsole); + + ConsoleProcessHandle* ProcessData = nullptr; + + LockConsole(); + + DWORD const dwProcessId = (DWORD)pReceiveMsg->Descriptor.Process; + DWORD const dwThreadId = (DWORD)pReceiveMsg->Descriptor.Object; + + CONSOLE_API_CONNECTINFO Cac; + NTSTATUS Status = ConsoleInitializeConnectInfo(pReceiveMsg, &Cac); + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + Status = NTSTATUS_FROM_HRESULT(gci.ProcessHandleList.AllocProcessData(dwProcessId, + dwThreadId, + Cac.ProcessGroupId, + nullptr, + &ProcessData)); + + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + ProcessData->fRootProcess = WI_IsFlagClear(gci.Flags, CONSOLE_INITIALIZED); + + // ConsoleApp will be false in the AttachConsole case. + if (Cac.ConsoleApp) + { + ServiceLocator::LocateConsoleControl()->NotifyConsoleApplication(dwProcessId); + } + + ServiceLocator::LocateAccessibilityNotifier()->NotifyConsoleStartApplicationEvent(dwProcessId); + + if (WI_IsFlagClear(gci.Flags, CONSOLE_INITIALIZED)) + { + Status = ConsoleAllocateConsole(&Cac); + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + WI_SetFlag(gci.Flags, CONSOLE_INITIALIZED); + } + + try + { + CommandHistory::s_Allocate({ Cac.AppName, Cac.AppNameLength / sizeof(wchar_t) }, (HANDLE)ProcessData); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + goto Error; + } + + gci.ProcessHandleList.ModifyConsoleProcessFocus(WI_IsFlagSet(gci.Flags, CONSOLE_HAS_FOCUS)); + + // Create the handles. + + Status = NTSTATUS_FROM_HRESULT(gci.pInputBuffer->AllocateIoHandle(ConsoleHandleData::HandleType::Input, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + ProcessData->pInputHandle)); + + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + + auto& screenInfo = gci.GetActiveOutputBuffer().GetMainBuffer(); + Status = NTSTATUS_FROM_HRESULT(screenInfo.AllocateIoHandle(ConsoleHandleData::HandleType::Output, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + ProcessData->pOutputHandle)); + + if (!NT_SUCCESS(Status)) + { + goto Error; + } + + // Complete the request. + pReceiveMsg->SetReplyStatus(STATUS_SUCCESS); + pReceiveMsg->SetReplyInformation(sizeof(CD_CONNECTION_INFORMATION)); + + CD_CONNECTION_INFORMATION ConnectionInformation = ProcessData->GetConnectionInformation(); + pReceiveMsg->Complete.Write.Data = &ConnectionInformation; + pReceiveMsg->Complete.Write.Size = sizeof(CD_CONNECTION_INFORMATION); + + if (FAILED(ServiceLocator::LocateGlobals().pDeviceComm->CompleteIo(&pReceiveMsg->Complete))) + { + CommandHistory::s_Free((HANDLE)ProcessData); + gci.ProcessHandleList.FreeProcessData(ProcessData); + } + + UnlockConsole(); + + return nullptr; + +Error: + FAIL_FAST_IF(NT_SUCCESS(Status)); + + if (ProcessData != nullptr) + { + CommandHistory::s_Free((HANDLE)ProcessData); + gci.ProcessHandleList.FreeProcessData(ProcessData); + } + + UnlockConsole(); + + pReceiveMsg->SetReplyStatus(Status); + + return pReceiveMsg; +} + +// Routine Description: +// - This routine is called when a process is destroyed. It closes the process's handles and frees the console if it's the last reference. +// Arguments: +// - pProcessData - Pointer to the client's process information structure. +// Return Value: +// - A pointer to the reply message. +PCONSOLE_API_MSG IoDispatchers::ConsoleClientDisconnectRoutine(_In_ PCONSOLE_API_MSG pMessage) +{ + Telemetry::Instance().LogApiCall(Telemetry::ApiCall::FreeConsole); + + ConsoleProcessHandle* const pProcessData = pMessage->GetProcessHandle(); + + ServiceLocator::LocateAccessibilityNotifier()->NotifyConsoleEndApplicationEvent(pProcessData->dwProcessId); + + LOG_IF_FAILED(RemoveConsole(pProcessData)); + + pMessage->SetReplyStatus(STATUS_SUCCESS); + + return pMessage; +} + +// Routine Description: +// - This routine validates a user IO and dispatches it to the appropriate worker routine. +// Arguments: +// - pMessage - Supplies the message representing the user IO. +// Return Value: +// - A pointer to the reply message, if this message is to be completed inline; nullptr if this message will pend now and complete later. +PCONSOLE_API_MSG IoDispatchers::ConsoleDispatchRequest(_In_ PCONSOLE_API_MSG pMessage) +{ + return ApiSorter::ConsoleDispatchRequest(pMessage); +} diff --git a/src/server/IoDispatchers.h b/src/server/IoDispatchers.h new file mode 100644 index 000000000..6323f65e8 --- /dev/null +++ b/src/server/IoDispatchers.h @@ -0,0 +1,31 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IoDispatchers.h + +Abstract: +- This file processes a majority of server-contained IO operations received from a client + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp +--*/ + +#pragma once + +#include "ApiMessage.h" + +class IoDispatchers +{ +public: + // TODO: MSFT: 9115192 temp for now. going to ApiSorter and IoDispatchers + static PCONSOLE_API_MSG ConsoleHandleConnectionRequest(_In_ PCONSOLE_API_MSG pReceiveMsg); + static PCONSOLE_API_MSG ConsoleDispatchRequest(_In_ PCONSOLE_API_MSG pMessage); + static PCONSOLE_API_MSG ConsoleCreateObject(_In_ PCONSOLE_API_MSG pMessage); + static PCONSOLE_API_MSG ConsoleCloseObject(_In_ PCONSOLE_API_MSG pMessage); + static PCONSOLE_API_MSG ConsoleClientDisconnectRoutine(_In_ PCONSOLE_API_MSG pMessage); +}; diff --git a/src/server/IoSorter.cpp b/src/server/IoSorter.cpp new file mode 100644 index 000000000..c21917cbf --- /dev/null +++ b/src/server/IoSorter.cpp @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "IoSorter.h" + +#include "IoDispatchers.h" +#include "ApiDispatchers.h" + +#include "ApiSorter.h" + +#include "..\host\globals.h" + +#include "..\host\getset.h" +#include "..\host\stream.h" + +void IoSorter::ServiceIoOperation(_In_ CONSOLE_API_MSG* const pMsg, + _Out_ CONSOLE_API_MSG** ReplyMsg) +{ + NTSTATUS Status; + HRESULT hr; + BOOL ReplyPending = FALSE; + + ZeroMemory(&pMsg->State, sizeof(pMsg->State)); + ZeroMemory(&pMsg->Complete, sizeof(CD_IO_COMPLETE)); + + pMsg->Complete.Identifier = pMsg->Descriptor.Identifier; + + switch (pMsg->Descriptor.Function) + { + case CONSOLE_IO_USER_DEFINED: + *ReplyMsg = IoDispatchers::ConsoleDispatchRequest(pMsg); + break; + + case CONSOLE_IO_CONNECT: + *ReplyMsg = IoDispatchers::ConsoleHandleConnectionRequest(pMsg); + break; + + case CONSOLE_IO_DISCONNECT: + *ReplyMsg = IoDispatchers::ConsoleClientDisconnectRoutine(pMsg); + break; + + case CONSOLE_IO_CREATE_OBJECT: + *ReplyMsg = IoDispatchers::ConsoleCreateObject(pMsg); + break; + + case CONSOLE_IO_CLOSE_OBJECT: + *ReplyMsg = IoDispatchers::ConsoleCloseObject(pMsg); + break; + + case CONSOLE_IO_RAW_WRITE: + ZeroMemory(&pMsg->u.consoleMsgL1.WriteConsole, sizeof(CONSOLE_WRITECONSOLE_MSG)); + pMsg->msgHeader.ApiNumber = API_NUMBER_WRITECONSOLE; // Required for Wait blocks to identify the right callback. + ReplyPending = FALSE; + hr = ApiDispatchers::ServerWriteConsole(pMsg, &ReplyPending); + Status = NTSTATUS_FROM_HRESULT(hr); + if (ReplyPending) + { + *ReplyMsg = nullptr; + + } + else + { + pMsg->SetReplyStatus(Status); + *ReplyMsg = pMsg; + } + break; + + case CONSOLE_IO_RAW_READ: + ZeroMemory(&pMsg->u.consoleMsgL1.ReadConsole, sizeof(CONSOLE_READCONSOLE_MSG)); + pMsg->msgHeader.ApiNumber = API_NUMBER_READCONSOLE; // Required for Wait blocks to identify the right callback. + pMsg->u.consoleMsgL1.ReadConsole.ProcessControlZ = TRUE; + ReplyPending = FALSE; + hr = ApiDispatchers::ServerReadConsole(pMsg, &ReplyPending); + Status = NTSTATUS_FROM_HRESULT(hr); + if (ReplyPending) + { + *ReplyMsg = nullptr; + + } + else + { + pMsg->SetReplyStatus(Status); + *ReplyMsg = pMsg; + } + break; + + case CONSOLE_IO_RAW_FLUSH: + ReplyPending = FALSE; + + Status = NTSTATUS_FROM_HRESULT(ApiDispatchers::ServerFlushConsoleInputBuffer(pMsg, &ReplyPending)); + FAIL_FAST_IF(!(!ReplyPending)); + pMsg->SetReplyStatus(Status); + *ReplyMsg = pMsg; + break; + + default: + pMsg->SetReplyStatus(STATUS_UNSUCCESSFUL); + *ReplyMsg = pMsg; + } +} diff --git a/src/server/IoSorter.h b/src/server/IoSorter.h new file mode 100644 index 000000000..3fffc1332 --- /dev/null +++ b/src/server/IoSorter.h @@ -0,0 +1,28 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- IoSorter.h + +Abstract: +- This file sorts out the various IO requests that can occur and finds an appropriate target. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in srvinit.cpp +--*/ + +#pragma once + +#include "ApiMessage.h" + +class IoSorter +{ +public: + // TODO: MSFT: 9115192 - probably not void. + static void ServiceIoOperation(_In_ CONSOLE_API_MSG* const pMsg, + _Out_ CONSOLE_API_MSG** ReplyMsg); +}; diff --git a/src/server/IoThread.h b/src/server/IoThread.h new file mode 100644 index 000000000..f170ec598 --- /dev/null +++ b/src/server/IoThread.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +class ConsoleArguments; + +[[nodiscard]] +HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args); diff --git a/src/server/ObjectHandle.cpp b/src/server/ObjectHandle.cpp new file mode 100644 index 000000000..6c640e0a3 --- /dev/null +++ b/src/server/ObjectHandle.cpp @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ObjectHandle.h" + +#include "..\host\globals.h" +#include "..\host\inputReadHandleData.h" +#include "..\host\input.h" +#include "..\host\screenInfo.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +ConsoleHandleData::ConsoleHandleData(const ULONG ulHandleType, + const ACCESS_MASK amAccess, + const ULONG ulShareAccess, + _In_ PVOID const pvClientPointer) : + _ulHandleType(ulHandleType), + _amAccess(amAccess), + _ulShareAccess(ulShareAccess), + _pvClientPointer(pvClientPointer), + _pClientInput(nullptr) +{ + if (_IsInput()) + { + _pClientInput = std::make_unique(); + } +} + +// Routine Description: +// - Closes this handle destroying memory as appropriate and freeing ref counts. +// Do not use this handle after closing. +ConsoleHandleData::~ConsoleHandleData() +{ + if (_IsInput()) + { + THROW_IF_FAILED(_CloseInputHandle()); + } + else if (_IsOutput()) + { + THROW_IF_FAILED(_CloseOutputHandle()); + } + else + { + FAIL_FAST_HR(E_UNEXPECTED); + } +} + +// Routine Description: +// - Checks if this handle represents an input type object. +// Arguments: +// - +// Return Value: +// - True if this handle is for an input object. False otherwise. +bool ConsoleHandleData::_IsInput() const +{ + return WI_IsFlagSet(_ulHandleType, HandleType::Input); +} + +// Routine Description: +// - Checks if this handle represents an output type object. +// Arguments: +// - +// Return Value: +// - True if this handle is for an output object. False otherwise. +bool ConsoleHandleData::_IsOutput() const +{ + return WI_IsFlagSet(_ulHandleType, HandleType::Output); +} + +// Routine Description: +// - Indicates whether this handle is allowed to be used for reading the underlying object data. +// Arguments: +// - +// Return Value: +// - True if read is permitted. False otherwise. +bool ConsoleHandleData::IsReadAllowed() const +{ + return WI_IsFlagSet(_amAccess, GENERIC_READ); +} + +// Routine Description: +// - Indicates whether this handle allows multiple customers to share reading of the underlying object data. +// Arguments: +// - +// Return Value: +// - True if sharing read access is permitted. False otherwise. +bool ConsoleHandleData::IsReadShared() const +{ + return WI_IsFlagSet(_ulShareAccess, FILE_SHARE_READ); +} + +// Routine Description: +// - Indicates whether this handle is allowed to be used for writing the underlying object data. +// Arguments: +// - +// Return Value: +// - True if write is permitted. False otherwise. +bool ConsoleHandleData::IsWriteAllowed() const +{ + return WI_IsFlagSet(_amAccess, GENERIC_WRITE); +} + +// Routine Description: +// - Indicates whether this handle allows multiple customers to share writing of the underlying object data. +// Arguments: +// - +// Return Value: +// - True if sharing write access is permitted. False otherwise. +bool ConsoleHandleData::IsWriteShared() const +{ + return WI_IsFlagSet(_ulShareAccess, FILE_SHARE_WRITE); +} + +// Routine Description: +// - Retieves the properly typed Input Buffer from the Handle. +// Arguments: +// - amRequested - Access that the client would like for manipulating the buffer +// - ppInputBuffer - On success, filled with the referenced Input Buffer object +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT ConsoleHandleData::GetInputBuffer(const ACCESS_MASK amRequested, + _Outptr_ InputBuffer** const ppInputBuffer) const +{ + *ppInputBuffer = nullptr; + + RETURN_HR_IF(E_ACCESSDENIED, WI_IsAnyFlagClear(_amAccess, amRequested)); + RETURN_HR_IF(E_HANDLE, WI_IsAnyFlagClear(_ulHandleType, HandleType::Input)); + + *ppInputBuffer = static_cast(_pvClientPointer); + + return S_OK; +} + +// Routine Description: +// - Retieves the properly typed Screen Buffer from the Handle. +// Arguments: +// - amRequested - Access that the client would like for manipulating the buffer +// - ppInputBuffer - On success, filled with the referenced Screen Buffer object +// Return Value: +// - HRESULT S_OK or suitable error. +[[nodiscard]] +HRESULT ConsoleHandleData::GetScreenBuffer(const ACCESS_MASK amRequested, + _Outptr_ SCREEN_INFORMATION** const ppScreenInfo) const +{ + *ppScreenInfo = nullptr; + + RETURN_HR_IF(E_ACCESSDENIED, WI_IsAnyFlagClear(_amAccess, amRequested)); + RETURN_HR_IF(E_HANDLE, WI_IsAnyFlagClear(_ulHandleType, HandleType::Output)); + + *ppScreenInfo = static_cast(_pvClientPointer); + + return S_OK; +} + +// Routine Description: +// - Retrieves the wait queue associated with the given object held by this handle. +// Arguments: +// - ppWaitQueue - On success, filled with a pointer to the desired queue +// Return Value: +// - HRESULT S_OK or E_UNEXPECTED if the handle data structure is in an invalid state. +[[nodiscard]] +HRESULT ConsoleHandleData::GetWaitQueue(_Outptr_ ConsoleWaitQueue** const ppWaitQueue) const +{ + CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (_IsInput()) + { + InputBuffer* const pObj = static_cast(_pvClientPointer); + *ppWaitQueue = &pObj->WaitQueue; + return S_OK; + } + else if (_IsOutput()) + { + // TODO MSFT 9405322: shouldn't the output queue be per output object target, not global? https://osgvsowi/9405322 + *ppWaitQueue = &gci.OutputQueue; + return S_OK; + } + else + { + return E_UNEXPECTED; + } +} + +// Routine Description: +// - For input buffers only, retrieves an extra handle data structure used to save some information +// across multiple reads from the same handle. +// Arguments: +// - +// Return Value: +// - Pointer to the input read handle data structure with the aforementioned extra info. +INPUT_READ_HANDLE_DATA* ConsoleHandleData::GetClientInput() const +{ + return _pClientInput.get(); +} + +// Routine Description: +// - This routine closes an input handle. It decrements the input buffer's +// reference count. If it goes to zero, the buffer is reinitialized. +// Otherwise, the handle is removed from sharing. +// Arguments: +// - +// Return Value: +// - HRESULT S_OK or suitable error code. +// Note: +// - The console lock must be held when calling this routine. +[[nodiscard]] +HRESULT ConsoleHandleData::_CloseInputHandle() +{ + FAIL_FAST_IF(!(_IsInput())); + InputBuffer* pInputBuffer = static_cast(_pvClientPointer); + INPUT_READ_HANDLE_DATA* pReadHandleData = GetClientInput(); + pReadHandleData->CompletePending(); + + // see if there are any reads waiting for data via this handle. if + // there are, wake them up. there aren't any other outstanding i/o + // operations via this handle because the console lock is held. + + if (pReadHandleData->GetReadCount() != 0) + { + pInputBuffer->WaitQueue.NotifyWaiters(true, WaitTerminationReason::HandleClosing); + } + + FAIL_FAST_IF(pReadHandleData->GetReadCount() > 0); + + // TODO: MSFT: 9115192 - THIS IS BAD. It should use a destructor. + LOG_IF_FAILED(pInputBuffer->FreeIoHandle(this)); + + if (!pInputBuffer->HasAnyOpenHandles()) + { + pInputBuffer->ReinitializeInputBuffer(); + } + + return S_OK; +} + +// Routine Description: +// - This routine closes an output handle. It decrements the screen buffer's +// reference count. If it goes to zero, the buffer is freed. Otherwise, +// the handle is removed from sharing. +// Arguments: +// - +// Return Value: +// - HRESULT S_OK or suitable error code. +// Note: +// - The console lock must be held when calling this routine. +[[nodiscard]] +HRESULT ConsoleHandleData::_CloseOutputHandle() +{ + FAIL_FAST_IF(!(_IsOutput())); + SCREEN_INFORMATION* pScreenInfo = static_cast(_pvClientPointer); + + pScreenInfo = &pScreenInfo->GetMainBuffer(); + + // TODO: MSFT: 9115192 - THIS IS BAD. It should use a destructor. + LOG_IF_FAILED(pScreenInfo->FreeIoHandle(this)); + if (!pScreenInfo->HasAnyOpenHandles()) + { + SCREEN_INFORMATION::s_RemoveScreenBuffer(pScreenInfo); + } + + return S_OK; +} diff --git a/src/server/ObjectHandle.h b/src/server/ObjectHandle.h new file mode 100644 index 000000000..21eb55168 --- /dev/null +++ b/src/server/ObjectHandle.h @@ -0,0 +1,88 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ObjectHandle.h + +Abstract: +- This file defines a handle associated with a console input or output buffer object. +- This is used to expose a handle to a client application via the API. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +class INPUT_READ_HANDLE_DATA; + +class InputBuffer; + +class SCREEN_INFORMATION; + +#include "WaitQueue.h" + +class ConsoleHandleData final +{ +public: + ConsoleHandleData(const ULONG ulHandleType, + const ACCESS_MASK amAccess, + const ULONG ulShareAccess, + _In_ PVOID const pvClientPointer); + + ~ConsoleHandleData(); + ConsoleHandleData(const ConsoleHandleData&) = delete; + ConsoleHandleData(ConsoleHandleData&&) = delete; + ConsoleHandleData& operator=(const ConsoleHandleData&) & = delete; + ConsoleHandleData& operator=(ConsoleHandleData&&) & = delete; + + [[nodiscard]] + HRESULT GetInputBuffer(const ACCESS_MASK amRequested, + _Outptr_ InputBuffer** const ppInputBuffer) const; + [[nodiscard]] + HRESULT GetScreenBuffer(const ACCESS_MASK amRequested, + _Outptr_ SCREEN_INFORMATION** const ppScreenInfo) const; + + [[nodiscard]] + HRESULT GetWaitQueue(_Outptr_ ConsoleWaitQueue** const ppWaitQueue) const; + + INPUT_READ_HANDLE_DATA* GetClientInput() const; + + bool IsReadAllowed() const; + bool IsReadShared() const; + bool IsWriteAllowed() const; + bool IsWriteShared() const; + + // TODO: MSFT 9355178 Temporary public access to types... http://osgvsowi/9355178 + bool IsInputHandle() const + { + return _IsInput(); + } + + enum HandleType + { + Input = 0x1, + Output = 0x2 + }; + +private: + bool _IsInput() const; + bool _IsOutput() const; + + [[nodiscard]] + HRESULT _CloseInputHandle(); + [[nodiscard]] + HRESULT _CloseOutputHandle(); + + ULONG const _ulHandleType; + ACCESS_MASK const _amAccess; + ULONG const _ulShareAccess; + PVOID _pvClientPointer; // This will be a pointer to a SCREEN_INFORMATION or INPUT_INFORMATION object. + std::unique_ptr _pClientInput; +}; + +DEFINE_ENUM_FLAG_OPERATORS(ConsoleHandleData::HandleType); diff --git a/src/server/ObjectHeader.cpp b/src/server/ObjectHeader.cpp new file mode 100644 index 000000000..78786db61 --- /dev/null +++ b/src/server/ObjectHeader.cpp @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ObjectHeader.h" +#include "ObjectHandle.h" + +#include "..\host\inputReadHandleData.h" + +ConsoleObjectHeader::ConsoleObjectHeader() : + _ulOpenCount(0), + _ulReaderCount(0), + _ulWriterCount(0), + _ulReadShareCount(0), + _ulWriteShareCount(0) +{ + +} + +// Routine Description: +// - This routine allocates an input or output handle from the process's handle table. +// - This routine initializes all non-type specific fields in the handle data structure. +// Arguments: +// - ulHandleType - Flag indicating input or output handle. +// - amDesired - The accesses that will be permitted to this handle after creation +// - ulShareMode - The share states that will be permitted to this handle after creation +// - ppOut - On return, filled with a pointer to the handle data structure. When returned to the API caller, cast to a handle value. +// Return Value: +// - HRESULT S_OK or appropriate error. +// Note: +// TODO: MSFT 614400 - Add concurrency SAL to enforce the lock http://osgvsowi/614400 +// - The console lock must be held when calling this routine. The handle is allocated from the per-process handle table. Holding the console +// lock serializes both threads within the calling process and any other process that shares the console. +[[nodiscard]] +HRESULT ConsoleObjectHeader::AllocateIoHandle(const ConsoleHandleData::HandleType ulHandleType, + const ACCESS_MASK amDesired, + const ULONG ulShareMode, + std::unique_ptr& out) +{ + try + { + // Allocate all necessary state. + std::unique_ptr pHandleData = std::make_unique(ulHandleType, + amDesired, + ulShareMode, + this); + + // Check the share mode. + if (((pHandleData->IsReadAllowed()) && (_ulOpenCount > _ulReadShareCount)) || + ((!pHandleData->IsReadShared()) && (_ulReaderCount > 0)) || + ((pHandleData->IsWriteAllowed()) && (_ulOpenCount > _ulWriteShareCount)) || + ((!pHandleData->IsWriteShared()) && (_ulWriterCount > 0))) + { + RETURN_WIN32(ERROR_SHARING_VIOLATION); + } + + // Update share/open counts and store handle information. + _ulOpenCount++; + + if (pHandleData->IsReadAllowed()) + { + _ulReaderCount++; + } + + if (pHandleData->IsReadShared()) + { + _ulReadShareCount++; + } + + if (pHandleData->IsWriteAllowed()) + { + _ulWriterCount++; + } + + if (pHandleData->IsWriteShared()) + { + _ulWriteShareCount++; + } + + out.swap(pHandleData); + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Frees and decrements ref counts of the handle associated with this object. +// Arguments: +// - pFree - Pointer to the handle data to be freed +// Return Value: +// - HRESULT S_OK or appropriate error. +[[nodiscard]] +HRESULT ConsoleObjectHeader::FreeIoHandle(_In_ ConsoleHandleData* const pFree) +{ + // This absolutely should not happen and our state is corrupt/bad if we try to release past 0. + THROW_HR_IF(E_NOT_VALID_STATE, !(_ulOpenCount > 0)); + + _ulOpenCount--; + + if (pFree->IsReadAllowed()) + { + _ulReaderCount--; + } + + if (pFree->IsReadShared()) + { + _ulReadShareCount--; + } + + if (pFree->IsWriteAllowed()) + { + _ulWriterCount--; + } + + if (pFree->IsWriteShared()) + { + _ulWriteShareCount--; + } + + return S_OK; +} + +// Routine Description: +// - Checks if there are any known open handles connected to this object. +// Arguments: +// - +// Return Value: +// - True if there are any (>0) open handles. False if there are none (0). +bool ConsoleObjectHeader::HasAnyOpenHandles() const +{ + return _ulOpenCount != 0; +} + +// Routine Description: +// - Adds a fake reference to the ref counts to ensure the original screen buffer is never destroyed. +// - This is a temporary kludge to be solved in TODO http://osgvsowi/9355013 +// Arguments: +// - +// Return Value: +// - +void ConsoleObjectHeader::IncrementOriginalScreenBuffer() +{ + _ulOpenCount++; + _ulReaderCount++; + _ulReadShareCount++; + _ulWriterCount++; + _ulWriteShareCount++; +} diff --git a/src/server/ObjectHeader.h b/src/server/ObjectHeader.h new file mode 100644 index 000000000..c40a0fc68 --- /dev/null +++ b/src/server/ObjectHeader.h @@ -0,0 +1,52 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ObjectHeader.h + +Abstract: +- This file defines the header information to count handles attached to a given object + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +#include "ObjectHandle.h" + +class ConsoleObjectHeader +{ +public: + ConsoleObjectHeader(); + + // NOTE: This class must have a virtual method for the stored "this" pointers to match what we're actually looking for. + // If there is no virtual method, we may have the "this" pointer be offset by 8 from the actual object that inherits ConsoleObjectHeader. + virtual ~ConsoleObjectHeader() {}; + + [[nodiscard]] + HRESULT AllocateIoHandle(const ConsoleHandleData::HandleType ulHandleType, + const ACCESS_MASK amDesired, + const ULONG ulShareMode, + std::unique_ptr& out); + + [[nodiscard]] + HRESULT FreeIoHandle(_In_ ConsoleHandleData* const pFree); + + bool HasAnyOpenHandles() const; + + // TODO: MSFT 9355013 come up with a better solution than this. http://osgvsowi/9355013 + // It's currently a "load bearing" piece because things like the renderer expect there to always be a "current screen buffer" + void IncrementOriginalScreenBuffer(); + +private: + ULONG _ulOpenCount; + ULONG _ulReaderCount; + ULONG _ulWriterCount; + ULONG _ulReadShareCount; + ULONG _ulWriteShareCount; +}; diff --git a/src/server/ProcessHandle.cpp b/src/server/ProcessHandle.cpp new file mode 100644 index 000000000..531e445f3 --- /dev/null +++ b/src/server/ProcessHandle.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ProcessHandle.h" + +#include "..\host\globals.h" +#include "..\host\telemetry.hpp" + +// Routine Description: +// - Constructs an instance of the ConsoleProcessHandle Class +// - NOTE: Can throw if allocation fails or if there is a console policy we do not understand. +// - NOTE: Not being able to open the process by ID isn't a failure. It will be logged and continued. +ConsoleProcessHandle::ConsoleProcessHandle(const DWORD dwProcessId, + const DWORD dwThreadId, + const ULONG ulProcessGroupId) : + pWaitBlockQueue(std::make_unique()), + pInputHandle(nullptr), + pOutputHandle(nullptr), + fRootProcess(false), + dwProcessId(dwProcessId), + dwThreadId(dwThreadId), + _ulTerminateCount(0), + _ulProcessGroupId(ulProcessGroupId), + _hProcess(LOG_LAST_ERROR_IF_NULL(OpenProcess(MAXIMUM_ALLOWED, + FALSE, + dwProcessId))), + _policy(ConsoleProcessPolicy::s_CreateInstance(_hProcess.get())) +{ + if (nullptr != _hProcess.get()) + { + Telemetry::Instance().LogProcessConnected(_hProcess.get()); + } +} + +CD_CONNECTION_INFORMATION ConsoleProcessHandle::GetConnectionInformation() const +{ + CD_CONNECTION_INFORMATION result = { 0 }; + result.Process = reinterpret_cast(this); + result.Input = reinterpret_cast(pInputHandle.get()); + result.Output = reinterpret_cast(pOutputHandle.get()); + return result; +} + +// Routine Description: +// - Retrieves the policies set on this particular process handle +// - This specifies restrictions that may apply to the calling console client application +const ConsoleProcessPolicy ConsoleProcessHandle::GetPolicy() const +{ + return _policy; +} diff --git a/src/server/ProcessHandle.h b/src/server/ProcessHandle.h new file mode 100644 index 000000000..384765067 --- /dev/null +++ b/src/server/ProcessHandle.h @@ -0,0 +1,60 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ProcessHandle.h + +Abstract: +- This file defines the handles that were given to a particular client process ID when it connected. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +#include "ObjectHandle.h" +#include "WaitQueue.h" +#include "ProcessPolicy.h" + +#include +#include + +class ConsoleProcessHandle +{ +public: + std::unique_ptr const pWaitBlockQueue; + std::unique_ptr pInputHandle; + std::unique_ptr pOutputHandle; + + bool fRootProcess; + + DWORD const dwProcessId; + DWORD const dwThreadId; + + const ConsoleProcessPolicy GetPolicy() const; + + CD_CONNECTION_INFORMATION GetConnectionInformation() const; + +private: + ConsoleProcessHandle(const DWORD dwProcessId, + const DWORD dwThreadId, + const ULONG ulProcessGroupId); + ~ConsoleProcessHandle() = default; + ConsoleProcessHandle(const ConsoleProcessHandle&) = delete; + ConsoleProcessHandle(ConsoleProcessHandle&&) = delete; + ConsoleProcessHandle& operator=(const ConsoleProcessHandle&) & = delete; + ConsoleProcessHandle& operator=(ConsoleProcessHandle&&) & = delete; + + ULONG _ulTerminateCount; + ULONG const _ulProcessGroupId; + wil::unique_handle const _hProcess; + + const ConsoleProcessPolicy _policy; + + friend class ConsoleProcessList; // ensure List manages lifetimes and not other classes. +}; diff --git a/src/server/ProcessList.cpp b/src/server/ProcessList.cpp new file mode 100644 index 000000000..05cadbeb6 --- /dev/null +++ b/src/server/ProcessList.cpp @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ProcessList.h" + +#include "..\host\conwinuserrefs.h" +#include "..\host\globals.h" +#include "..\host\telemetry.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Routine Description: +// - Allocates and stores in a list the process information given. +// - Will not create a new entry in the list given information matches a known process. Will instead return existing entry. +// Arguments: +// - dwProcessId - Process ID of connecting client +// - dwThreadId - Thread ID of connecting client +// - ulProcessGroupId - Process Group ID from connecting client (sometimes referred to as parent) +// - pParentProcessData - Used to specify parent while locating appropriate scope of sending control messages +// - ppProcessData - Filled on exit with a pointer to the process handle information. Optional. +// - If not used, return code will specify whether this process is known to the list or not. +// Return Value: +// - S_OK if the process was recorded in the list successfully or already existed. +// - E_FAIL if we're running into an LPC port conflict by nature of the process chain. +// - E_OUTOFMEMORY if there wasn't space to allocate a handle or push it into the list. +[[nodiscard]] +HRESULT ConsoleProcessList::AllocProcessData(const DWORD dwProcessId, + const DWORD dwThreadId, + const ULONG ulProcessGroupId, + _In_opt_ ConsoleProcessHandle* const pParentProcessData, + _Outptr_opt_ ConsoleProcessHandle** const ppProcessData) +{ + FAIL_FAST_IF(!(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked())); + + ConsoleProcessHandle* pProcessData = FindProcessInList(dwProcessId); + if (nullptr != pProcessData) + { + // In the GenerateConsoleCtrlEvent it's OK for this process to already have a ProcessData object. However, the other case is someone + // connecting to our LPC port and they should only do that once, so we fail subsequent connection attempts. + if (nullptr == pParentProcessData) + { + return E_FAIL; + // TODO: MSFT: 9574803 - This fires all the time. Did it always do that? + //RETURN_HR(E_FAIL); + } + else + { + if (nullptr != ppProcessData) + { + *ppProcessData = pProcessData; + } + RETURN_HR(S_OK); + } + } + + try + { + pProcessData = new ConsoleProcessHandle(dwProcessId, + dwThreadId, + ulProcessGroupId); + + // Some applications, when reading the process list through the GetConsoleProcessList API, are expecting + // the returned list of attached process IDs to be from newest to oldest. + // As such, we have to put the newest process into the head of the list. + _processes.push_front(pProcessData); + + if (nullptr != ppProcessData) + { + *ppProcessData = pProcessData; + } + } + CATCH_RETURN(); + + RETURN_HR(S_OK); +} + +// Routine Description: +// - This routine frees any per-process data allocated by the console. +// Arguments: +// - pProcessData - Pointer to the per-process data structure. +// Return Value: +// - +void ConsoleProcessList::FreeProcessData(_In_ ConsoleProcessHandle* const pProcessData) +{ + FAIL_FAST_IF(!(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked())); + + // Assert that the item exists in the list. If it doesn't exist, the end/last will be returned. + FAIL_FAST_IF(!(_processes.cend() != std::find(_processes.cbegin(), _processes.cend(), pProcessData))); + + _processes.remove(pProcessData); + + delete pProcessData; +} + +// Routine Description: +// - Locates a process handle in this list. +// - NOTE: Calling FindProcessInList(0) means you want the root process. +// Arguments: +// - dwProcessId - ID of the process to search for or ROOT_PROCESS_ID to find the root process. +// Return Value: +// - Pointer to the process handle information or nullptr if no match was found. +ConsoleProcessHandle* ConsoleProcessList::FindProcessInList(const DWORD dwProcessId) const +{ + auto it = _processes.cbegin(); + + while (it != _processes.cend()) + { + ConsoleProcessHandle* const pProcessHandleRecord = *it; + + if (ROOT_PROCESS_ID != dwProcessId) + { + if (pProcessHandleRecord->dwProcessId == dwProcessId) + { + return pProcessHandleRecord; + } + } + else + { + if (pProcessHandleRecord->fRootProcess) + { + return pProcessHandleRecord; + } + } + + it = std::next(it); + } + + return nullptr; +} + +// Routine Description: +// - Locates a process handle by the group ID reference. +// Arguments: +// - ulProcessGroupId - Group to search for in the list +// Return Value: +// - Pointer to first matching process handle with given group ID. nullptr if no match was found. +ConsoleProcessHandle* ConsoleProcessList::FindProcessByGroupId(_In_ ULONG ulProcessGroupId) const +{ + auto it = _processes.cbegin(); + + while (it != _processes.cend()) + { + ConsoleProcessHandle* const pProcessHandleRecord = *it; + if (pProcessHandleRecord->_ulProcessGroupId == ulProcessGroupId) + { + return pProcessHandleRecord; + } + + it = std::next(it); + } + + return nullptr; +} + +// Routine Description: +// - Retrieves the entire list of process IDs that is known to this list. +// - Requires caller to allocate space. If not enough space, pcProcessList will be filled with count of array necessary. +// Arguments: +// - pProcessList - Pointer to buffer to store process IDs. Caller allocated. +// - pcProcessList - On the way in, the length of the buffer given. On the way out, the amount of the buffer used. +// - If buffer was insufficient, filled with necessary buffer size (in elements) on the way out. +// Return Value: +// - S_OK if buffer was filled successfully and resulting count of items is in pcProcessList. +// - E_NOT_SUFFICIENT_BUFFER if the buffer given was too small. Refer to pcProcessList for size requirement. +[[nodiscard]] +HRESULT ConsoleProcessList::GetProcessList(_Inout_updates_(*pcProcessList) DWORD* const pProcessList, + _Inout_ size_t* const pcProcessList) const +{ + HRESULT hr = S_OK; + + size_t const cProcesses = _processes.size(); + + // If we can fit inside the given list space, copy out the data. + if (cProcesses <= *pcProcessList) + { + size_t cFilled = 0; + + // Loop over the list of processes and fill in the caller's buffer. + auto it = _processes.cbegin(); + while (it != _processes.cend() && cFilled < *pcProcessList) + { + pProcessList[cFilled] = (*it)->dwProcessId; + cFilled++; + it = std::next(it); + } + } + else + { + hr = E_NOT_SUFFICIENT_BUFFER; + } + + // Return how many items were copied (or how many values we would need to fit). + *pcProcessList = cProcesses; + + return hr; +} + +// Routine Description +// - Retrieves TERMINATION_RECORD structures for all processes known in the list (limited if necessary by parameter for group ID) +// - This is designed to copy the data so the global lock can be released while sending control information to attached processes. +// Arguments: +// - dwLimitingProcessId - Optional (0 if unused). Will restrict the return to only processes containing this group ID. +// - fCtrlClose - True if we're about to send a Ctrl Close command to the process. Will increment termination attempt count. +// - prgRecords - Pointer to callee allocated array of termination records. CALLER MUST FREE. +// - pcRecords - Length of records in prgRecords. +// Return Value: +// - S_OK if prgRecords was filled successfully or if no records were found that matched. +// - E_OUTOFMEMORY in a low memory situation. +[[nodiscard]] +HRESULT ConsoleProcessList::GetTerminationRecordsByGroupId(const DWORD dwLimitingProcessId, + const bool fCtrlClose, + _Outptr_result_buffer_all_(*pcRecords) ConsoleProcessTerminationRecord** prgRecords, + _Out_ size_t* const pcRecords) const +{ + *pcRecords = 0; + + try + { + std::deque> TermRecords; + + // Dig through known processes looking for a match + auto it = _processes.cbegin(); + while (it != _processes.cend()) + { + ConsoleProcessHandle* const pProcessHandleRecord = *it; + + // If no limit was specified OR if we have a match, generate a new termination record. + if (0 == dwLimitingProcessId || + pProcessHandleRecord->_ulProcessGroupId == dwLimitingProcessId) + { + std::unique_ptr pNewRecord = std::make_unique(); + + // If the duplicate failed, the best we can do is to skip including the process in the list and hope it goes away. + LOG_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), + pProcessHandleRecord->_hProcess.get(), + GetCurrentProcess(), + &pNewRecord->hProcess, + 0, + 0, + DUPLICATE_SAME_ACCESS)); + + pNewRecord->dwProcessID = pProcessHandleRecord->dwProcessId; + + // If we're hard closing the window, increment the counter. + if (fCtrlClose) + { + pProcessHandleRecord->_ulTerminateCount++; + } + + pNewRecord->ulTerminateCount = pProcessHandleRecord->_ulTerminateCount; + + TermRecords.push_back(std::move(pNewRecord)); + } + + it = std::next(it); + } + + // From all found matches, convert to C-style array to return + size_t const cchRetVal = TermRecords.size(); + ConsoleProcessTerminationRecord* pRetVal = new ConsoleProcessTerminationRecord[cchRetVal]; + + for (size_t i = 0; i < cchRetVal; i++) + { + pRetVal[i] = *TermRecords.at(i); + } + + *prgRecords = pRetVal; + *pcRecords = cchRetVal; + } + CATCH_RETURN(); + + return S_OK; +} + +// Routine Description: +// - Gets the first process in the list. +// - Used for reassigning a new root process. +// TODO: MSFT 9450737 - encapsulate root process logic. https://osgvsowi/9450737 +// Arguments: +// - +// Return Value: +// - Pointer to the first item in the list or nullptr if there are no items. +ConsoleProcessHandle* ConsoleProcessList::GetFirstProcess() const +{ + if (!_processes.empty()) + { + return _processes.front(); + } + + return nullptr; +} + +// Routine Description: +// - Requests that the OS change the process priority for the console and all attached client processes +// Arguments: +// - fForeground - True if console is in foreground and related processes should be prioritied. False if they can be backgrounded/deprioritized. +// Return Value: +// - +// - NOTE: Will attempt to request a change, but it's non fatal if it doesn't work. Failures will be logged to debug channel. +void ConsoleProcessList::ModifyConsoleProcessFocus(const bool fForeground) +{ + auto it = _processes.cbegin(); + while (it != _processes.cend()) + { + ConsoleProcessHandle* const pProcessHandle = *it; + + if (pProcessHandle->_hProcess != nullptr) + { + _ModifyProcessForegroundRights(pProcessHandle->_hProcess.get(), fForeground); + } + + it = std::next(it); + } + + // Do this for conhost.exe itself, too. + _ModifyProcessForegroundRights(GetCurrentProcess(), fForeground); +} + +// Routine Description: +// - Specifies that there are no remaining processes +// TODO: This should not be exposed, most likely. Whomever is calling it should join this class. +// Arguments: +// - +// Return Value: +// - True if the list is empty. False if we have known processes. +bool ConsoleProcessList::IsEmpty() const +{ + return _processes.empty(); +} + +// Routine Description: +// - Requests the OS allow the console to set one of its child processes as the foreground window +// Arguments: +// - hProcess - Handle to the process to modify +// - fForeground - True if we're allowed to set it as the foreground window. False otherwise. +// Return Value: +// - +void ConsoleProcessList::_ModifyProcessForegroundRights(const HANDLE hProcess, const bool fForeground) const +{ + LOG_IF_NTSTATUS_FAILED(ServiceLocator::LocateConsoleControl()->SetForeground(hProcess, fForeground)); +} diff --git a/src/server/ProcessList.h b/src/server/ProcessList.h new file mode 100644 index 000000000..e8766da34 --- /dev/null +++ b/src/server/ProcessList.h @@ -0,0 +1,70 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ProcessList.h + +Abstract: +- This file defines a list of process handles maintained by an instance of a console server + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +#include "ProcessHandle.h" + +// this structure is used to store relevant information from the console for ctrl processing so we can do it without +// holding the console lock. +struct ConsoleProcessTerminationRecord +{ + HANDLE hProcess; + DWORD dwProcessID; + ULONG ulTerminateCount; +}; + +class ConsoleProcessList +{ +public: + + static const DWORD ROOT_PROCESS_ID = 0; + + [[nodiscard]] + HRESULT AllocProcessData(const DWORD dwProcessId, + const DWORD dwThreadId, + const ULONG ulProcessGroupId, + _In_opt_ ConsoleProcessHandle* const pParentProcessData, + _Outptr_opt_ ConsoleProcessHandle** const ppProcessData); + + void FreeProcessData(_In_ ConsoleProcessHandle* const ProcessData); + + + ConsoleProcessHandle* FindProcessInList(const DWORD dwProcessId) const; + ConsoleProcessHandle* FindProcessByGroupId(_In_ ULONG ulProcessGroupId) const; + + [[nodiscard]] + HRESULT GetTerminationRecordsByGroupId(const DWORD dwLimitingProcessId, + const bool fCtrlClose, + _Outptr_result_buffer_all_(*pcRecords) ConsoleProcessTerminationRecord** prgRecords, + _Out_ size_t* const pcRecords) const; + + ConsoleProcessHandle* GetFirstProcess() const; + + [[nodiscard]] + HRESULT GetProcessList(_Inout_updates_(*pcProcessList) DWORD* const pProcessList, + _Inout_ size_t* const pcProcessList) const; + + void ModifyConsoleProcessFocus(const bool fForeground); + + bool IsEmpty() const; + +private: + std::list _processes; + + void _ModifyProcessForegroundRights(const HANDLE hProcess, const bool fForeground) const; +}; diff --git a/src/server/ProcessPolicy.cpp b/src/server/ProcessPolicy.cpp new file mode 100644 index 000000000..8191137ff --- /dev/null +++ b/src/server/ProcessPolicy.cpp @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "ProcessPolicy.h" + +#include "..\inc\conint.h" + +// Routine Description: +// - Constructs a new instance of the process policy class. +// Arguments: +// - All arguments specify a true/false status to a policy that could be applied to a console client app. +ConsoleProcessPolicy::ConsoleProcessPolicy(const bool fCanReadOutputBuffer, + const bool fCanWriteInputBuffer) : + _fCanReadOutputBuffer(fCanReadOutputBuffer), + _fCanWriteInputBuffer(fCanWriteInputBuffer) +{ +} + +// Routine Description: +// - Destructs an instance of the process policy class. +ConsoleProcessPolicy::~ConsoleProcessPolicy() +{ +} + +// Routine Description: +// - Opens the process token for the given handle and resolves the application model policies +// that apply to the given process handle. This may reveal restrictions on operations that are +// supposed to be enforced against a given console client application. +// Arguments: +// - hProcess - Handle to a connected process +// Return Value: +// - ConsoleProcessPolicy object containing resolved policy data. +ConsoleProcessPolicy ConsoleProcessPolicy::s_CreateInstance(const HANDLE hProcess) +{ + // If we cannot determine the policy status, then we block access by default. + bool fCanReadOutputBuffer = false; + bool fCanWriteInputBuffer = false; + + wil::unique_handle hToken; + if (LOG_IF_WIN32_BOOL_FALSE(OpenProcessToken(hProcess, TOKEN_READ, &hToken))) + { + bool fIsWrongWayBlocked = true; + + // First check AppModel Policy: + LOG_IF_FAILED(Microsoft::Console::Internal::ProcessPolicy::CheckAppModelPolicy(hToken.get(), fIsWrongWayBlocked)); + + // If we're not restricted by AppModel Policy, also check for Integrity Level below our own. + if (!fIsWrongWayBlocked) + { + LOG_IF_FAILED(Microsoft::Console::Internal::ProcessPolicy::CheckIntegrityLevelPolicy(hToken.get(), fIsWrongWayBlocked)); + } + + // If we're not blocking wrong way verbs, adjust the read/write policies to permit read out and write in. + if (!fIsWrongWayBlocked) + { + fCanReadOutputBuffer = true; + fCanWriteInputBuffer = true; + } + } + + return ConsoleProcessPolicy(fCanReadOutputBuffer, fCanWriteInputBuffer); +} + +// Routine Description: +// - Determines whether a console client should be allowed to read back from the output buffers. +// - This includes any of our classic APIs which could allow retrieving data from the output "screen buffer". +// Arguments: +// - +// Return Value: +// - True if read back is allowed. False otherwise. +bool ConsoleProcessPolicy::CanReadOutputBuffer() const +{ + return _fCanReadOutputBuffer; +} + +// Routine Description: +// - Determines whether a console client should be allowed to write to the input buffers. +// - This includes any of our classic APIs which could allow inserting data into the input buffer. +// Arguments: +// - +// Return Value: +// - True if writing input is allowed. False otherwise. +bool ConsoleProcessPolicy::CanWriteInputBuffer() const +{ + return _fCanWriteInputBuffer; +} diff --git a/src/server/ProcessPolicy.h b/src/server/ProcessPolicy.h new file mode 100644 index 000000000..370b7118e --- /dev/null +++ b/src/server/ProcessPolicy.h @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- ProcessPolicy.h + +Abstract: +- This file defines a policy framework that applies to attached client applications + to restrict or enforce certain behavior depending on the client app type. + +Author: +- Michael Niksa (miniksa) 06-Oct-2017 + +--*/ + +#pragma once + +class ConsoleProcessPolicy +{ +public: + ~ConsoleProcessPolicy(); + + static ConsoleProcessPolicy s_CreateInstance(const HANDLE hProcess); + + bool CanReadOutputBuffer() const; + bool CanWriteInputBuffer() const; + +private: + ConsoleProcessPolicy(const bool fCanReadOutputBuffer, + const bool fCanWriteInputBuffer); + + const bool _fCanReadOutputBuffer; + const bool _fCanWriteInputBuffer; +}; diff --git a/src/server/WaitBlock.cpp b/src/server/WaitBlock.cpp new file mode 100644 index 000000000..8cf8cf2c8 --- /dev/null +++ b/src/server/WaitBlock.cpp @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "WaitBlock.h" +#include "WaitQueue.h" + +#include "ApiSorter.h" + +#include "..\host\globals.h" +#include "..\host\utils.hpp" + +#include "..\interactivity\inc\ServiceLocator.hpp" + +// Routine Description: +// - Initializes a ConsoleWaitBlock +// - ConsoleWaitBlocks will self-manage their position in their two queues. +// - They will push themselves into the tail and store the iterator for constant deletion time later. +// Arguments: +// - pProcessQueue - The queue attached to the client process ID that requested this action +// - pObjectQueue - The queue attached to the console object that will service the action when data arrives +// - pWaitReplyMessage - The original API message related to the client process's service request +// - pWaiter - The context to return to later when the wait is satisfied. +ConsoleWaitBlock::ConsoleWaitBlock(_In_ ConsoleWaitQueue* const pProcessQueue, + _In_ ConsoleWaitQueue* const pObjectQueue, + const CONSOLE_API_MSG* const pWaitReplyMessage, + _In_ IWaitRoutine* const pWaiter) : + _pProcessQueue(THROW_HR_IF_NULL(E_INVALIDARG, pProcessQueue)), + _pObjectQueue(THROW_HR_IF_NULL(E_INVALIDARG, pObjectQueue)), + _pWaiter(THROW_HR_IF_NULL(E_INVALIDARG, pWaiter)) +{ + _itProcessQueue = _pProcessQueue->_blocks.insert(_pProcessQueue->_blocks.end(), this); + _itObjectQueue = _pObjectQueue->_blocks.insert(_pObjectQueue->_blocks.end(), this); + + _WaitReplyMessage = *pWaitReplyMessage; + + // We will write the original message back (with updated out parameters/payload) when the request is finally serviced. + if (pWaitReplyMessage->Complete.Write.Data != nullptr) + { + _WaitReplyMessage.Complete.Write.Data = &_WaitReplyMessage.u; + } +} + +// Routine Description: +// - Destroys a ConsolewaitBlock +// - On deletion, ConsoleWaitBlocks will erase themselves from the process and object queues in +// constant time with the iterator acquired on construction. +ConsoleWaitBlock::~ConsoleWaitBlock() +{ + _pProcessQueue->_blocks.erase(_itProcessQueue); + _pObjectQueue->_blocks.erase(_itObjectQueue); + + if (_pWaiter != nullptr) + { + delete _pWaiter; + } +} + +// Routine Description: +// - Creates and enqueues a new wait for later callback when a routine cannot be serviced at this time. +// - Will extract the process ID and the target object, enqueuing in both to know when to callback +// Arguments: +// - pWaitReplyMessage - The original API message from the client asking for servicing +// - pWaiter - The context/callback information to restore and dispatch the call later. +// Return Value: +// - S_OK if queued and ready to go. Appropriate HRESULT value if it failed. +[[nodiscard]] +HRESULT ConsoleWaitBlock::s_CreateWait(_Inout_ CONSOLE_API_MSG* const pWaitReplyMessage, + _In_ IWaitRoutine* const pWaiter) +{ + ConsoleProcessHandle* const ProcessData = pWaitReplyMessage->GetProcessHandle(); + FAIL_FAST_IF_NULL(ProcessData); + + ConsoleWaitQueue* const pProcessQueue = ProcessData->pWaitBlockQueue.get(); + + ConsoleHandleData* const pHandleData = pWaitReplyMessage->GetObjectHandle(); + FAIL_FAST_IF_NULL(pHandleData); + + ConsoleWaitQueue* pObjectQueue = nullptr; + LOG_IF_FAILED(pHandleData->GetWaitQueue(&pObjectQueue)); + FAIL_FAST_IF_NULL(pObjectQueue); + + ConsoleWaitBlock* pWaitBlock; + try + { + pWaitBlock = new ConsoleWaitBlock(pProcessQueue, + pObjectQueue, + pWaitReplyMessage, + pWaiter); + } + catch (...) + { + const HRESULT hr = wil::ResultFromCaughtException(); + pWaitReplyMessage->SetReplyStatus(NTSTATUS_FROM_HRESULT(hr)); + return hr; + } + + return S_OK; +} + +// Routine Description: +// - Used to trigger the callback routine inside this wait block. +// Arguments: +// - TerminationReason - A reason to tell the callback to terminate early or 0 if it should operate normally. +// Return Value: +// - True if the routine was able to successfully return data (or terminate). False otherwise. +bool ConsoleWaitBlock::Notify(const WaitTerminationReason TerminationReason) +{ + bool fRetVal; + + NTSTATUS status; + size_t NumBytes = 0; + DWORD dwControlKeyState; + bool fIsUnicode = true; + + std::deque> outEvents; + // TODO: MSFT 14104228 - get rid of this void* and get the data + // out of the read wait object properly. + void* pOutputData = nullptr; + // 1. Get unicode status of notify call based on message type. + // We still need to know the Unicode status on reads as they will be converted after the wait operation. + // Writes will have been converted before hitting the wait state. + switch (_WaitReplyMessage.msgHeader.ApiNumber) + { + case API_NUMBER_GETCONSOLEINPUT: + { + CONSOLE_GETCONSOLEINPUT_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.GetConsoleInput); + fIsUnicode = !!a->Unicode; + pOutputData = &outEvents; + break; + } + case API_NUMBER_READCONSOLE: + { + CONSOLE_READCONSOLE_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.ReadConsole); + fIsUnicode = !!a->Unicode; + break; + } + case API_NUMBER_WRITECONSOLE: + { + CONSOLE_WRITECONSOLE_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.WriteConsole); + fIsUnicode = !!a->Unicode; + break; + } + default: + { + FAIL_FAST_HR(E_NOTIMPL); // we shouldn't be getting a wait/notify on API numbers we don't support. + break; + } + } + + // 2. If we have a waiter, dispatch to it. + if (_pWaiter->Notify(TerminationReason, fIsUnicode, &status, &NumBytes, &dwControlKeyState, pOutputData)) + { + // 3. If the wait was successful, set reply info and attach any additional return information that this request type might need. + _WaitReplyMessage.SetReplyStatus(status); + _WaitReplyMessage.SetReplyInformation(NumBytes); + + if (API_NUMBER_GETCONSOLEINPUT == _WaitReplyMessage.msgHeader.ApiNumber) + { + // ReadConsoleInput/PeekConsoleInput has this extra reply + // information with the number of records, not number of + // bytes. + CONSOLE_GETCONSOLEINPUT_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.GetConsoleInput); + + void* buffer; + ULONG cbBuffer; + if (FAILED(_WaitReplyMessage.GetOutputBuffer(&buffer, &cbBuffer))) + { + return false; + } + + INPUT_RECORD* const pRecordBuffer = static_cast(buffer); + a->NumRecords = static_cast(outEvents.size()); + for (size_t i = 0; i < a->NumRecords; ++i) + { + if (outEvents.empty()) + { + break; + } + pRecordBuffer[i] = outEvents.front()->ToInputRecord(); + outEvents.pop_front(); + } + + } + else if (API_NUMBER_READCONSOLE == _WaitReplyMessage.msgHeader.ApiNumber) + { + // ReadConsole has this extra reply information with the control key state. + CONSOLE_READCONSOLE_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.ReadConsole); + a->ControlKeyState = dwControlKeyState; + a->NumBytes = gsl::narrow(NumBytes); + + // - This routine is called when a ReadConsole or ReadFile request is about to be completed. + // - It sets the number of bytes written as the information to be written with the completion status and, + // if CTRL+Z processing is enabled and a CTRL+Z is detected, switches the number of bytes read to zero. + if (a->ProcessControlZ != FALSE && + a->NumBytes > 0 && + _WaitReplyMessage.State.OutputBuffer != nullptr && + *(PUCHAR)_WaitReplyMessage.State.OutputBuffer == 0x1a) + { + // On changing this, we also need to notify the Reply Information because it was stowed above into the reply packet. + a->NumBytes = 0; + // Setting the reply length to 0 and returning successfully from a blocked wait + // will imply that the user has reached "End of File" on a raw read file stream. + _WaitReplyMessage.SetReplyInformation(0); + } + } + else if (API_NUMBER_WRITECONSOLE == _WaitReplyMessage.msgHeader.ApiNumber) + { + CONSOLE_WRITECONSOLE_MSG* a = &(_WaitReplyMessage.u.consoleMsgL1.WriteConsoleW); + a->NumBytes = gsl::narrow(NumBytes); + } + + LOG_IF_FAILED(_WaitReplyMessage.ReleaseMessageBuffers()); + + LOG_IF_FAILED(ServiceLocator::LocateGlobals().pDeviceComm->CompleteIo(&_WaitReplyMessage.Complete)); + + fRetVal = true; + } + else + { + // If fThreadDying is TRUE we need to make sure that we removed the pWaitBlock from the list (which we don't do on this branch). + FAIL_FAST_IF(!(WI_IsFlagClear(TerminationReason, WaitTerminationReason::ThreadDying))); + fRetVal = false; + } + + return fRetVal; +} diff --git a/src/server/WaitBlock.h b/src/server/WaitBlock.h new file mode 100644 index 000000000..f7d6d1c4e --- /dev/null +++ b/src/server/WaitBlock.h @@ -0,0 +1,56 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WaitBlock.h + +Abstract: +- This file defines a queued operation when a console buffer object cannot currently satisfy the request. + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +#include "..\host\conapi.h" +#include "IWaitRoutine.h" +#include "WaitTerminationReason.h" + +#include + +class ConsoleWaitQueue; + +class ConsoleWaitBlock +{ +public: + + ~ConsoleWaitBlock(); + + bool Notify(const WaitTerminationReason TerminationReason); + + [[nodiscard]] + static HRESULT s_CreateWait(_Inout_ CONSOLE_API_MSG* const pWaitReplymessage, + _In_ IWaitRoutine* const pWaiter); + + +private: + ConsoleWaitBlock(_In_ ConsoleWaitQueue* const pProcessQueue, + _In_ ConsoleWaitQueue* const pObjectQueue, + const CONSOLE_API_MSG* const pWaitReplyMessage, + _In_ IWaitRoutine* const pWaiter); + + ConsoleWaitQueue* const _pProcessQueue; + std::_List_const_iterator>> _itProcessQueue; + + ConsoleWaitQueue* const _pObjectQueue; + std::_List_const_iterator>> _itObjectQueue; + + CONSOLE_API_MSG _WaitReplyMessage; + + IWaitRoutine* const _pWaiter; +}; diff --git a/src/server/WaitQueue.cpp b/src/server/WaitQueue.cpp new file mode 100644 index 000000000..94244603c --- /dev/null +++ b/src/server/WaitQueue.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "WaitQueue.h" +#include "WaitBlock.h" + +#include "..\host\globals.h" +#include "..\host\utils.hpp" + +// Routine Description: +// - Instantiates a new ConsoleWaitQueue +ConsoleWaitQueue::ConsoleWaitQueue() : + _blocks() +{ + +} + +// Routine Description: +// - Destructs a ConsoleWaitQueue +// - This will notify any remaining waiting items that the associated process/object is dying. +ConsoleWaitQueue::~ConsoleWaitQueue() +{ + // Notify all blocks that the thread or object is dying when destroyed. + NotifyWaiters(TRUE, WaitTerminationReason::ThreadDying); +} + +// Routine Description: +// - Establishes a wait (call me back later) for a particular message with a given callback routine and its parameter +// Arguments: +// - pWaitReplyMessage - The API message that we're deferring until data is available later. +// - pWaiter - The context/callback information to restore and dispatch the call later. +// Return Value: +// - S_OK if enqueued appropriately and everything is alright. Or suitable HRESULT failure otherwise. +[[nodiscard]] +HRESULT ConsoleWaitQueue::s_CreateWait(_Inout_ CONSOLE_API_MSG* const pWaitReplyMessage, + _In_ IWaitRoutine* const pWaiter) +{ + // Normally we'd have the Wait Queue handle the insertion of the block into the queue, but + // the console does queues in a somewhat special way. + // + // Each block belongs in two queues: + // 1. The process queue of the client that dispatched the request + // 2. The object queue that the request will be serviced by + // As such, when a wait occurs, it gets added to both queues. + // + // It will end up being serviced by one or the other queue, but when it is serviced, it must be + // removed from both so it is not double processed. + // + // Therefore, I've inverted the queue management responsibility into the WaitBlock itself + // and made it a friend to this WaitQueue class. + + return ConsoleWaitBlock::s_CreateWait(pWaitReplyMessage, + pWaiter); +} + +// Routine Description: +// - Instructs this queue to attempt to callback waiting requests +// Arguments: +// - fNotifyAll - If true, we will notify all items in the queue. If false, we will only notify the first item. +// Return Value: +// - True if any block was successfully notified. False if no blocks were successful. +bool ConsoleWaitQueue::NotifyWaiters(const bool fNotifyAll) +{ + return NotifyWaiters(fNotifyAll, WaitTerminationReason::NoReason); +} + +// Routine Description: +// - Instructs this queue to attempt to callback waiting requests and request termination with the given reason +// Arguments: +// - fNotifyAll - If true, we will notify all items in the queue. If false, we will only notify the first item. +// - TerminationReason - A reason/message to pass to each waiter signaling it should terminate appropriately. +// Return Value: +// - True if any block was successfully notified. False if no blocks were successful. +bool ConsoleWaitQueue::NotifyWaiters(const bool fNotifyAll, + const WaitTerminationReason TerminationReason) +{ + bool fResult = false; + + auto it = _blocks.cbegin(); + while (!_blocks.empty() && it != _blocks.cend()) + { + ConsoleWaitBlock* const WaitBlock = (*it); + if (nullptr == WaitBlock) + { + break; + } + + auto const nextIt = std::next(it); // we have to capture next before it is potentially erased + + if (_NotifyBlock(WaitBlock, TerminationReason)) + { + fResult = true; + } + + if (!fNotifyAll) + { + break; + } + + it = nextIt; + } + + return fResult; +} + +// Routine Description: +// - A helper to delete successfully notified callbacks +// Arguments: +// - pWaitBlock - A block containing callback data +// - TerminationReason - Optional reason to tell the callback to terminate. If 0, we're not requesting termination. +// Return Value: +// - True if callback successfully delivered data. False if callback still needs to wait longer. +bool ConsoleWaitQueue::_NotifyBlock(_In_ ConsoleWaitBlock* pWaitBlock, + _In_ WaitTerminationReason TerminationReason) +{ + // Attempt to notify block with the given reason. + bool const fResult = pWaitBlock->Notify(TerminationReason); + + if (fResult) + { + // If it was successful, delete it. (It will remove itself from appropriate queues.) + delete pWaitBlock; + } + + return fResult; +} diff --git a/src/server/WaitQueue.h b/src/server/WaitQueue.h new file mode 100644 index 000000000..d525abeef --- /dev/null +++ b/src/server/WaitQueue.h @@ -0,0 +1,51 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WaitQueue.h + +Abstract: +- This file manages a queue of wait blocks + +Author: +- Michael Niksa (miniksa) 17-Oct-2016 + +Revision History: +- Adapted from original items in handle.h +--*/ + +#pragma once + +#include + +#include "..\host\conapi.h" + +#include "IWaitRoutine.h" +#include "WaitBlock.h" +#include "WaitTerminationReason.h" + +class ConsoleWaitQueue +{ +public: + ConsoleWaitQueue(); + + ~ConsoleWaitQueue(); + + bool NotifyWaiters(const bool fNotifyAll); + + bool NotifyWaiters(const bool fNotifyAll, + const WaitTerminationReason TerminationReason); + + [[nodiscard]] + static HRESULT s_CreateWait(_Inout_ CONSOLE_API_MSG* const pWaitReplyMessage, + _In_ IWaitRoutine* const pWaiter); + +private: + bool _NotifyBlock(_In_ ConsoleWaitBlock* pWaitBlock, + const WaitTerminationReason TerminationReason); + + std::list _blocks; + + friend class ConsoleWaitBlock; // Blocks live in multiple queues so we let them manage the lifetime. +}; diff --git a/src/server/WaitTerminationReason.h b/src/server/WaitTerminationReason.h new file mode 100644 index 000000000..c0cfd1ce3 --- /dev/null +++ b/src/server/WaitTerminationReason.h @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WaitTerminationReason.h + +Abstract: +- This file defines the reasons to terminate a wait block + +Author: +- Michael Niksa (miniksa) 12-Oct-2016 + +Revision History: +- Adapted from original items in input.h +--*/ + +#pragma once + +enum class WaitTerminationReason +{ + NoReason = 0x0, + CtrlC = 0x1, + CtrlBreak = 0x2, + ThreadDying = 0x4, + HandleClosing = 0x8 +}; + +DEFINE_ENUM_FLAG_OPERATORS(WaitTerminationReason); diff --git a/src/server/WinNTControl.cpp b/src/server/WinNTControl.cpp new file mode 100644 index 000000000..9762258bb --- /dev/null +++ b/src/server/WinNTControl.cpp @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WinNTControl.h" + +// Routine Description: +// - Creates an instance of the NTDLL method-invoking class. +// - This class helps maintain a loose coupling on NTDLL without reliance on the driver kit headers/libs. +WinNTControl::WinNTControl() : + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _NtDllDll(THROW_LAST_ERROR_IF_NULL(LoadLibraryExW(L"ntdll.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))), + _NtOpenFile(reinterpret_cast(THROW_LAST_ERROR_IF_NULL(GetProcAddress(_NtDllDll.get(), "NtOpenFile")))) +{ +} + +// Routine Description: +// - Destructs an instance of the NTDLL method-invoking class. +WinNTControl::~WinNTControl() +{ + +} + +// Routine Description: +// - Provides the singleton pattern for WinNT control. Stores the single instance and returns it. +// Arguments: +// - +// Return Value: +// - Reference to the single instance of NTDLL.dll wrapped methods. +WinNTControl& WinNTControl::GetInstance() +{ + static WinNTControl Instance; + return Instance; +} + +// Routine Description: +// - Provides access to the NtOpenFile method documented at: +// https://msdn.microsoft.com/en-us/library/bb432381(v=vs.85).aspx +// Arguments: +// - See definitions at MSDN +// Return Value: +// - See definitions at MSDN +[[nodiscard]] +NTSTATUS WinNTControl::NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions) +{ + try + { + return GetInstance()._NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions); + } + CATCH_RETURN(); +} diff --git a/src/server/WinNTControl.h b/src/server/WinNTControl.h new file mode 100644 index 000000000..dc99b9e37 --- /dev/null +++ b/src/server/WinNTControl.h @@ -0,0 +1,45 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- WinNTControl.h + +Abstract: +- This module helps wrap methods from NTDLL.dll to avoid needing Driver Kit headers in the project. + +Author: +- Michael Niksa (MiNiksa) 14-Sept-2016 + +Revision History: +--*/ + +#pragma once + +class WinNTControl +{ +public: + [[nodiscard]] + static NTSTATUS NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions); + + ~WinNTControl(); + +private: + WinNTControl(); + + WinNTControl(WinNTControl const&) = delete; + void operator=(WinNTControl const&) = delete; + + static WinNTControl& GetInstance(); + + wil::unique_hmodule const _NtDllDll; + + typedef NTSTATUS(NTAPI* PfnNtOpenFile)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, ULONG, ULONG); + PfnNtOpenFile const _NtOpenFile; + +}; diff --git a/src/server/dirs b/src/server/dirs new file mode 100644 index 000000000..95a663151 --- /dev/null +++ b/src/server/dirs @@ -0,0 +1,3 @@ +DIRS=lib \ + + diff --git a/src/server/lib/server.vcxproj b/src/server/lib/server.vcxproj new file mode 100644 index 000000000..a2a1baedc --- /dev/null +++ b/src/server/lib/server.vcxproj @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {18D09A24-8240-42D6-8CB6-236EEE820262} + Win32Proj + server + Server + ConServer + + + + $(SolutionDir)\dep;$(SolutionDir)\dep\Console;$(SolutionDir)\dep\Win32K;$(SolutionDir)\dep\MinCore;%(AdditionalIncludeDirectories) + + + + + + \ No newline at end of file diff --git a/src/server/lib/server.vcxproj.filters b/src/server/lib/server.vcxproj.filters new file mode 100644 index 000000000..4577c2fcc --- /dev/null +++ b/src/server/lib/server.vcxproj.filters @@ -0,0 +1,141 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/server/lib/sources b/src/server/lib/sources new file mode 100644 index 000000000..c89495737 --- /dev/null +++ b/src/server/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConServer +TARGETTYPE = LIBRARY diff --git a/src/server/precomp.cpp b/src/server/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/server/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/server/precomp.h b/src/server/precomp.h new file mode 100644 index 000000000..a728cd060 --- /dev/null +++ b/src/server/precomp.h @@ -0,0 +1,79 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#endif + +// Windows Header Files: +#include + +typedef long NTSTATUS; +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#define STATUS_SUCCESS ((DWORD)0x0) +#define STATUS_UNSUCCESSFUL ((DWORD)0xC0000001L) +#define STATUS_SHARING_VIOLATION ((NTSTATUS)0xC0000043L) +#define STATUS_INSUFFICIENT_RESOURCES ((DWORD)0xC000009AL) +#define STATUS_ILLEGAL_FUNCTION ((DWORD)0xC00000AFL) +#define STATUS_PIPE_DISCONNECTED ((DWORD)0xC00000B0L) +#define STATUS_BUFFER_TOO_SMALL ((DWORD)0xC0000023L) +#define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225L) + +// +// Map a WIN32 error value into an NTSTATUS +// Note: This assumes that WIN32 errors fall in the range -32k to 32k. +// + +#define FACILITY_NTWIN32 0x7 + +#define __NTSTATUS_FROM_WIN32(x) ((NTSTATUS)(x) <= 0 ? ((NTSTATUS)(x)) : ((NTSTATUS) (((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR))) + +#ifdef INLINE_NTSTATUS_FROM_WIN32 +#ifndef __midl +__inline NTSTATUS_FROM_WIN32(long x) { return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); } +#else +#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) +#endif +#else +#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) +#endif + +//#include + +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +// private dependencies +#include "..\host\conddkrefs.h" + +#include +#include +#include +#include +#include + +// TODO: MSFT 9355094 Find a better way of doing this. http://osgvsowi/9355094 +[[nodiscard]] +inline NTSTATUS NTSTATUS_FROM_HRESULT(HRESULT hr) +{ + return NTSTATUS_FROM_WIN32(HRESULT_CODE(hr)); +} diff --git a/src/server/sources.inc b/src/server/sources.inc new file mode 100644 index 000000000..e5a4adc79 --- /dev/null +++ b/src/server/sources.inc @@ -0,0 +1,56 @@ +!include ..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Server Communications Layer +# ------------------------------------- + +# This module encapsulates the communication layer between the console driver +# and the console server application. +# All IOCTL driver communications are handled in this layer as well as management +# activities like process and handle tracking. + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +# Warning 4201: nonstandard extension used: nameless struct/union +# Warning 4702: unreachable code +MSC_WARNING_LEVEL = $(MSC_WARNING_LEVEL) /wd4201 /wd4702 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES= \ + ..\ApiDispatchers.cpp \ + ..\ApiDispatchersInternal.cpp \ + ..\ApiMessage.cpp \ + ..\ApiMessageState.cpp \ + ..\ApiSorter.cpp \ + ..\DeviceComm.cpp \ + ..\DeviceHandle.cpp \ + ..\Entrypoints.cpp \ + ..\IoDispatchers.cpp \ + ..\IoSorter.cpp \ + ..\ObjectHandle.cpp \ + ..\ObjectHeader.cpp \ + ..\ProcessHandle.cpp \ + ..\ProcessList.cpp \ + ..\ProcessPolicy.cpp \ + ..\WaitBlock.cpp \ + ..\WaitQueue.cpp \ + ..\WinNTControl.cpp \ + +INCLUDES= \ + $(INCLUDES); \ + ..; \ diff --git a/src/server/winbasep.h b/src/server/winbasep.h new file mode 100644 index 000000000..03f50e5a9 --- /dev/null +++ b/src/server/winbasep.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#define ProcThreadAttributeConsoleReference 10 + +#define PROC_THREAD_ATTRIBUTE_CONSOLE_REFERENCE \ + ProcThreadAttributeValue (10, FALSE, TRUE, FALSE) diff --git a/src/terminal/adapter/DispatchCommon.cpp b/src/terminal/adapter/DispatchCommon.cpp new file mode 100644 index 000000000..24b4bb88a --- /dev/null +++ b/src/terminal/adapter/DispatchCommon.cpp @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "DispatchCommon.hpp" +#include "../../types/inc/Viewport.hpp" + +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::VirtualTerminal; + +// Method Description: +// - Resizes the window to the specified dimensions, in characters. +// Arguments: +// - conApi: The ConGetSet implementation to call back into. +// - usWidth: The new width of the window, in columns +// - usHeight: The new height of the window, in rows +// Return Value: +// True if handled successfully. False othewise. +bool DispatchCommon::s_ResizeWindow(ConGetSet& conApi, + const unsigned short usWidth, + const unsigned short usHeight) +{ + SHORT sColumns = 0; + SHORT sRows = 0; + + // We should do nothing if 0 is passed in for a size. + bool fSuccess = SUCCEEDED(UShortToShort(usWidth, &sColumns)) && + SUCCEEDED(UShortToShort(usHeight, &sRows)) && + (usWidth > 0 && usHeight > 0); + + if (fSuccess) + { + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + fSuccess = !!conApi.GetConsoleScreenBufferInfoEx(&csbiex); + + if (fSuccess) + { + const Viewport oldViewport = Viewport::FromInclusive(csbiex.srWindow); + const Viewport newViewport = Viewport::FromDimensions(oldViewport.Origin(), + sColumns, + sRows); + // Always resize the width of the console + csbiex.dwSize.X = sColumns; + // Only set the screen buffer's height if it's currently less than + // what we're requesting. + if(sRows > csbiex.dwSize.Y) + { + csbiex.dwSize.Y = sRows; + } + + // SetConsoleWindowInfo expect inclusive rects + SMALL_RECT sri = newViewport.ToInclusive(); + + // SetConsoleScreenBufferInfoEx however expects exclusive rects + SMALL_RECT sre = newViewport.ToExclusive(); + csbiex.srWindow = sre; + + fSuccess = !!conApi.SetConsoleScreenBufferInfoEx(&csbiex); + if (fSuccess) + { + fSuccess = !!conApi.SetConsoleWindowInfo(true, &sri); + } + } + } + return fSuccess; +} + +// Routine Description: +// - Force the host to repaint the screen. +// Arguments: +// - conApi: The ConGetSet implementation to call back into. +// Return Value: +// True if handled successfully. False othewise. +bool DispatchCommon::s_RefreshWindow(ConGetSet& conApi) +{ + return !!conApi.PrivateRefreshWindow(); +} + +// Routine Description: +// - Force the host to tell the renderer to not emit anything in response to the +// next resize event. This is used by VT I/O to prevent a terminal from +// requesting a resize, then having the renderer echo that to the terminal, +// then having the terminal echo back to the host... +// Arguments: +// - conApi: The ConGetSet implementation to call back into. +// Return Value: +// True if handled successfully. False othewise. +bool DispatchCommon::s_SuppressResizeRepaint(ConGetSet& conApi) +{ + return !!conApi.PrivateSuppressResizeRepaint(); +} diff --git a/src/terminal/adapter/DispatchCommon.hpp b/src/terminal/adapter/DispatchCommon.hpp new file mode 100644 index 000000000..e9385bdc6 --- /dev/null +++ b/src/terminal/adapter/DispatchCommon.hpp @@ -0,0 +1,35 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- DispatchCommon.hpp + +Abstract: +- Defines a number of common functions and enums whose implementation is the + same in both the AdaptDispatch and the InteractDispatch. + +Author(s): +- Mike Griese (migrie) 11 Oct 2017 +--*/ + +#pragma once + +#include "conGetSet.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class DispatchCommon final + { + public: + + static bool s_ResizeWindow(ConGetSet& conApi, + const unsigned short usWidth, + const unsigned short usHeight); + + static bool s_RefreshWindow(ConGetSet& conApi); + + static bool s_SuppressResizeRepaint(ConGetSet& conApi); + + }; +} diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp new file mode 100644 index 000000000..dd159bf88 --- /dev/null +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace Microsoft::Console::VirtualTerminal::DispatchTypes +{ + enum class EraseType : unsigned int + { + ToEnd = 0, + FromBeginning = 1, + All = 2, + Scrollback = 3 + }; + + enum GraphicsOptions : unsigned int + { + Off = 0, + BoldBright = 1, + RGBColor = 2, + // 2 is also Faint, decreased intensity (ISO 6429). + Underline = 4, + Xterm256Index = 5, + // 5 is also Blink (appears as Bold). + // the 2 and 5 entries here are only for the extended graphics options + // as we do not currently support those features individually + Negative = 7, + UnBold = 22, + NoUnderline = 24, + Positive = 27, + ForegroundBlack = 30, + ForegroundRed = 31, + ForegroundGreen = 32, + ForegroundYellow = 33, + ForegroundBlue = 34, + ForegroundMagenta = 35, + ForegroundCyan = 36, + ForegroundWhite = 37, + ForegroundExtended = 38, + ForegroundDefault = 39, + BackgroundBlack = 40, + BackgroundRed = 41, + BackgroundGreen = 42, + BackgroundYellow = 43, + BackgroundBlue = 44, + BackgroundMagenta = 45, + BackgroundCyan = 46, + BackgroundWhite = 47, + BackgroundExtended = 48, + BackgroundDefault = 49, + BrightForegroundBlack = 90, + BrightForegroundRed = 91, + BrightForegroundGreen = 92, + BrightForegroundYellow = 93, + BrightForegroundBlue = 94, + BrightForegroundMagenta = 95, + BrightForegroundCyan = 96, + BrightForegroundWhite = 97, + BrightBackgroundBlack = 100, + BrightBackgroundRed = 101, + BrightBackgroundGreen = 102, + BrightBackgroundYellow = 103, + BrightBackgroundBlue = 104, + BrightBackgroundMagenta = 105, + BrightBackgroundCyan = 106, + BrightBackgroundWhite = 107, + }; + + enum class AnsiStatusType : unsigned int + { + CPR_CursorPositionReport = 6, + }; + + enum PrivateModeParams : unsigned short + { + DECCKM_CursorKeysMode = 1, + DECCOLM_SetNumberOfColumns = 3, + ATT610_StartCursorBlink = 12, + DECTCEM_TextCursorEnableMode = 25, + VT200_MOUSE_MODE = 1000, + BUTTTON_EVENT_MOUSE_MODE = 1002, + ANY_EVENT_MOUSE_MODE = 1003, + UTF8_EXTENDED_MODE = 1005, + SGR_EXTENDED_MODE = 1006, + ALTERNATE_SCROLL = 1007, + ASB_AlternateScreenBuffer = 1049 + }; + + enum VTCharacterSets : wchar_t + { + DEC_LineDrawing = L'0', + USASCII = L'B' + }; + + enum TabClearType : unsigned short + { + ClearCurrentColumn = 0, + ClearAllColumns = 3 + }; + + enum WindowManipulationType : unsigned int + { + Invalid = 0, + RefreshWindow = 7, + ResizeWindowInCharacters = 8, + }; + + enum class CursorStyle : unsigned int + { + BlinkingBlock = 0, + BlinkingBlockDefault = 1, + SteadyBlock = 2, + BlinkingUnderline = 3, + SteadyUnderline = 4, + BlinkingBar = 5, + SteadyBar = 6 + }; + + constexpr short s_sDECCOLMSetColumns = 132; + constexpr short s_sDECCOLMResetColumns = 80; + +} diff --git a/src/terminal/adapter/IInteractDispatch.hpp b/src/terminal/adapter/IInteractDispatch.hpp new file mode 100644 index 000000000..94c5d8670 --- /dev/null +++ b/src/terminal/adapter/IInteractDispatch.hpp @@ -0,0 +1,42 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- InteractDispatch.hpp + +Abstract: +- Base class for Input State Machine callbacks. When actions occur, they will + be dispatched to the methods on this interface which must be implemented by + a child class and passed into the state machine on creation. + +Author(s): +- Mike Griese (migrie) 11 Oct 2017 +--*/ +#pragma once + +#include "DispatchTypes.hpp" +#include "../../types/inc/IInputEvent.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class IInteractDispatch + { + public: + virtual ~IInteractDispatch() = default; + + virtual bool WriteInput(_In_ std::deque>& inputEvents) = 0; + + virtual bool WriteCtrlC() = 0; + + virtual bool WriteString(_In_reads_(cch) const wchar_t* const pws, const size_t cch) = 0; + + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) = 0; + + virtual bool MoveCursor(const unsigned int row, + const unsigned int col) = 0; + + }; +} diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp new file mode 100644 index 000000000..6f4bcd9a2 --- /dev/null +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- ITermDispatch.hpp + +Abstract: +- This is the interface for all output state machine callbacks. When actions + occur, they will be dispatched to the methods on this interface which must + be implemented by a child class and passed into the state machine on + creation. +*/ +#pragma once +#include "DispatchTypes.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class ITermDispatch; +}; + +class Microsoft::Console::VirtualTerminal::ITermDispatch +{ +public: + virtual ~ITermDispatch() = 0; + virtual void Execute(const wchar_t wchControl) = 0; + virtual void Print(const wchar_t wchPrintable) = 0; + virtual void PrintString(const wchar_t* const rgwch, const size_t cch) = 0; + + virtual bool CursorUp(const unsigned int uiDistance) = 0; // CUU + virtual bool CursorDown(const unsigned int uiDistance) = 0; // CUD + virtual bool CursorForward(const unsigned int uiDistance) = 0; // CUF + virtual bool CursorBackward(const unsigned int uiDistance) = 0; // CUB + virtual bool CursorNextLine(const unsigned int uiDistance) = 0; // CNL + virtual bool CursorPrevLine(const unsigned int uiDistance) = 0; // CPL + virtual bool CursorHorizontalPositionAbsolute(const unsigned int uiColumn) = 0; // CHA + virtual bool VerticalLinePositionAbsolute(const unsigned int uiLine) = 0; // VPA + virtual bool CursorPosition(const unsigned int uiLine, const unsigned int uiColumn) = 0; // CUP + virtual bool CursorSavePosition() = 0; // DECSC + virtual bool CursorRestorePosition() = 0; // DECRC + virtual bool CursorVisibility(const bool fIsVisible) = 0; // DECTCEM + virtual bool InsertCharacter(const unsigned int uiCount) = 0; // ICH + virtual bool DeleteCharacter(const unsigned int uiCount) = 0; // DCH + virtual bool ScrollUp(const unsigned int uiDistance) = 0; // SU + virtual bool ScrollDown(const unsigned int uiDistance) = 0; // SD + virtual bool InsertLine(const unsigned int uiDistance) = 0; // IL + virtual bool DeleteLine(const unsigned int uiDistance) = 0; // DL + virtual bool SetColumns(const unsigned int uiColumns) = 0; // DECSCPP, DECCOLM + virtual bool SetCursorKeysMode(const bool fApplicationMode) = 0; // DECCKM + virtual bool SetKeypadMode(const bool fApplicationMode) = 0; // DECKPAM, DECKPNM + virtual bool EnableCursorBlinking(const bool fEnable) = 0; // ATT610 + virtual bool SetTopBottomScrollingMargins(const SHORT sTopMargin, const SHORT sBottomMargin) = 0; // DECSTBM + virtual bool ReverseLineFeed() = 0; // RI + virtual bool SetWindowTitle(std::wstring_view title) = 0; // OscWindowTitle + virtual bool UseAlternateScreenBuffer() = 0; // ASBSET + virtual bool UseMainScreenBuffer() = 0; // ASBRST + virtual bool HorizontalTabSet() = 0; // HTS + virtual bool ForwardTab(const SHORT sNumTabs) = 0; // CHT + virtual bool BackwardsTab(const SHORT sNumTabs) = 0; // CBT + virtual bool TabClear(const SHORT sClearType) = 0; // TBC + virtual bool EnableVT200MouseMode(const bool fEnabled) = 0; // ?1000 + virtual bool EnableUTF8ExtendedMouseMode(const bool fEnabled) = 0; // ?1005 + virtual bool EnableSGRExtendedMouseMode(const bool fEnabled) = 0; // ?1006 + virtual bool EnableButtonEventMouseMode(const bool fEnabled) = 0; // ?1002 + virtual bool EnableAnyEventMouseMode(const bool fEnabled) = 0; // ?1003 + virtual bool EnableAlternateScroll(const bool fEnabled) = 0; // ?1007 + virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD dwColor) = 0; // OSCColorTable + + virtual bool EraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // ED + virtual bool EraseInLine(const DispatchTypes::EraseType eraseType) = 0; // EL + virtual bool EraseCharacters(const unsigned int uiNumChars) = 0; // ECH + + virtual bool SetGraphicsRendition(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions) = 0; // SGR + + virtual bool SetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, + const size_t cParams) = 0; // DECSET + + virtual bool ResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, + const size_t cParams) = 0; // DECRST + + virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) = 0; // DSR + virtual bool DeviceAttributes() = 0; // DA + + virtual bool DesignateCharset(const wchar_t wchCharset) = 0; // DesignateCharset + + virtual bool SoftReset() = 0; // DECSTR + virtual bool HardReset() = 0; // RIS + + virtual bool SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) = 0; // DECSCUSR + virtual bool SetCursorColor(const COLORREF Color) = 0; // OSCSetCursorColor, OSCResetCursorColor + + // DTTERM_WindowManipulation + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) = 0; + +}; +inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() { } + diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp new file mode 100644 index 000000000..6572b655d --- /dev/null +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "InteractDispatch.hpp" +#include "DispatchCommon.hpp" +#include "conGetSet.hpp" +#include "../../types/inc/Viewport.hpp" +#include "../../types/inc/convert.hpp" +#include "../../inc/unicode.hpp" + +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::VirtualTerminal; + +// takes ownership of pConApi +InteractDispatch::InteractDispatch(ConGetSet* const pConApi) + : _pConApi(THROW_IF_NULL_ALLOC(pConApi)) +{ + +} + +// Method Description: +// - Writes a collection of input to the host. The new input is appended to the +// end of the input buffer. +// If Ctrl+C is written with this function, it will not trigger a Ctrl-C +// interrupt in the client, but instead write a Ctrl+C to the input buffer +// to be read by the client. +// Arguments: +// - inputEvents: a collection of IInputEvents +// Return Value: +// True if handled successfully. False otherwise. +bool InteractDispatch::WriteInput(_In_ std::deque>& inputEvents) +{ + size_t dwWritten = 0; + return !!_pConApi->PrivateWriteConsoleInputW(inputEvents, dwWritten); +} + +// Method Description: +// - Writes a Ctrl-C event to the host. The host will then decide what to do +// with it, including potentially sending an interrupt to a client +// application. +// Arguments: +// +// Return Value: +// True if handled successfully. False otherwise. +bool InteractDispatch::WriteCtrlC() +{ + KeyEvent key = KeyEvent(true, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); + return !!_pConApi->PrivateWriteConsoleControlInput(key); +} + +// Method Description: +// - Writes a string of input to the host. The string is converted to keystrokes +// that will faithfully represent the input by CharToKeyEvents. +// Arguments: +// - pws: a string to write to the console. +// - cch: the number of chars in pws. +// Return Value: +// True if handled successfully. False otherwise. +bool InteractDispatch::WriteString(_In_reads_(cch) const wchar_t* const pws, + const size_t cch) +{ + if (cch == 0) + { + return true; + } + + unsigned int codepage = 0; + bool fSuccess = !!_pConApi->GetConsoleOutputCP(&codepage); + if (fSuccess) + { + std::deque> keyEvents; + + for (size_t i = 0; i < cch; ++i) + { + std::deque> convertedEvents = CharToKeyEvents(pws[i], codepage); + + std::move(convertedEvents.begin(), + convertedEvents.end(), + std::back_inserter(keyEvents)); + } + + fSuccess = WriteInput(keyEvents); + } + return fSuccess; +} + +//Method Description: +// Window Manipulation - Performs a variety of actions relating to the window, +// such as moving the window position, resizing the window, querying +// window state, forcing the window to repaint, etc. +// This is kept seperate from the output version, as there may be +// codes that are supported in one direction but not the other. +//Arguments: +// - uiFunction - An identifier of the WindowManipulation function to perform +// - rgusParams - Additional parameters to pass to the function +// - cParams - size of rgusParams +// Return value: +// True if handled successfully. False otherwise. +bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) +{ + bool fSuccess = false; + // Other Window Manipulation functions: + // MSFT:13271098 - QueryViewport + // MSFT:13271146 - QueryScreenSize + switch (uiFunction) + { + case DispatchTypes::WindowManipulationType::RefreshWindow: + if (cParams == 0) + { + fSuccess = DispatchCommon::s_RefreshWindow(*_pConApi); + } + break; + case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: + if (cParams == 2) + { + fSuccess = DispatchCommon::s_ResizeWindow(*_pConApi, rgusParams[1], rgusParams[0]); + if (fSuccess) + { + DispatchCommon::s_SuppressResizeRepaint(*_pConApi); + } + } + break; + default: + fSuccess = false; + break; + } + + return fSuccess; +} + +//Method Description: +// Move Cursor: Moves the cursor to the provided VT coordinates. This is the +// coordinate space where 1,1 is the top left cell of the viewport. +//Arguments: +// - row: The row to move the cursor to. +// - col: The column to move the cursor to. +// Return value: +// True if we successfully moved the cursor to the given location. +// False otherwise, including if given invalid coordinates (either component being 0) +// or if any API calls failed. +bool InteractDispatch::MoveCursor(const unsigned int row, const unsigned int col) +{ + unsigned int uiRow = row; + unsigned int uiCol = col; + + bool fSuccess = true; + // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. + if (row != 0) + { + uiRow = row - 1; + } + else + { + // The parser should never return 0 (0 maps to 1), so this is a failure condition. + fSuccess = false; + } + + if (col != 0) + { + uiCol = col - 1; + } + else + { + // The parser should never return 0 (0 maps to 1), so this is a failure condition. + fSuccess = false; + } + + if (fSuccess) + { + // First retrieve some information about the buffer + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + fSuccess = !!_pConApi->GetConsoleScreenBufferInfoEx(&csbiex); + + if (fSuccess) + { + COORD coordCursor = csbiex.dwCursorPosition; + + // Safely convert the UINT positions we were given into shorts (which is the size the console deals with) + fSuccess = SUCCEEDED(UIntToShort(uiRow, &coordCursor.Y)) && + SUCCEEDED(UIntToShort(uiCol, &coordCursor.X)); + + if (fSuccess) + { + // Set the line and column values as offsets from the viewport edge. Use safe math to prevent overflow. + fSuccess = SUCCEEDED(ShortAdd(coordCursor.Y, csbiex.srWindow.Top, &coordCursor.Y)) && + SUCCEEDED(ShortAdd(coordCursor.X, csbiex.srWindow.Left, &coordCursor.X)); + + if (fSuccess) + { + // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. + coordCursor.Y = std::clamp(coordCursor.Y, csbiex.srWindow.Top, gsl::narrow(csbiex.srWindow.Bottom - 1)); + coordCursor.X = std::clamp(coordCursor.X, csbiex.srWindow.Left, gsl::narrow(csbiex.srWindow.Right - 1)); + + // Finally, attempt to set the adjusted cursor position back into the console. + fSuccess = !!_pConApi->SetConsoleCursorPosition(coordCursor); + } + } + } + } + + return fSuccess; +} diff --git a/src/terminal/adapter/InteractDispatch.hpp b/src/terminal/adapter/InteractDispatch.hpp new file mode 100644 index 000000000..a49b07eec --- /dev/null +++ b/src/terminal/adapter/InteractDispatch.hpp @@ -0,0 +1,44 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- adaptDispatch.hpp + +Abstract: +- This serves as the Windows Console API-specific implementation of the + callbacks from our generic Virtual Terminal Input parser. + +Author(s): +- Mike Griese (migrie) 11 Oct 2017 +--*/ +#pragma once + +#include "DispatchTypes.hpp" +#include "IInteractDispatch.hpp" +#include "conGetSet.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class InteractDispatch : public IInteractDispatch + { + public: + + InteractDispatch(ConGetSet* const pConApi); + + virtual ~InteractDispatch() override = default; + + virtual bool WriteInput(_In_ std::deque>& inputEvents) override; + virtual bool WriteCtrlC() override; + virtual bool WriteString(_In_reads_(cch) const wchar_t* const pws, const size_t cch) override; + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) override; // DTTERM_WindowManipulation + virtual bool MoveCursor(const unsigned int row, + const unsigned int col) override; + private: + + std::unique_ptr _pConApi; + + }; +} diff --git a/src/terminal/adapter/MouseInput.cpp b/src/terminal/adapter/MouseInput.cpp new file mode 100644 index 000000000..bce739a9c --- /dev/null +++ b/src/terminal/adapter/MouseInput.cpp @@ -0,0 +1,737 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "MouseInput.hpp" + +#include "strsafe.h" +#include +#include +using namespace Microsoft::Console::VirtualTerminal; + +#define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES +#include + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "..\..\interactivity\inc\VtApiRedirection.hpp" +#endif + +// 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." +#define KEY_PRESSED 0x8000 + +// Alternate scroll sequences +#define CURSOR_UP_SEQUENCE (L"\x1b[A") +#define CURSOR_DOWN_SEQUENCE (L"\x1b[B") +#define CCH_CURSOR_SEQUENCES (3) + +MouseInput::MouseInput(const WriteInputEvents pfnWriteEvents) : + _pfnWriteEvents(pfnWriteEvents), + _coordLastPos{ -1, -1 }, + _lastButton{ 0 } +{ + +} + +MouseInput::~MouseInput() +{ + +} + +// Routine Description: +// - Determines if the input windows message code describes a button event +// (left, middle, right button and any of up, down or double click) +// Also returns true for wheel events, which are buttons in *nix terminals +// Parameters: +// - uiButton - the message to decode. +// Return value: +// - true iff uiButton is a button message to translate +bool MouseInput::s_IsButtonMsg(const unsigned int uiButton) +{ + bool fIsButton = false; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + fIsButton = true; + break; + } + return fIsButton; +} + +// Routine Description: +// - Determines if the input windows message code describes a hover event +// Parameters: +// - uiButtonCode - the message to decode. +// Return value: +// - true iff uiButtonCode is a hover enent to translate +bool MouseInput::s_IsHoverMsg(const unsigned int uiButtonCode) +{ + return uiButtonCode == WM_MOUSEMOVE; +} + +// Routine Description: +// - Determines if the input windows message code describes a button press +// (either down or doubleclick) +// Parameters: +// - uiButton - the message to decode. +// Return value: +// - true iff uiButton is a button down event +bool MouseInput::s_IsButtonDown(const unsigned int uiButton) +{ + bool fIsButtonDown = false; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + fIsButtonDown = true; + break; + } + return fIsButtonDown; +} + +// Routine Description: +// - translates the input windows mouse message into it's equivalent X11 encoding. +// X Button Encoding: +// |7|6|5|4|3|2|1|0| +// | |W|H|M|C|S|B|B| +// bits 0 and 1 are used for button: +// 00 - MB1 pressed (left) +// 01 - MB2 pressed (middle) +// 10 - MB3 pressed (right) +// 11 - released (none) +// Next three bits indicate modifier keys: +// 0x04 - shift (This never makes it through, as our emulator is skipped when shift is pressed.) +// 0x08 - ctrl +// 0x10 - meta +// 32 (x20) is added for "hover" events: +// "For example, motion into cell x,y with button 1 down is reported as `CSI M @ CxCy`. +// ( @ = 32 + 0 (button 1) + 32 (motion indicator) ). +// Similarly, motion with button 3 down is reported as `CSI M B CxCy`. +// ( B = 32 + 2 (button 3) + 32 (motion indicator) ). +// 64 (x40) is added for wheel events. +// so wheel up? is 64, and wheel down? is 65. +// +// Parameters: +// - uiButton - the message to decode. +// Return value: +// - the int representing the equivalent X button encoding. +int MouseInput::s_WindowsButtonToXEncoding(const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta) +{ + int iXValue = 0; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + iXValue = 0; + break; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + iXValue = 3; + break; + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + iXValue = 2; + break; + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + iXValue = 1; + break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + iXValue = sWheelDelta > 0 ? 0x40 : 0x41; + } + if (fIsHover) + { + iXValue += 0x20; + } + + // Shift will never pass through to us, because shift is used by the host to skip VT mouse and use the default handler. + // TODO: MSFT:8804719 Add an option to disable/remap shift as a bypass for VT mousemode handling + // iXValue += (sModifierKeystate & MK_SHIFT) ? 0x04 : 0x00; + iXValue += (sModifierKeystate & MK_CONTROL) ? 0x08 : 0x00; + // Unfortunately, we don't get meta/alt as a part of mouse events. Only Ctrl and Shift. + // iXValue += (sModifierKeystate & MK_META) ? 0x10 : 0x00; + + return iXValue; +} + + +// Routine Description: +// - translates the input windows mouse message into it's equivalent SGR encoding. +// This is nearly identical to the X encoding, with an important difference. +// The button is always encoded as 0, 1, 2. +// 3 is reserved for mouse hovers with _no_ buttons pressed. +// See MSFT:19461988 and https://github.com/Microsoft/console/issues/296 +// Parameters: +// - uiButton - the message to decode. +// Return value: +// - the int representing the equivalent X button encoding. +int MouseInput::s_WindowsButtonToSGREncoding(const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta) +{ + int iXValue = 0; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + iXValue = 0; + break; + case WM_RBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + iXValue = 2; + break; + case WM_MBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + iXValue = 1; + break; + case WM_MOUSEMOVE: + iXValue = 3; + break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + iXValue = sWheelDelta > 0 ? 0x40 : 0x41; + } + if (fIsHover) + { + iXValue += 0x20; + } + + // Shift will never pass through to us, because shift is used by the host to skip VT mouse and use the default handler. + // TODO: MSFT:8804719 Add an option to disable/remap shift as a bypass for VT mousemode handling + // iXValue += (sModifierKeystate & MK_SHIFT) ? 0x04 : 0x00; + iXValue += (sModifierKeystate & MK_CONTROL) ? 0x08 : 0x00; + // Unfortunately, we don't get meta/alt as a part of mouse events. Only Ctrl and Shift. + // iXValue += (sModifierKeystate & MK_META) ? 0x10 : 0x00; + + return iXValue; +} + +// Routine Description: +// - Attempt to handle the given mouse coordinates and windows button as a VT-style mouse event. +// If the event should be transmitted in the selected mouse mode, then we'll try and +// encode the event according to the rules of the selected ExtendedMode, and insert those characters into the input buffer. +// Parameters: +// - coordMousePosition - The windows coordinates (top,left = 0,0) of the mouse event +// - uiButton - the message to decode. +// - sModifierKeystate - the modifier keys pressed with this button +// - sWheelDelta - the amount that the scroll wheel changed (should be 0 unless uiButton is a WM_MOUSE*WHEEL) +// Return value: +// - true if the event was handled and we should stop event propagation to the default window handler. +bool MouseInput::HandleMouse(const COORD coordMousePosition, + const unsigned int uiButton, + const short sModifierKeystate, + const short sWheelDelta) +{ + bool fSuccess = false; + if (_ShouldSendAlternateScroll(uiButton, sWheelDelta)) + { + fSuccess = _SendAlternateScroll(sWheelDelta); + } + else + { + fSuccess = (_TrackingMode != TrackingMode::None); + if (fSuccess) + { + // fIsHover is only true for WM_MOUSEMOVE events + const bool fIsHover = s_IsHoverMsg(uiButton); + const bool fIsButton = s_IsButtonMsg(uiButton); + + const bool fSameCoord = (coordMousePosition.X == _coordLastPos.X) && + (coordMousePosition.Y == _coordLastPos.Y) && + (_lastButton == uiButton); + + // If we have a WM_MOUSEMOVE, we need to know if any of the mouse + // buttons are actually pressed. If they are, + // s_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 unsigned int uiRealButton = fIsHover ? s_GetPressedButton() : uiButton; + + // 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 bool physicalButtonPressed = uiRealButton != WM_LBUTTONUP; + + fSuccess = (fIsButton && _TrackingMode != TrackingMode::None) || + (fIsHover && _TrackingMode == TrackingMode::ButtonEvent && ((!fSameCoord) && (physicalButtonPressed))) || + (fIsHover && _TrackingMode == TrackingMode::AnyEvent && !fSameCoord); + if (fSuccess) + { + wchar_t* pwchSequence = nullptr; + size_t cchSequenceLength = 0; + switch (_ExtendedMode) + { + case ExtendedMode::None: + fSuccess = _GenerateDefaultSequence(coordMousePosition, + uiRealButton, + fIsHover, + sModifierKeystate, + sWheelDelta, + &pwchSequence, + &cchSequenceLength); + break; + case ExtendedMode::Utf8: + fSuccess = _GenerateUtf8Sequence(coordMousePosition, + uiRealButton, + fIsHover, + sModifierKeystate, + sWheelDelta, + &pwchSequence, + &cchSequenceLength); + break; + case ExtendedMode::Sgr: + // 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. + fSuccess = _GenerateSGRSequence(coordMousePosition, + physicalButtonPressed ? uiRealButton : uiButton, + s_IsButtonDown(uiRealButton), // Use uiRealButton here, to properly get the up/down state + fIsHover, + sModifierKeystate, + sWheelDelta, + &pwchSequence, + &cchSequenceLength); + break; + case ExtendedMode::Urxvt: + default: + fSuccess = false; + break; + } + if (fSuccess) + { + _SendInputSequence(pwchSequence, cchSequenceLength); + delete[] pwchSequence; + fSuccess = true; + } + if (_TrackingMode == TrackingMode::ButtonEvent || _TrackingMode == TrackingMode::AnyEvent) + { + _coordLastPos.X = coordMousePosition.X; + _coordLastPos.Y = coordMousePosition.Y; + _lastButton = uiButton; + } + } + } + } + return fSuccess; +} + +// Routine Description: +// - Generates a sequence encoding the mouse event according to the default scheme. +// see http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +// Parameters: +// - coordMousePosition - The windows coordinates (top,left = 0,0) of the mouse event +// - uiButton - the message to decode. +// - fIsHover - true if the sequence is generated in response to a mouse hover +// - sModifierKeystate - the modifier keys pressed with this button +// - sWheelDelta - the amount that the scroll wheel changed (should be 0 unless uiButton 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. +bool MouseInput::_GenerateDefaultSequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const +{ + bool fSuccess = false; + + // 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. + // There are more details in _GenerateUtf8Sequence, but basically, we can't put anything above x80 into the input + // stream without bash.exe trying to convert it into utf8, and generating extra bytes in the process. + if (coordMousePosition.X <= MouseInput::s_MaxDefaultCoordinate && coordMousePosition.Y <= MouseInput::s_MaxDefaultCoordinate) + { + const COORD coordVTCoords = s_WinToVTCoord(coordMousePosition); + const short sEncodedX = s_EncodeDefaultCoordinate(coordVTCoords.X); + const short sEncodedY = s_EncodeDefaultCoordinate(coordVTCoords.Y); + wchar_t* pwchFormat = new(std::nothrow) wchar_t[7]{ L"\x1b[Mbxy" }; + if (pwchFormat != nullptr) + { + pwchFormat[3] = ' ' + (short)s_WindowsButtonToXEncoding(uiButton, fIsHover, sModifierKeystate, sWheelDelta); + pwchFormat[4] = sEncodedX; + pwchFormat[5] = sEncodedY; + + *ppwchSequence = pwchFormat; + *pcchLength = 7; + fSuccess = true; + } + } + + return fSuccess; +} + +// Routine Description: +// - Generates a sequence encoding the mouse event according to the UTF8 Extended scheme. +// see http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Extended-coordinates +// Parameters: +// - coordMousePosition - The windows coordinates (top,left = 0,0) of the mouse event +// - uiButton - the message to decode. +// - fIsHover - true if the sequence is generated in response to a mouse hover +// - sModifierKeystate - the modifier keys pressed with this button +// - sWheelDelta - the amount that the scroll wheel changed (should be 0 unless uiButton 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. +bool MouseInput::_GenerateUtf8Sequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const +{ + bool fSuccess = false; + + // So we have some complications here. + // The windows input stream is typically encoded as UTF16. + // Bash.exe knows this, and converts the utf16 input, character by character, into utf8, to send to wsl. + // So, if we want to emit a char > x80 here, great. bash.exe will convert the x80 into xC280 and pass that along, which is great. + // The *nix application was expecting a utf8 stream, and it got one. + // However, a normal windows program asks for utf8 mode, then it gets the utf16 encoded result. This is not what it wanted. + // It was looking for \x1b[M#\xC280y and got \x1b[M#\x0080y + // Now, I'd argue that in requesting utf8 mode, the application should be enlightened enough to not want the utf16 input stream, + // and convert it the same way bash.exe does. + // Though, the point could be made to place the utf8 bytes into the input, and read them that way. + // However, if we did this, bash.exe would translate those bytes thinking they're utf16, and x80->xC280->xC382C280 + // So bash would also need to change, but how could it tell the difference between them? no real good way. + // I'm going to emit a utf16 encoded value for now. Besides, if a windows program really wants it, just use the SGR mode, which is unambiguous. + // TODO: Followup once the UTF-8 input stack is ready, MSFT:8509613 + if (coordMousePosition.X <= (SHORT_MAX - 33) && coordMousePosition.Y <= (SHORT_MAX - 33)) + { + const COORD coordVTCoords = s_WinToVTCoord(coordMousePosition); + const short sEncodedX = s_EncodeDefaultCoordinate(coordVTCoords.X); + const short sEncodedY = s_EncodeDefaultCoordinate(coordVTCoords.Y); + wchar_t* pwchFormat = new(std::nothrow) wchar_t[7]{ L"\x1b[Mbxy" }; + if (pwchFormat != nullptr) + { + // The short cast is safe because we know s_WindowsButtonToXEncoding never returns more than xff + pwchFormat[3] = ' ' + (short)s_WindowsButtonToXEncoding(uiButton, fIsHover, sModifierKeystate, sWheelDelta); + pwchFormat[4] = sEncodedX; + pwchFormat[5] = sEncodedY; + + *ppwchSequence = pwchFormat; + *pcchLength = 7; + fSuccess = true; + } + } + + return fSuccess; +} + +// Routine Description: +// - Generates a sequence encoding the mouse event according to the SGR Extended scheme. +// see http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Extended-coordinates +// Parameters: +// - coordMousePosition - The windows coordinates (top,left = 0,0) of the mouse event +// - uiButton - the message to decode. WM_MOUSERMOVE is used for mouse hovers with no buttons pressed. +// - isDown - true iff a mouse button was pressed. +// - fIsHover - true if the sequence is generated in response to a mouse hover +// - sModifierKeystate - the modifier keys pressed with this button +// - sWheelDelta - the amount that the scroll wheel changed (should be 0 unless uiButton 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. +bool MouseInput::_GenerateSGRSequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool isDown, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const +{ + // Format for SGR events is: + // "\x1b[<%d;%d;%d;%c", xButton, x+1, y+1, fButtonDown? 'M' : 'm' + bool fSuccess = false; + const int iXButton = s_WindowsButtonToSGREncoding(uiButton, fIsHover, sModifierKeystate, sWheelDelta); + + #pragma warning( push ) + #pragma warning( disable: 4996 ) + // Disable 4996 - The _s version of _snprintf doesn't return the cch if the buffer is null, and we need the cch + #pragma prefast(suppress:28719, "Using the output of _snwprintf to determine cch. _snwprintf_s used below.") + int iNeededChars = _snwprintf(nullptr, 0, L"\x1b[<%d;%d;%d%c", + iXButton, coordMousePosition.X+1, coordMousePosition.Y+1, isDown ? L'M' : L'm'); + + #pragma warning( pop ) + + iNeededChars += 1; // for null + + wchar_t* pwchFormat = new(std::nothrow) wchar_t[iNeededChars]; + if (pwchFormat != nullptr) + { + int iTakenChars = _snwprintf_s(pwchFormat, + iNeededChars, + iNeededChars, + L"\x1b[<%d;%d;%d%c", + iXButton, + coordMousePosition.X+1, + coordMousePosition.Y+1, + isDown ? L'M' : L'm'); + if (iTakenChars == iNeededChars-1) // again, adjust for null + { + *ppwchSequence = pwchFormat; + *pcchLength = iTakenChars; + fSuccess = true; + } + else + { + delete[] pwchFormat; + } + } + return fSuccess; +} + +// Routine Description: +// - Either enables or disables UTF-8 extended mode encoding. This *should* cause +// the coordinates of a mouse event to be encoded as a UTF-8 byte stream, however, because windows' input is +// typically UTF-16 encoded, it emits a UTF-16 stream. +// Does NOT enable or disable mouse mode by itself. This matches the behavior I found in Ubuntu terminals. +// Parameters: +// - fEnable - either enable or disable. +// Return value: +// +void MouseInput::SetUtf8ExtendedMode(const bool fEnable) +{ + _ExtendedMode = fEnable ? ExtendedMode::Utf8 : ExtendedMode::None; +} + +// Routine Description: +// - Either enables or disables SGR extended mode encoding. This causes the +// coordinates of a mouse event to be emitted in a human readable format, +// eg, x,y=203,504 -> "^[[ +void MouseInput::SetSGRExtendedMode(const bool fEnable) +{ + _ExtendedMode = fEnable ? ExtendedMode::Sgr : ExtendedMode::None; +} + +// Routine Description: +// - Either enables or disables mouse mode handling. Leaves the extended mode alone, +// so if we disable then re-enable mouse mode without toggling an extended mode, the mode will persist. +// Parameters: +// - fEnable - either enable or disable. +// Return value: +// +void MouseInput::EnableDefaultTracking(const bool fEnable) +{ + _TrackingMode = fEnable ? TrackingMode::Default : TrackingMode::None; + _coordLastPos = {-1,-1}; // Clear out the last saved mouse position & button. + _lastButton = 0; +} + +// Routine Description: +// - Either enables or disables ButtonEvent mouse handling. Button Event mode +// sends additional sequences when a button is pressed and the mouse changes character cells. +// Leaves the extended mode alone, so if we disable then re-enable mouse mode +// without toggling an extended mode, the mode will persist. +// Parameters: +// - fEnable - either enable or disable. +// Return value: +// +void MouseInput::EnableButtonEventTracking(const bool fEnable) +{ + _TrackingMode = fEnable ? TrackingMode::ButtonEvent : TrackingMode::None; + _coordLastPos = {-1,-1}; // Clear out the last saved mouse position & button. + _lastButton = 0; +} + +// Routine Description: +// - Either enables or disables AnyEvent mouse handling. Any Event mode sends sequences +// for any and every mouse event, regardless if a button is pressed or not. +// Leaves the extended mode alone, so if we disable then re-enable mouse mode +// without toggling an extended mode, the mode will persist. +// Parameters: +// - fEnable - either enable or disable. +// Return value: +// +void MouseInput::EnableAnyEventTracking(const bool fEnable) +{ + _TrackingMode = fEnable ? TrackingMode::AnyEvent : TrackingMode::None; + _coordLastPos = {-1,-1}; // Clear out the last saved mouse position & button. + _lastButton = 0; +} + +// Routine Description: +// - Sends the given sequence into the input callback specified by _pfnWriteEvents. +// Typically, this inserts the characters into the input buffer as KeyDown KEY_EVENTs. +// Parameters: +// - pwszSequence - sequence to send to _pfnWriteEvents +// - cchLength - the length of pwszSequence +// Return value: +// +void MouseInput::_SendInputSequence(_In_reads_(cchLength) const wchar_t* const pwszSequence, + const size_t cchLength) const +{ + size_t cch = 0; + // + 1 to max sequence length for null terminator count which is required by StringCchLengthW + if (SUCCEEDED(StringCchLengthW(pwszSequence, cchLength + 1, &cch)) && cch > 0 && cch < DWORD_MAX) + { + std::deque> events; + try + { + for (size_t i = 0; i < cch; ++i) + { + events.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, pwszSequence[i], 0)); + } + + _pfnWriteEvents(events); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } +} + +// Routine Description: +// - Translates the given coord from windows coordinate space (origin=0,0) to VT space (origin=1,1) +// Parameters: +// - coordWinCoordinate - the coordinate to translate +// Return value: +// - the translated coordinate. +COORD MouseInput::s_WinToVTCoord(const COORD coordWinCoordinate) +{ + return {coordWinCoordinate.X + 1, coordWinCoordinate.Y + 1}; +} + +// Routine Description: +// - Encodes the given value as a default (or utf-8) encoding value. +// 32 is added so that the value 0 can be emitted as the printable characher ' '. +// Parameters: +// - sCoordinateValue - the value to encode. +// Return value: +// - the encoded value. +short MouseInput::s_EncodeDefaultCoordinate(const short sCoordinateValue) +{ + return sCoordinateValue + 32; +} + +// Routine Description: +// - Retrieves which mouse button is currently pressed. This is needed because +// MOUSEMOVE events do not also tell us if any mouse buttons are pressed during the move. +// Parameters: +// +// Return value: +// - a uiButton corresponding to any pressed mouse buttons, else WM_LBUTTONUP if none are pressed. +unsigned int MouseInput::s_GetPressedButton() +{ + unsigned int uiButton = WM_LBUTTONUP; // Will be treated as a release, or no button pressed. + if (WI_IsFlagSet(GetKeyState(VK_LBUTTON), KEY_PRESSED)) + { + uiButton = WM_LBUTTONDOWN; + } + else if (WI_IsFlagSet(GetKeyState(VK_MBUTTON), KEY_PRESSED)) + { + uiButton = WM_MBUTTONDOWN; + } + else if (WI_IsFlagSet(GetKeyState(VK_RBUTTON), KEY_PRESSED)) + { + uiButton = WM_RBUTTONDOWN; + } + return uiButton; +} + + +// Routine Description: +// - Enables alternate scroll mode. This sends Cursor Up/down sequences when in the alternate buffer +// Parameters: +// - fEnable - either enable or disable. +// Return value: +// +void MouseInput::EnableAlternateScroll(const bool fEnable) +{ + _fAlternateScroll = fEnable; +} + +// Routine Description: +// - Notify the MouseInput handler that the screen buffer has been swapped to the alternate buffer +// Parameters: +// +// Return value: +// +void MouseInput::UseAlternateScreenBuffer() +{ + _fInAlternateBuffer = true; +} + +// Routine Description: +// - Notify the MouseInput handler that the screen buffer has been swapped to the alternate buffer +// Parameters: +// +// Return value: +// +void MouseInput::UseMainScreenBuffer() +{ + _fInAlternateBuffer = false; +} + +// Routine Description: +// - Returns true if we should translate the input event (uiButton, sScrollDelta) +// into an alternate scroll event instead of the default scroll event, +// dependiong on if alternate scroll mode is enabled and we're in the alternate buffer. +// Parameters: +// - uiButton: The mouse event code of the input event +// - sScrollDelta: The scroll wheel delta of the input event +// Return value: +// True iff the alternate buffer is active and alternate scroll mode is enabled and the event is a mouse wheel event. +bool MouseInput::_ShouldSendAlternateScroll(_In_ unsigned int uiButton, _In_ short sScrollDelta) const +{ + return _fInAlternateBuffer && + _fAlternateScroll && + (uiButton == WM_MOUSEWHEEL || uiButton == WM_MOUSEHWHEEL) + && sScrollDelta != 0; +} + +// Routine Description: +// - Sends a sequence to the input coresponding to cursor up / down depending on the sScrollDelta. +// Parameters: +// - sScrollDelta: The scroll wheel delta of the input event +// Return value: +// True iff the input sequence was sent successfully. +bool MouseInput::_SendAlternateScroll(_In_ short sScrollDelta) const +{ + const wchar_t* const pwchSequence = sScrollDelta > 0? CURSOR_UP_SEQUENCE : CURSOR_DOWN_SEQUENCE; + _SendInputSequence(pwchSequence, CCH_CURSOR_SEQUENCES); + + return true; +} diff --git a/src/terminal/adapter/MouseInput.hpp b/src/terminal/adapter/MouseInput.hpp new file mode 100644 index 000000000..3b9df609e --- /dev/null +++ b/src/terminal/adapter/MouseInput.hpp @@ -0,0 +1,122 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- MouseInput.hpp + +Abstract: +- This serves as an adapter between mouse input from a user and the virtual terminal sequences that are + typically emitted by an xterm-compatible console. + +Author(s): +- Mike Griese (migrie) 01-Aug-2016 +--*/ +#pragma once + +#include "../../types/inc/IInputEvent.hpp" + +#include +#include + +namespace Microsoft::Console::VirtualTerminal +{ + typedef void(*WriteInputEvents)(_Inout_ std::deque>& events); + + class MouseInput sealed + { + public: + MouseInput(const WriteInputEvents pfnWriteEvents); + ~MouseInput(); + + bool HandleMouse(const COORD coordMousePosition, + const unsigned int uiButton, + const short sModifierKeystate, + const short sWheelDelta); + + void SetUtf8ExtendedMode(const bool fEnable); + void SetSGRExtendedMode(const bool fEnable); + + void EnableDefaultTracking(const bool fEnable); + void EnableButtonEventTracking(const bool fEnable); + void EnableAnyEventTracking(const bool fEnable); + + void EnableAlternateScroll(const bool fEnable); + void UseAlternateScreenBuffer(); + void UseMainScreenBuffer(); + + enum class ExtendedMode : unsigned int + { + None, + Utf8, + Sgr, + Urxvt + }; + + enum class TrackingMode : unsigned int + { + None, + Default, + ButtonEvent, + AnyEvent + }; + + private: + static const int s_MaxDefaultCoordinate = 94; + + WriteInputEvents _pfnWriteEvents; + + ExtendedMode _ExtendedMode = ExtendedMode::None; + TrackingMode _TrackingMode = TrackingMode::None; + + bool _fAlternateScroll = false; + bool _fInAlternateBuffer = false; + + COORD _coordLastPos; + unsigned int _lastButton; + + void _SendInputSequence(_In_reads_(cchLength) const wchar_t* const pwszSequence, const size_t cchLength) const; + bool _GenerateDefaultSequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const; + bool _GenerateUtf8Sequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const; + bool _GenerateSGRSequence(const COORD coordMousePosition, + const unsigned int uiButton, + const bool isDown, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta, + _Outptr_result_buffer_(*pcchLength) wchar_t** const ppwchSequence, + _Out_ size_t* const pcchLength) const; + + bool _ShouldSendAlternateScroll(_In_ unsigned int uiButton, _In_ short sScrollDelta) const; + bool _SendAlternateScroll(_In_ short sScrollDelta) const; + + static int s_WindowsButtonToXEncoding(const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta); + + static int s_WindowsButtonToSGREncoding(const unsigned int uiButton, + const bool fIsHover, + const short sModifierKeystate, + const short sWheelDelta); + + static bool s_IsButtonDown(const unsigned int uiButton); + static bool s_IsButtonMsg(const unsigned int uiButton); + static bool s_IsHoverMsg(const unsigned int uiButton); + static COORD s_WinToVTCoord(const COORD coordWinCoordinate); + static short s_EncodeDefaultCoordinate(const short sCoordinateValue); + static unsigned int s_GetPressedButton(); + }; +} diff --git a/src/terminal/adapter/adaptDefaults.hpp b/src/terminal/adapter/adaptDefaults.hpp new file mode 100644 index 000000000..a97ae829d --- /dev/null +++ b/src/terminal/adapter/adaptDefaults.hpp @@ -0,0 +1,29 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- adaptDefaults.hpp + +Abstract: +- This serves as an abstraction for the default cases in the state machine (which is to just print or execute a simple single character. +- This can also handle processing of an entire string of printable characters, as an optimization. +- When using the Windows Console API adapter (AdaptDispatch), this must be passed in to signify where standard actions should go. + +Author(s): +- Michael Niksa (MiNiksa) 30-July-2015 +- Mike Griese (migrie) 07-March-2016 +--*/ +#pragma once + +namespace Microsoft::Console::VirtualTerminal +{ + class AdaptDefaults + { + public: + virtual void Print(const wchar_t wch) = 0; + // These characters need to be mutable so that they can be processed by the TerminalInput translater. + virtual void PrintString(const wchar_t* const rgwch, const size_t cch) = 0; + virtual void Execute(const wchar_t wch) = 0; + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp new file mode 100644 index 000000000..a0f3356bc --- /dev/null +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -0,0 +1,1895 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "adaptDispatch.hpp" +#include "conGetSet.hpp" +#include "../../types/inc/Viewport.hpp" + +// Inspired from RETURN_IF_WIN32_BOOL_FALSE +// WIL doesn't include a RETURN_IF_FALSE, and RETURN_IF_WIN32_BOOL_FALSE +// will actually return the value of GLE. +#define RETURN_IF_FALSE(b) do { BOOL __boolRet = wil::verify_bool(b); if (!__boolRet) { return b; }} while (0, 0) + +using namespace Microsoft::Console::Types; +using namespace Microsoft::Console::VirtualTerminal; + +// Routine Description: +// - No Operation helper. It's just here to make sure they're always all the same. +// Arguments: +// - +// Return Value: +// - Always false to signify we didn't handle it. +bool NoOp() { return false; } + +// Note: AdaptDispatch will take ownership of pConApi and pDefaults +AdaptDispatch::AdaptDispatch(ConGetSet* const pConApi, + AdaptDefaults* const pDefaults) + : _conApi{ THROW_IF_NULL_ALLOC(pConApi) }, + _pDefaults{ THROW_IF_NULL_ALLOC(pDefaults) }, + _fChangedBackground(false), + _fChangedForeground(false), + _fChangedMetaAttrs(false), + _TermOutput() +{ + // The top-left corner in VT-speak is 1,1. Our internal array uses 0 indexes, but VT uses 1,1 for top left corner. + _coordSavedCursor.X = 1; + _coordSavedCursor.Y = 1; + _srScrollMargins = {0}; // initially, there are no scroll margins. + _fIsSetColumnsEnabled = false; // by default, DECSCPP is disabled. + // TODO:10086990 - Create a setting to re-enable this. + +} + +void AdaptDispatch::Print(const wchar_t wchPrintable) +{ + _pDefaults->Print(_TermOutput.TranslateKey(wchPrintable)); +} + +void AdaptDispatch::PrintString(const wchar_t* const rgwch, const size_t cch) +{ + try + { + if (_TermOutput.NeedToTranslate()) + { + std::unique_ptr tempArray = std::make_unique(cch); + for (size_t i = 0; i < cch; i++) + { + tempArray[i] = _TermOutput.TranslateKey(rgwch[i]); + } + _pDefaults->PrintString(tempArray.get(), cch); + } + else + { + _pDefaults->PrintString(rgwch, cch); + } + + } + CATCH_LOG(); +} + +// Routine Description: +// - Generalizes cursor movement for up/down/left/right and next/previous line. +// Arguments: +// - dir - Specific direction to move +// - uiDistance - Magnitude of the move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_CursorMovement(const CursorDirection dir, _In_ unsigned int const uiDistance) const +{ + // First retrieve some information about the buffer + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + COORD coordCursor = csbiex.dwCursorPosition; + + // For next/previous line, we unconditionally need to move the X position to the left edge of the viewport. + switch (dir) + { + case CursorDirection::NextLine: + case CursorDirection::PrevLine: + coordCursor.X = csbiex.srWindow.Left; + break; + } + + // Safely convert the UINT magnitude of the move we were given into a short (which is the size the console deals with) + SHORT sDelta = 0; + fSuccess = SUCCEEDED(UIntToShort(uiDistance, &sDelta)); + + if (fSuccess) + { + // Prepare our variables for math. All operations are some variation on these two parameters + SHORT* pcoordVal = nullptr; // The coordinate X or Y gets modified + SHORT sBoundaryVal = 0; // There is a particular edge of the viewport that is our boundary condition as we approach it. + + // Up and Down modify the Y coordinate. Left and Right modify the X. + switch (dir) + { + case CursorDirection::Up: + case CursorDirection::Down: + case CursorDirection::NextLine: + case CursorDirection::PrevLine: + pcoordVal = &coordCursor.Y; + break; + case CursorDirection::Left: + case CursorDirection::Right: + pcoordVal = &coordCursor.X; + break; + default: + fSuccess = false; + break; + } + + // Moving upward is bounded by top, etc. + switch (dir) + { + case CursorDirection::Up: + case CursorDirection::PrevLine: + sBoundaryVal = csbiex.srWindow.Top; + break; + case CursorDirection::Down: + case CursorDirection::NextLine: + sBoundaryVal = csbiex.srWindow.Bottom; + break; + case CursorDirection::Left: + sBoundaryVal = csbiex.srWindow.Left; + break; + case CursorDirection::Right: + sBoundaryVal = csbiex.srWindow.Right; + break; + default: + fSuccess = false; + break; + } + + if (fSuccess) + { + // For up and left, we need to subtract the magnitude of the vector to get the new spot. Right/down = add. + // Use safe short subtraction to prevent under/overflow. + switch (dir) + { + case CursorDirection::Up: + case CursorDirection::Left: + case CursorDirection::PrevLine: + fSuccess = SUCCEEDED(ShortSub(*pcoordVal, sDelta, pcoordVal)); + break; + case CursorDirection::Down: + case CursorDirection::Right: + case CursorDirection::NextLine: + fSuccess = SUCCEEDED(ShortAdd(*pcoordVal, sDelta, pcoordVal)); + break; + } + + if (fSuccess) + { + // Now apply the boundary condition. Up, Left can't be smaller than their boundary. Top, Right can't be larger. + switch (dir) + { + case CursorDirection::Up: + case CursorDirection::Left: + case CursorDirection::PrevLine: + *pcoordVal = std::max(*pcoordVal, sBoundaryVal); + break; + case CursorDirection::Down: + case CursorDirection::Right: + case CursorDirection::NextLine: + // For the bottom and right edges, the viewport value is stated to be one outside the rectangle. + *pcoordVal = std::min(*pcoordVal, gsl::narrow(sBoundaryVal - 1)); + break; + default: + fSuccess = false; + break; + } + + if (fSuccess) + { + // Finally, attempt to set the adjusted cursor position back into the console. + fSuccess = !!_conApi->SetConsoleCursorPosition(coordCursor); + } + } + } + } + } + + return fSuccess; +} + +// Routine Description: +// - CUU - Handles cursor upward movement by given distance. +// CUU and CUD are handled seperately from other CUP sequences, because they are +// constrained by the margins. +// See: https://vt100.net/docs/vt510-rm/CUU.html +// "The cursor stops at the top margin. If the cursor is already above the top +// margin, then the cursor stops at the top line." +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorUp(_In_ unsigned int const uiDistance) +{ + SHORT sDelta = 0; + if (SUCCEEDED(UIntToShort(uiDistance, &sDelta))) + { + return !!_conApi->MoveCursorVertically(-sDelta); + } + return false; +} + +// Routine Description: +// - CUD - Handles cursor downward movement by given distance +// CUU and CUD are handled seperately from other CUP sequences, because they are +// constrained by the margins. +// See: https://vt100.net/docs/vt510-rm/CUD.html +// "The cursor stops at the bottom margin. If the cursor is already above the +// bottom margin, then the cursor stops at the bottom line." +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorDown(_In_ unsigned int const uiDistance) +{ + SHORT sDelta = 0; + if (SUCCEEDED(UIntToShort(uiDistance, &sDelta))) + { + return !!_conApi->MoveCursorVertically(sDelta); + } + return false; +} + +// Routine Description: +// - CUF - Handles cursor forward movement by given distance +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorForward(_In_ unsigned int const uiDistance) +{ + return _CursorMovement(CursorDirection::Right, uiDistance); +} + +// Routine Description: +// - CUB - Handles cursor backward movement by given distance +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorBackward(_In_ unsigned int const uiDistance) +{ + return _CursorMovement(CursorDirection::Left, uiDistance); +} + +// Routine Description: +// - CNL - Handles cursor movement to the following line (or N lines down) +// - Moves to the beginning X/Column position of the line. +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorNextLine(_In_ unsigned int const uiDistance) +{ + return _CursorMovement(CursorDirection::NextLine, uiDistance); +} + +// Routine Description: +// - CPL - Handles cursor movement to the previous line (or N lines up) +// - Moves to the beginning X/Column position of the line. +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorPrevLine(_In_ unsigned int const uiDistance) +{ + return _CursorMovement(CursorDirection::PrevLine, uiDistance); +} + +// Routine Description: +// - Generalizes cursor movement to a specific coordinate position +// - If a parameter is left blank, we will maintain the existing position in that dimension. +// Arguments: +// - puiRow - Optional row to move to +// - puiColumn - Optional column to move to +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_CursorMovePosition(_In_opt_ const unsigned int* const puiRow, _In_opt_ const unsigned int* const puiCol) const +{ + bool fSuccess = true; + + // First retrieve some information about the buffer + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + // handle optional parameters. If not specified, keep same cursor position from what we just loaded. + unsigned int uiRow = 0; + unsigned int uiCol = 0; + + if (puiRow != nullptr) + { + if (*puiRow != 0) + { + uiRow = *puiRow - 1; // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. + } + else + { + fSuccess = false; // The parser should never return 0 (0 maps to 1), so this is a failure condition. + } + } + else + { + uiRow = csbiex.dwCursorPosition.Y - csbiex.srWindow.Top; // remember, in VT speak, this is relative to the viewport. not absolute. + } + + if (puiCol != nullptr) + { + if (*puiCol != 0) + { + uiCol = *puiCol - 1; // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. + } + else + { + fSuccess = false; // The parser should never return 0 (0 maps to 1), so this is a failure condition. + } + } + else + { + uiCol = csbiex.dwCursorPosition.X - csbiex.srWindow.Left; // remember, in VT speak, this is relative to the viewport. not absolute. + } + + if (fSuccess) + { + COORD coordCursor = csbiex.dwCursorPosition; + + // Safely convert the UINT positions we were given into shorts (which is the size the console deals with) + fSuccess = SUCCEEDED(UIntToShort(uiRow, &coordCursor.Y)) && SUCCEEDED(UIntToShort(uiCol, &coordCursor.X)); + + if (fSuccess) + { + // Set the line and column values as offsets from the viewport edge. Use safe math to prevent overflow. + fSuccess = SUCCEEDED(ShortAdd(coordCursor.Y, csbiex.srWindow.Top, &coordCursor.Y)) && + SUCCEEDED(ShortAdd(coordCursor.X, csbiex.srWindow.Left, &coordCursor.X)); + + if (fSuccess) + { + // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. + coordCursor.Y = std::clamp(coordCursor.Y, csbiex.srWindow.Top, gsl::narrow(csbiex.srWindow.Bottom - 1)); + coordCursor.X = std::clamp(coordCursor.X, csbiex.srWindow.Left, gsl::narrow(csbiex.srWindow.Right - 1)); + + // Finally, attempt to set the adjusted cursor position back into the console. + fSuccess = !!_conApi->SetConsoleCursorPosition(coordCursor); + } + } + } + } + + return fSuccess; +} + +// Routine Description: +// - CHA - Moves the cursor to an exact X/Column position on the current line. +// Arguments: +// - uiColumn - Specific X/Column position to move to +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorHorizontalPositionAbsolute(_In_ unsigned int const uiColumn) +{ + return _CursorMovePosition(nullptr, &uiColumn); +} + +// Routine Description: +// - VPA - Moves the cursor to an exact Y/row position on the current column. +// Arguments: +// - uiLine - Specific Y/Row position to move to +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::VerticalLinePositionAbsolute(_In_ unsigned int const uiLine) +{ + return _CursorMovePosition(&uiLine, nullptr); +} + +// Routine Description: +// - CUP - Moves the cursor to an exact X/Column and Y/Row/Line coordinate position. +// Arguments: +// - uiLine - Specific Y/Row/Line position to move to +// - uiColumn - Specific X/Column position to move to +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorPosition(_In_ unsigned int const uiLine, _In_ unsigned int const uiColumn) +{ + return _CursorMovePosition(&uiLine, &uiColumn); +} + +// Routine Description: +// - DECSC - Saves the current cursor position into a memory buffer. +// Arguments: +// - +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorSavePosition() +{ + // First retrieve some information about the buffer + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + // The cursor is given to us by the API as relative to the whole buffer. + // But in VT speak, the cursor should be relative to the current viewport. Adjust. + COORD const coordCursor = csbiex.dwCursorPosition; + + SMALL_RECT const srViewport = csbiex.srWindow; + + // VT is also 1 based, not 0 based, so correct by 1. + _coordSavedCursor.X = coordCursor.X - srViewport.Left + 1; + _coordSavedCursor.Y = coordCursor.Y - srViewport.Top + 1; + } + + return fSuccess; +} + +// Routine Description: +// - DECRC - Restores a saved cursor position from the DECSC command back into the console state. +// - If no position was set, this defaults to the top left corner (see AdaptDispatch constructor.) +// Arguments: +// - +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorRestorePosition() +{ + unsigned int const uiRow = _coordSavedCursor.Y; + unsigned int const uiCol = _coordSavedCursor.X; + + return _CursorMovePosition(&uiRow, &uiCol); +} + +// Routine Description: +// - DECTCEM - Sets the show/hide visibility status of the cursor. +// Arguments: +// - fIsVisible - Turns the cursor rendering on (TRUE) or off (FALSE). +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::CursorVisibility(const bool fIsVisible) +{ + // This uses a private API instead of the public one, because the public API + // will set the cursor shape back to legacy. + return !!_conApi->PrivateShowCursor(fIsVisible); +} + +// Routine Description: +// - This helper will do the work of performing an insert or delete character operation +// - Both operations are similar in that they cut text and move it left or right in the buffer, padding the leftover area with spaces. +// Arguments: +// - uiCount - The number of characters to insert +// - fIsInsert - TRUE if insert mode (cut and paste to the right, away from the cursor). FALSE if delete mode (cut and paste to the left, toward the cursor) +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_InsertDeleteHelper(_In_ unsigned int const uiCount, const bool fIsInsert) const +{ + // We'll be doing short math on the distance since all console APIs use shorts. So check that we can successfully convert the uint into a short first. + SHORT sDistance; + RETURN_IF_FALSE(SUCCEEDED(UIntToShort(uiCount, &sDistance))); + + // get current cursor, viewport + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + RETURN_IF_FALSE(_conApi->MoveToBottom()); + RETURN_IF_FALSE(_conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + const auto cursor = csbiex.dwCursorPosition; + const auto viewport = Viewport::FromExclusive(csbiex.srWindow); + // Rectangle to cut out of the existing buffer + SMALL_RECT srScroll; + srScroll.Left = cursor.X; + srScroll.Right = viewport.RightExclusive(); + srScroll.Top = cursor.Y; + srScroll.Bottom = srScroll.Top; + + // Paste coordinate for cut text above + COORD coordDestination; + coordDestination.Y = cursor.Y; + coordDestination.X = cursor.X; + + // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) + CHAR_INFO ciFill; + ciFill.Attributes = csbiex.wAttributes; + ciFill.Char.UnicodeChar = L' '; + + bool fSuccess = false; + if (fIsInsert) + { + // Insert makes space by moving characters out to the right. So move the destination of the cut/paste region. + fSuccess = SUCCEEDED(ShortAdd(coordDestination.X, sDistance, &coordDestination.X)); + } + else + { + // for delete, we need to add to the scroll region to move it off toward the right. + fSuccess = SUCCEEDED(ShortAdd(srScroll.Left, sDistance, &srScroll.Left)); + } + + if (fSuccess) + { + if (srScroll.Left >= viewport.RightExclusive() || + coordDestination.X >= viewport.RightExclusive()) + { + DWORD const nLength = viewport.RightExclusive() - cursor.X; + size_t written = 0; + + // if the select/scroll region is off screen to the right or the destination is off screen to the right, fill instead of scrolling. + fSuccess = !!_conApi->FillConsoleOutputCharacterW(ciFill.Char.UnicodeChar, + nLength, + cursor, + written); + + if (fSuccess) + { + written = 0; + fSuccess = !!_conApi->FillConsoleOutputAttribute(ciFill.Attributes, + nLength, + cursor, + written); + } + } + else + { + // clip inside the viewport. + fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, + &csbiex.srWindow, + coordDestination, + &ciFill); + + if (fSuccess && !fIsInsert) + { + // See MSFT:19888564 + // We've now shifted a number of the characters to the left. + // If the number of chars we've shifted doesn't fill the + // entire region we deleted, then artifacts of the + // previous contents of the row can get left behind. + // + // Example: (this is tested by DeleteCharsNearEndOfLineSimpleFirstCase) + // start with the following buffer contents, and the cursor on the "D" + // [ABCDEFG ] + // ^ + // When you DCH(3) here, we are trying to delete the D, E and F. + // We do that by shifting the contents of the line after the deleted + // characters to the left. HOWEVER, there are only 2 chars left to move. + // So (before the fix) the buffer end up like this: + // [ABCG F ] + // ^ + // The G and " " have moved, but the F did not get overwritten. + // + // Fill the remaining space after the characters we + // shifted with spaces (empty cells). + const short scrolledChars = viewport.RightExclusive() - srScroll.Left; + const short shiftedRightPos = cursor.X + scrolledChars; + if (shiftedRightPos < srScroll.Left) + { + size_t written = 0; + const short spacesToFill = viewport.RightInclusive() - (shiftedRightPos); + const COORD fillPos{ shiftedRightPos, cursor.Y }; + fSuccess = !!_conApi->FillConsoleOutputCharacterW(ciFill.Char.UnicodeChar, + spacesToFill, + fillPos, + written); + if (fSuccess) + { + written = 0; + fSuccess = !!_conApi->FillConsoleOutputAttribute(ciFill.Attributes, + spacesToFill, + fillPos, + written); + } + } + } + } + } + + + return fSuccess; +} + +// Routine Description: +// ICH - Insert Character - Blank/default attribute characters will be inserted at the current cursor position. +// - Each inserted character will push all text in the row to the right. +// Arguments: +// - uiCount - The number of characters to insert +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::InsertCharacter(_In_ unsigned int const uiCount) +{ + return _InsertDeleteHelper(uiCount, true); +} + +// Routine Description: +// DCH - Delete Character - The character at the cursor position will be deleted. Blank/attribute characters will +// be inserted from the right edge of the current line. +// Arguments: +// - uiCount - The number of characters to delete +// Return Value: +// - True if handled successfuly. False otherwise. +bool AdaptDispatch::DeleteCharacter(_In_ unsigned int const uiCount) +{ + return _InsertDeleteHelper(uiCount, false); +} +// Routine Description: +// - Internal helper to erase a specific number of characters in one particular line of the buffer. +// Erased positions are replaced with spaces. +// Arguments: +// - coordStartPosition - The position to begin erasing at. +// - dwLength - the number of characters to erase. +// - wFillColor - The attributes to apply to the erased positions. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const +{ + WCHAR const wchSpace = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. + + size_t written = 0; + bool fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, dwLength, coordStartPosition, written); + + if (fSuccess) + { + fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, dwLength, coordStartPosition, written); + } + + return fSuccess; +} + +bool AdaptDispatch::_EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor) +{ + WCHAR const wchSpace = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. + + size_t written = 0; + FAIL_FAST_IF(!(coordStartPosition.X < coordLastPosition.X)); + FAIL_FAST_IF(!(coordStartPosition.Y < coordLastPosition.Y)); + bool fSuccess = false; + for (short y = coordStartPosition.Y; y < coordLastPosition.Y; y++) + { + const COORD coordLine = {coordStartPosition.X, y}; + fSuccess = !!_conApi->FillConsoleOutputCharacterW(wchSpace, coordLastPosition.X - coordStartPosition.X, coordLine, written); + if (fSuccess) + { + fSuccess = !!_conApi->FillConsoleOutputAttribute(wFillColor, coordLastPosition.X - coordStartPosition.X, coordLine, written); + } + + if (!fSuccess) + { + break; + } + } + return fSuccess; +} + +// Routine Description: +// - Internal helper to erase one particular line of the buffer. Either from beginning to the cursor, from the cursor to the end, or the entire line. +// - Used by both erase line (used just once) and by erase screen (used in a loop) to erase a portion of the buffer. +// Arguments: +// - pcsbiex - Pointer to the console screen buffer that we will be erasing (and getting cursor data from within) +// - DispatchTypes::EraseType - Enumeration mode of which kind of erase to perform: beginning to cursor, cursor to end, or entire line. +// - sLineId - The line number (array index value, starts at 0) of the line to operate on within the buffer. +// - This is not aware of circular buffer. Line 0 is always the top visible line if you scrolled the whole way up the window. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const +{ + COORD coordStartPosition = { 0 }; + coordStartPosition.Y = sLineId; + + // determine start position from the erase type + // remember that erases are inclusive of the current cursor position. + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + case DispatchTypes::EraseType::All: + coordStartPosition.X = pcsbiex->srWindow.Left; // from beginning and the whole line start from the left viewport edge. + break; + case DispatchTypes::EraseType::ToEnd: + coordStartPosition.X = pcsbiex->dwCursorPosition.X; // from the current cursor position (including it) + break; + } + + DWORD nLength = 0; + + // determine length of erase from erase type + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + // +1 because if cursor were at the left edge, the length would be 0 and we want to paint at least the 1 character the cursor is on. + nLength = (pcsbiex->dwCursorPosition.X - pcsbiex->srWindow.Left) + 1; + break; + case DispatchTypes::EraseType::ToEnd: + case DispatchTypes::EraseType::All: + // Remember the .Right value is 1 farther than the right most displayed character in the viewport. Therefore no +1. + nLength = pcsbiex->srWindow.Right - coordStartPosition.X; + break; + } + + return _EraseSingleLineDistanceHelper(coordStartPosition, nLength, wFillColor); + +} + +// Routine Description: +// - ECH - Erase Characters from the current cursor position, by replacing +// them with a space. This will only erase characters in the current line, +// and won't wrap to the next. The attributes of any erased positions +// recieve the currently selected attributes. +// Arguments: +// - uiNumChars - The number of characters to erase. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::EraseCharacters(_In_ unsigned int const uiNumChars) +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = !!_conApi->GetConsoleScreenBufferInfoEx(&csbiex); + + if (fSuccess) + { + const COORD coordStartPosition = csbiex.dwCursorPosition; + + const SHORT sRemainingSpaces = csbiex.srWindow.Right - coordStartPosition.X; + const unsigned short usActualRemaining = (sRemainingSpaces < 0)? 0 : sRemainingSpaces; + // erase at max the number of characters remaining in the line from the current position. + const DWORD dwEraseLength = (uiNumChars <= usActualRemaining)? uiNumChars : usActualRemaining; + + fSuccess = _EraseSingleLineDistanceHelper(coordStartPosition, dwEraseLength, csbiex.wAttributes); + } + return fSuccess; +} + +// Routine Description: +// - ED - Erases a portion of the current viewable area (viewport) of the console. +// Arguments: +// - DispatchTypes::EraseType - Determines whether to erase: +// From beginning (top-left corner) to the cursor +// From cursor to end (bottom-right corner) +// The entire viewport area +// The scrollback (outside the viewport area) +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) +{ + // First things first. If this is a "Scrollback" clear, then just do that. + // Scrollback clears erase everything in the "scrollback" of a *nix terminal + // Everything that's scrolled off the screen so far. + // Or if it's an Erase All, then we also need to handle that specially + // by moving the current contents of the viewport into the scrollback. + if (eraseType == DispatchTypes::EraseType::Scrollback) + { + return _EraseScrollback(); + } + else if (eraseType == DispatchTypes::EraseType::All) + { + return _EraseAll(); + } + + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + // What we need to erase is grouped into 3 types: + // 1. Lines before cursor + // 2. Cursor Line + // 3. Lines after cursor + // We erase one or more of these based on the erase type: + // A. FromBeginning - Erase 1 and Some of 2. + // B. ToEnd - Erase some of 2 and 3. + // C. All - Erase 1, 2, and 3. + + // 1. Lines before cursor line + if (eraseType == DispatchTypes::EraseType::FromBeginning) + { + // For beginning and all, erase all complete lines before (above vertically) from the cursor position. + for (SHORT sStartLine = csbiex.srWindow.Top; sStartLine < csbiex.dwCursorPosition.Y; sStartLine++) + { + fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes); + + if (!fSuccess) + { + break; + } + } + } + + if (fSuccess) + { + // 2. Cursor Line + fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes); + } + + if (fSuccess) + { + // 3. Lines after cursor line + if (eraseType == DispatchTypes::EraseType::ToEnd) + { + // For beginning and all, erase all complete lines after (below vertically) the cursor position. + // Remember that the viewport bottom value is 1 beyond the viewable area of the viewport. + for (SHORT sStartLine = csbiex.dwCursorPosition.Y + 1; sStartLine < csbiex.srWindow.Bottom; sStartLine++) + { + fSuccess = _EraseSingleLineHelper(&csbiex, DispatchTypes::EraseType::All, sStartLine, csbiex.wAttributes); + + if (!fSuccess) + { + break; + } + } + } + } + } + + return fSuccess; +} + +// Routine Description: +// - EL - Erases the line that the cursor is currently on. +// Arguments: +// - DispatchTypes::EraseType - Determines whether to erase: From beginning (left edge) to the cursor, from cursor to end (right edge), or the entire line. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = !!_conApi->GetConsoleScreenBufferInfoEx(&csbiex); + + if (fSuccess) + { + fSuccess = _EraseSingleLineHelper(&csbiex, eraseType, csbiex.dwCursorPosition.Y, csbiex.wAttributes); + } + + return fSuccess; +} + + +// Routine Description: +// - DSR - Reports status of a console property back to the STDIN based on the type of status requested. +// - This particular routine responds to ANSI status patterns only (CSI # n), not the DEC format (CSI ? # n) +// Arguments: +// - statusType - ANSI status type indicating what property we should report back +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) +{ + bool fSuccess = false; + + switch (statusType) + { + case DispatchTypes::AnsiStatusType::CPR_CursorPositionReport: + fSuccess = _CursorPositionReport(); + break; + } + + return fSuccess; +} + +// Routine Description: +// - DA - Reports the identity of this Virtual Terminal machine to the caller. +// - In our case, we'll report back to acknowledge we understand, but reveal no "hardware" upgrades like physical terminals of old. +// Arguments: +// - +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::DeviceAttributes() +{ + // See: http://vt100.net/docs/vt100-ug/chapter3.html#DA + wchar_t* const pwszResponse = L"\x1b[?1;0c"; + + return _WriteResponse(pwszResponse, wcslen(pwszResponse)); +} + +// Routine Description: +// - DSR-CPR - Reports the current cursor position within the viewport back to the input channel +// Arguments: +// - +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_CursorPositionReport() const +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + // First pull the cursor position relative to the entire buffer out of the console. + COORD coordCursorPos = csbiex.dwCursorPosition; + + // Now adjust it for its position in respect to the current viewport. + coordCursorPos.X -= csbiex.srWindow.Left; + coordCursorPos.Y -= csbiex.srWindow.Top; + + // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + coordCursorPos.X++; + coordCursorPos.Y++; + + // Now send it back into the input channel of the console. + // First format the response string. + wchar_t pwszResponseBuffer[50]; + swprintf_s(pwszResponseBuffer, ARRAYSIZE(pwszResponseBuffer), L"\x1b[%d;%dR", coordCursorPos.Y, coordCursorPos.X); + + size_t const cBuffer = wcslen(pwszResponseBuffer); + + fSuccess = _WriteResponse(pwszResponseBuffer, cBuffer); + } + + return fSuccess; +} + +// Routine Description: +// - Helper to send a string reply to the input stream of the console. +// - Used by various commands where the program attached would like a reply to one of the commands issued. +// - This will generate two "key presses" (one down, one up) for every character in the string and place them into the head of the console's input stream. +// Arguments: +// - pwszReply - The reply string to transmit back to the input stream +// - cReply - The length of the string. +// Return Value: +// - True if the string was converted to input events and placed into the console input buffer successfuly. False otherwise. +bool AdaptDispatch::_WriteResponse(_In_reads_(cchReply) PCWSTR pwszReply, const size_t cchReply) const +{ + bool fSuccess = false; + std::deque> inEvents; + try + { + // generate a paired key down and key up event for every + // character to be sent into the console's input buffer + for (size_t i = 0; i < cchReply; ++i) + { + // This wasn't from a real keyboard, so we're leaving key/scan codes blank. + KeyEvent keyEvent{ TRUE, 1, 0, 0, pwszReply[i], 0 }; + + inEvents.push_back(std::make_unique(keyEvent)); + keyEvent.SetKeyDown(false); + inEvents.push_back(std::make_unique(keyEvent)); + } + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + return false; + } + + size_t eventsWritten; + fSuccess = !!_conApi->PrivatePrependConsoleInput(inEvents, eventsWritten); + + return fSuccess; +} + +// Routine Description: +// - Generalizes scrolling movement for up/down +// Arguments: +// - sdDirection - Specific direction to move +// - uiDistance - Magnitude of the move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_ScrollMovement(const ScrollDirection sdDirection, _In_ unsigned int const uiDistance) const +{ + // We'll be doing short math on the distance since all console APIs use shorts. So check that we can successfully convert the uint into a short first. + SHORT sDistance; + bool fSuccess = SUCCEEDED(UIntToShort(uiDistance, &sDistance)); + + if (fSuccess) + { + // get current cursor + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + if (fSuccess) + { + SMALL_RECT srScreen = csbiex.srWindow; + + // Paste coordinate for cut text above + COORD coordDestination; + coordDestination.X = srScreen.Left; + // Scroll starting from the top of the scroll margins. + coordDestination.Y = (_srScrollMargins.Top + srScreen.Top) + sDistance * (sdDirection == ScrollDirection::Up? -1 : 1); + // We don't need to worry about clipping the margins at all, ScrollRegion inside conhost will do that correctly for us + + // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) + CHAR_INFO ciFill; + ciFill.Attributes = csbiex.wAttributes; + ciFill.Char.UnicodeChar = L' '; + fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScreen, &srScreen, coordDestination, &ciFill); + } + } + + return fSuccess; +} + +// Routine Description: +// - SU - Pans the window DOWN by given distance (uiDistance new lines appear at the bottom of the screen) +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::ScrollUp(_In_ unsigned int const uiDistance) +{ + return _ScrollMovement(ScrollDirection::Up, uiDistance); +} + +// Routine Description: +// - SD - Pans the window UP by given distance (uiDistance new lines appear at the top of the screen) +// Arguments: +// - uiDistance - Distance to move +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::ScrollDown(_In_ unsigned int const uiDistance) +{ + return _ScrollMovement(ScrollDirection::Down, uiDistance); +} + +// Routine Description: +// - DECSCPP / DECCOLM Sets the number of columns "per page" AKA sets the console width. +// DECCOLM also clear the screen (like a CSI 2 J sequence), while DECSCPP just sets the width. +// (DECCOLM will do this seperately of this function) +// Arguments: +// - uiColumns - Number of columns +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetColumns(_In_ unsigned int const uiColumns) +{ + if (!_fIsSetColumnsEnabled) + { + // Only set columns if that option is available. Return true, as this is technically a successful handling. + return true; + } + + SHORT sColumns; + bool fSuccess = SUCCEEDED(UIntToShort(uiColumns, &sColumns)); + if (fSuccess) + { + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + fSuccess = !!_conApi->GetConsoleScreenBufferInfoEx(&csbiex); + + if (fSuccess) + { + csbiex.dwSize.X = sColumns; + fSuccess = !!_conApi->SetConsoleScreenBufferInfoEx(&csbiex); + } + } + return fSuccess; +} + +// Routine Description: +// - DECCOLM not only sets the number of columns, but also clears the screen buffer, resets the page margins, and places the cursor at 1,1 +// Arguments: +// - uiColumns - Number of columns +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_DoDECCOLMHelper(_In_ unsigned int const uiColumns) +{ + bool fSuccess = SetColumns(uiColumns); + if (fSuccess) + { + fSuccess = CursorPosition(1, 1); + if (fSuccess) + { + fSuccess = EraseInDisplay(DispatchTypes::EraseType::All); + if (fSuccess) + { + fSuccess = _DoSetTopBottomScrollingMargins(0, 0); + } + } + } + return fSuccess; +} + +bool AdaptDispatch::_PrivateModeParamsHelper(_In_ DispatchTypes::PrivateModeParams const param, const bool fEnable) +{ + bool fSuccess = false; + switch(param) + { + case DispatchTypes::PrivateModeParams::DECCKM_CursorKeysMode: + // set - Enable Application Mode, reset - Normal mode + fSuccess = SetCursorKeysMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::DECCOLM_SetNumberOfColumns: + fSuccess = _DoDECCOLMHelper(fEnable? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); + break; + case DispatchTypes::PrivateModeParams::ATT610_StartCursorBlink: + fSuccess = EnableCursorBlinking(fEnable); + break; + case DispatchTypes::PrivateModeParams::DECTCEM_TextCursorEnableMode: + fSuccess = CursorVisibility(fEnable); + break; + case DispatchTypes::PrivateModeParams::VT200_MOUSE_MODE: + fSuccess = EnableVT200MouseMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::BUTTTON_EVENT_MOUSE_MODE: + fSuccess = EnableButtonEventMouseMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::ANY_EVENT_MOUSE_MODE: + fSuccess = EnableAnyEventMouseMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::UTF8_EXTENDED_MODE: + fSuccess = EnableUTF8ExtendedMouseMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::SGR_EXTENDED_MODE: + fSuccess = EnableSGRExtendedMouseMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::ALTERNATE_SCROLL: + fSuccess = EnableAlternateScroll(fEnable); + break; + case DispatchTypes::PrivateModeParams::ASB_AlternateScreenBuffer: + fSuccess = fEnable? UseAlternateScreenBuffer() : UseMainScreenBuffer(); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + return fSuccess; +} + +// Routine Description: +// - Generalized handler for the setting/resetting of DECSET/DECRST parameters. +// All params in the rgParams will attempt to be executed, even if one +// fails, to allow us to successfully re/set params that are chained with +// params we don't yet support. +// Arguments: +// - rgParams - array of params to set/reset +// - cParams - length of rgParams +// Return Value: +// - True if ALL params were handled successfully. False otherwise. +bool AdaptDispatch::_SetResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, const size_t cParams, const bool fEnable) +{ + // because the user might chain together params we don't support with params we DO support, execute all + // params in the sequence, and only return failure if we failed at least one of them + size_t cFailures = 0; + for (size_t i = 0; i < cParams; i++) + { + cFailures += _PrivateModeParamsHelper(rgParams[i], fEnable)? 0 : 1; // increment the number of failures if we fail. + } + return cFailures == 0; +} + +// Routine Description: +// - DECSET - Enables the given DEC private mode params. +// Arguments: +// - rgParams - array of params to set +// - cParams - length of rgParams +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, const size_t cParams) +{ + return _SetResetPrivateModes(rgParams, cParams, true); +} + +// Routine Description: +// - DECRST - Disables the given DEC private mode params. +// Arguments: +// - rgParams - array of params to reset +// - cParams - length of rgParams +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::ResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, const size_t cParams) +{ + return _SetResetPrivateModes(rgParams, cParams, false); +} + +// - DECKPAM, DECKPNM - Sets the keypad input mode to either Application mode or Numeric mode (true, false respectively) +// Arguments: +// - fApplicationMode - set to true to enable Application Mode Input, false for Numeric Mode Input. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) +{ + return !!_conApi->PrivateSetKeypadMode(fApplicationMode); +} + +// - DECCKM - Sets the cursor keys input mode to either Application mode or Normal mode (true, false respectively) +// Arguments: +// - fApplicationMode - set to true to enable Application Mode Input, false for Normal Mode Input. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetCursorKeysMode(const bool fApplicationMode) +{ + return !!_conApi->PrivateSetCursorKeysMode(fApplicationMode); +} + +// - att610 - Enables or disables the cursor blinking. +// Arguments: +// - fEnable - set to true to enable blinking, false to disable +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::EnableCursorBlinking(const bool fEnable) +{ + return !!_conApi->PrivateAllowCursorBlinking(fEnable); +} + +// Routine Description: +// - IL - This control function inserts one or more blank lines, starting at the cursor. +// As lines are inserted, lines below the cursor and in the scrolling region move down. +// Lines scrolled off the page are lost. IL has no effect outside the page margins. +// Arguments: +// - uiDistance - number of lines to insert +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::InsertLine(_In_ unsigned int const uiDistance) +{ + return !!_conApi->InsertLines(uiDistance); +} + +// Routine Description: +// - DL - This control function deletes one or more lines in the scrolling +// region, starting with the line that has the cursor. +// As lines are deleted, lines below the cursor and in the scrolling region +// move up. The terminal adds blank lines with no visual character +// attributes at the bottom of the scrolling region. If uiDistance is greater than +// the number of lines remaining on the page, DL deletes only the remaining +// lines. DL has no effect outside the scrolling margins. +// Arguments: +// - uiDistance - number of lines to delete +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::DeleteLine(_In_ unsigned int const uiDistance) +{ + return !!_conApi->DeleteLines(uiDistance); +} + +// Routine Description: +// - DECSTBM - Set Scrolling Region +// This control function sets the top and bottom margins for the current page. +// You cannot perform scrolling outside the margins. +// Default: Margins are at the page limits. +// Arguments: +// - sTopMargin - the line number for the top margin. +// - sBottomMargin - the line number for the bottom margin. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::_DoSetTopBottomScrollingMargins(const SHORT sTopMargin, + const SHORT sBottomMargin) +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->MoveToBottom() && _conApi->GetConsoleScreenBufferInfoEx(&csbiex)); + + // so notes time: (input -> state machine out -> adapter out -> conhost internal) + // having only a top param is legal ([3;r -> 3,0 -> 3,h -> 3,h,true) + // having only a bottom param is legal ([;3r -> 0,3 -> 1,3 -> 1,3,true) + // having neither uses the defaults ([;r [r -> 0,0 -> 0,0 -> 0,0,false) + // an illegal combo (eg, 3;2r) is ignored + if (fSuccess) + { + SHORT sActualTop = sTopMargin; + SHORT sActualBottom = sBottomMargin; + SHORT sScreenHeight = csbiex.srWindow.Bottom - csbiex.srWindow.Top; + if ( sActualTop == 0 && sActualBottom == 0) + { + // Disable Margins + // This case is valid, and nothing changes. + } + else if (sActualBottom == 0) + { + sActualBottom = sScreenHeight; + } + else if (sActualBottom < sActualTop) + { + fSuccess = false; + } + else if ((sActualTop == 0 || sActualTop == 1) && sActualBottom == sScreenHeight) + { + // Client requests setting margins to the entire screen + // - clear them instead of setting them. + // This is for apps like `apt` (NOT `apt-get` which set scroll + // margins, but don't use the alt buffer.) + // Some apps will use 0 as a top, some will use 1. Both should behave the same. + sActualBottom = 0; + } + // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. + if (sActualTop > 0) + { + sActualTop -= 1; + } + if (sActualBottom > 0) + { + sActualBottom -= 1; + } + if (fSuccess) + { + _srScrollMargins.Top = sActualTop; + _srScrollMargins.Bottom = sActualBottom; + fSuccess = !!_conApi->PrivateSetScrollingRegion(&_srScrollMargins); + } + } + return fSuccess; +} + +// Routine Description: +// - DECSTBM - Set Scrolling Region +// This control function sets the top and bottom margins for the current page. +// You cannot perform scrolling outside the margins. +// Default: Margins are at the page limits. +// Arguments: +// - sTopMargin - the line number for the top margin. +// - sBottomMargin - the line number for the bottom margin. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetTopBottomScrollingMargins(const SHORT sTopMargin, + const SHORT sBottomMargin) +{ + // When this is called, the cursor should also be moved to home. + // Other functions that only need to set/reset the margins should call _DoSetTopBottomScrollingMargins + return _DoSetTopBottomScrollingMargins(sTopMargin, sBottomMargin) && CursorPosition(1, 1); +} + +// Routine Description: +// - RI - Performs a "Reverse line feed", essentially, the opposite of '\n'. +// Moves the cursor up one line, and tries to keep its position in the line +// Arguments: +// - None +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::ReverseLineFeed() +{ + return !!_conApi->PrivateReverseLineFeed(); +} + +// Routine Description: +// - OSC Set Window Title - Sets the title of the window +// Arguments: +// - title - The string to set the title to. Must be null terminated. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetWindowTitle(std::wstring_view title) +{ + return !!_conApi->SetConsoleTitleW(title); +} + +// - ASBSET - Creates and swaps to the alternate screen buffer. In virtual terminals, there exists both a "main" +// screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already +// existing alternate, it is discarded. +// Arguments: +// - None +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::UseAlternateScreenBuffer() +{ + return !!_conApi->PrivateUseAlternateScreenBuffer(); +} + +// Routine Description: +// - ASBRST - From the alternate buffer, returns to the main screen buffer. +// From the main screen buffer, does nothing. The alternate is discarded. +// Arguments: +// - None +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::UseMainScreenBuffer() +{ + return !!_conApi->PrivateUseMainScreenBuffer(); +} + +//Routine Description: +// HTS - sets a VT tab stop in the cursor's current column. +//Arguments: +// - None +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::HorizontalTabSet() +{ + return !!_conApi->PrivateHorizontalTabSet(); +} + +//Routine Description: +// CHT - performing a forwards tab. This will take the +// cursor to the tab stop following its current location. If there are no +// more tabs in this row, it will take it to the right side of the window. +// If it's already in the last column of the row, it will move it to the next line. +//Arguments: +// - sNumTabs - the number of tabs to perform +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::ForwardTab(const SHORT sNumTabs) +{ + return !!_conApi->PrivateForwardTab(sNumTabs); +} + +//Routine Description: +// CBT - performing a backwards tab. This will take the cursor to the tab stop +// previous to its current location. It will not reverse line feed. +//Arguments: +// - sNumTabs - the number of tabs to perform +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::BackwardsTab(const SHORT sNumTabs) +{ + return !!_conApi->PrivateBackwardsTab(sNumTabs); +} + +//Routine Description: +// TBC - Used to clear set tab stops. ClearType ClearCurrentColumn (0) results +// in clearing only the tab stop in the cursor's current column, if there +// is one. ClearAllColumns (3) results in resetting all set tab stops. +//Arguments: +// - sClearType - Whether to clear the current column, or all columns, defined in DispatchTypes::TabClearType +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::TabClear(const SHORT sClearType) +{ + bool fSuccess = false; + switch (sClearType) + { + case DispatchTypes::TabClearType::ClearCurrentColumn: + fSuccess = !!_conApi->PrivateTabClear(false); + break; + case DispatchTypes::TabClearType::ClearAllColumns: + fSuccess = !!_conApi->PrivateTabClear(true); + break; + } + return fSuccess; +} + +//Routine Description: +// Designate Charset - Sets the active charset to be the one mapped to wch. +// See DispatchTypes::VTCharacterSets for a list of supported charsets. +// Also http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Controls-beginning-with-ESC +// For a list of all charsets and their codes. +// If the specified charset is unsupported, we do nothing (remain on the current one) +//Arguments: +// - wchCharset - The character indicating the charset we should switch to. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::DesignateCharset(const wchar_t wchCharset) +{ + return _TermOutput.DesignateCharset(wchCharset); +} + +//Routine Description: +// Soft Reset - Perform a soft reset. See http://www.vt100.net/docs/vt510-rm/DECSTR.html +// The following table lists everything that should be done, 'X's indicate the ones that +// we actually perform. As the appropriate functionality is added to our ANSI support, +// we should update this. +// X Text cursor enable DECTCEM Cursor enabled. +// Insert/replace IRM Replace mode. +// Origin DECOM Absolute (cursor origin at upper-left of screen.) +// Autowrap DECAWM No autowrap. +// National replacement DECNRCM Multinational set. +// character set +// Keyboard action KAM Unlocked. +// X Numeric keypad DECNKM Numeric characters. +// X Cursor keys DECCKM Normal (arrow keys). +// X Set top and bottom margins DECSTBM Top margin = 1; bottom margin = page length. +// X All character sets G0, G1, G2, Default settings. +// G3, GL, GR +// X Select graphic rendition SGR Normal rendition. +// Select character attribute DECSCA Normal (erasable by DECSEL and DECSED). +// X Save cursor state DECSC Home position. +// Assign user preference DECAUPSS Set selected in Set-Up. +// supplemental set +// Select active DECSASD Main display. +// status display +// Keyboard position mode DECKPM Character codes. +// Cursor direction DECRLM Reset (Left-to-right), regardless of NVR setting. +// PC Term mode DECPCTERM Always reset. +//Arguments: +// +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::SoftReset() +{ + bool fSuccess = CursorVisibility(true); // Cursor enabled. + if (fSuccess) + { + fSuccess = SetCursorKeysMode(false); // Normal characters. + } + if (fSuccess) + { + fSuccess = SetKeypadMode(false); // Numeric characters. + } + if (fSuccess) + { + // Top margin = 1; bottom margin = page length. + fSuccess = _DoSetTopBottomScrollingMargins(0, 0); + } + if (fSuccess) + { + fSuccess = DesignateCharset(DispatchTypes::VTCharacterSets::USASCII); // Default Charset + } + if (fSuccess) + { + DispatchTypes::GraphicsOptions opt = DispatchTypes::GraphicsOptions::Off; + fSuccess = SetGraphicsRendition(&opt, 1); // Normal rendition. + } + if (fSuccess) + { + // Save cursor state: Home position. + _coordSavedCursor = {1, 1}; + } + + return fSuccess; +} + +//Routine Description: +// Full Reset - Perform a hard reset of the terminal. http://vt100.net/docs/vt220-rm/chapter4.html +// RIS performs the following actions: (Items with sub-bullets are supported) +// - Performs a communications line disconnect. +// - Clears UDKs. +// - Clears a down-line-loaded character set. +// - Clears the screen. +// * This is like Erase in Display (3), also clearing scrollback, as well as ED(2) +// - Returns the cursor to the upper-left corner of the screen. +// * CUP(1;1) +// - Sets the SGR state to normal. +// * SGR(Off) +// - Sets the selective erase attribute write state to "not erasable". +// - Sets all character sets to the default. +// * G0(USASCII) +//Arguments: +// +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::HardReset() +{ + // Clears the screen - Needs to be done in two operations. + bool fSuccess = _EraseScrollback(); + if (fSuccess) + { + fSuccess = EraseInDisplay(DispatchTypes::EraseType::All); + } + + // Cursor to 1,1 + if (fSuccess) + { + fSuccess = CursorPosition(1, 1); + } + + // Sets the SGR state to normal. + if (fSuccess) + { + fSuccess = SoftReset(); + } + + // delete all current tab stops and reapply + _conApi->PrivateSetDefaultTabStops(); + + return fSuccess; +} + +//Routine Description: +// Erase Scrollback (^[[3J - ED extension by xterm) +// Because conhost doesn't exactly have a scrollback, We have to be tricky here. +// We need to move the entire viewport to 0,0, and clear everything outside +// (0, 0, viewportWidth, viewportHeight) To give the appearance that +// everything above the viewport was cleared. +// We don't want to save the text BELOW the viewport, because in *nix, there isn't anything there +// (There isn't a scroll-forward, only a scrollback) +//Arguments: +// +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::_EraseScrollback() +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + // Make sure to reset the viewport (with MoveToBottom )to where it was + // before the user scrolled the console output + bool fSuccess = !!(_conApi->GetConsoleScreenBufferInfoEx(&csbiex) && _conApi->MoveToBottom()); + if (fSuccess) + { + const SMALL_RECT Screen = csbiex.srWindow; + const short sWidth = Screen.Right - Screen.Left; + const short sHeight = Screen.Bottom - Screen.Top; + FAIL_FAST_IF(!(sWidth > 0 && sHeight > 0)); + const COORD Cursor = csbiex.dwCursorPosition; + + // Rectangle to cut out of the existing buffer + SMALL_RECT srScroll = Screen; + // Paste coordinate for cut text above + COORD coordDestination; + coordDestination.X = 0; + coordDestination.Y = 0; + + // Fill character for remaining space left behind by "cut" operation (or for fill if we "cut" the entire line) + CHAR_INFO ciFill; + ciFill.Attributes = csbiex.wAttributes; + ciFill.Char.UnicodeChar = static_cast(0x20); // space character. use 0x20 instead of literal space because we can't assume the compiler will always turn ' ' into 0x20. + fSuccess = !!_conApi->ScrollConsoleScreenBufferW(&srScroll, nullptr, coordDestination, &ciFill); + if (fSuccess) + { + // Clear everything after the viewport. This is two regions: + // A. below the viewport + // B. to the right of the viewport. + + // First clear section A + const DWORD dwTotalAreaBelow = csbiex.dwSize.X * (csbiex.dwSize.Y - sHeight); + const COORD coordBelowStartPosition = {0, sHeight}; + // We don't use the _EraseAreaHelper here because _EraseSingleLineDistanceHelper does it all in one operation + fSuccess = _EraseSingleLineDistanceHelper(coordBelowStartPosition, dwTotalAreaBelow, csbiex.wAttributes); + + if (fSuccess) + { + // If there is a section B, clear it. + const COORD coordBottomRight = {csbiex.dwSize.X, coordBelowStartPosition.Y}; + const COORD coordRightStartPosition = {sWidth, 0}; + if (coordBottomRight.X > coordRightStartPosition.X) + { + // We use the Area helper here because the Line helper would + // erase the parts of the screen we want to keep too + fSuccess = _EraseAreaHelper(coordRightStartPosition, coordBottomRight, csbiex.wAttributes); + } + + if (fSuccess) + { + // Move the viewport (CAN'T be done in one call with SetConsoleScreenBufferInfoEx, because legacy) + SMALL_RECT srNewViewport; + srNewViewport.Left = 0; + srNewViewport.Top = 0; + // SetConsoleWindowInfo uses an inclusive rect, while GetConsoleScreenBufferInfo is exclusive + srNewViewport.Right = sWidth - 1; + srNewViewport.Bottom = sHeight - 1; + fSuccess = !!_conApi->SetConsoleWindowInfo(true, &srNewViewport); + + if (fSuccess) + { + // Move the cursor to the same relative location. + const COORD newCursor = {Cursor.X-Screen.Left, Cursor.Y-Screen.Top}; + fSuccess = !!_conApi->SetConsoleCursorPosition(newCursor); + } + } + } + } + } + return fSuccess; +} + +//Routine Description: +// Erase All (^[[2J - ED) +// Erase the current contents of the viewport. In most terminals, because they +// only have a scrollback (and not a buffer per-se), they implement this +// by scrolling the current contents of the buffer off of the screen. +// We can't properly replicate this behavior with only the public API, because +// we need to know where the last character in the buffer is. (it may be below the viewport) +//Arguments: +// +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::_EraseAll() +{ + return !!_conApi->PrivateEraseAll(); +} + +//Routine Description: +// Enable VT200 Mouse Mode - Enables/disables the mouse input handler in default tracking mode. +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableVT200MouseMode(const bool fEnabled) +{ + return !!_conApi->PrivateEnableVT200MouseMode(fEnabled); +} + +//Routine Description: +// Enable UTF-8 Extended Encoding - this changes the encoding scheme for sequences +// emitted by the mouse input handler. Does not enable/disable mouse mode on it's own. +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableUTF8ExtendedMouseMode(const bool fEnabled) +{ + return !!_conApi->PrivateEnableUTF8ExtendedMouseMode(fEnabled); +} + +//Routine Description: +// Enable SGR Extended Encoding - this changes the encoding scheme for sequences +// emitted by the mouse input handler. Does not enable/disable mouse mode on it's own. +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableSGRExtendedMouseMode(const bool fEnabled) +{ + return !!_conApi->PrivateEnableSGRExtendedMouseMode(fEnabled); +} + +//Routine Description: +// Enable Button Event mode - send mouse move events WITH A BUTTON PRESSED to the input. +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableButtonEventMouseMode(const bool fEnabled) +{ + return !!_conApi->PrivateEnableButtonEventMouseMode(fEnabled); +} + +//Routine Description: +// Enable Any Event mode - send all mouse events to the input. + +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableAnyEventMouseMode(const bool fEnabled) +{ + return !!_conApi->PrivateEnableAnyEventMouseMode(fEnabled); +} + +//Routine Description: +// Enable Alternate Scroll Mode - When in the Alt Buffer, send CUP and CUD on +// scroll up/down events instead of the usual sequences +//Arguments: +// - fEnabled - true to enable, false to disable. +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::EnableAlternateScroll(const bool fEnabled) +{ + return !!_conApi->PrivateEnableAlternateScroll(fEnabled); +} + +//Routine Description: +// Set Cursor Style - Changes the cursor's style to match the given Dispatch +// cursor style. Unix styles are a combination of the shape and the blinking state. +//Arguments: +// - cursorStyle - The unix-like cursor style to apply to the cursor +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) +{ + bool isPty = false; + _conApi->IsConsolePty(&isPty); + if (isPty) + { + return false; + } + + CursorType actualType = CursorType::Legacy; + bool fEnableBlinking = false; + + switch(cursorStyle) + { + case DispatchTypes::CursorStyle::BlinkingBlock: + case DispatchTypes::CursorStyle::BlinkingBlockDefault: + fEnableBlinking = true; + actualType = CursorType::FullBox; + break; + case DispatchTypes::CursorStyle::SteadyBlock: + fEnableBlinking = false; + actualType = CursorType::FullBox; + break; + + case DispatchTypes::CursorStyle::BlinkingUnderline: + fEnableBlinking = true; + actualType = CursorType::Underscore; + break; + case DispatchTypes::CursorStyle::SteadyUnderline: + fEnableBlinking = false; + actualType = CursorType::Underscore; + break; + + case DispatchTypes::CursorStyle::BlinkingBar: + fEnableBlinking = true; + actualType = CursorType::VerticalBar; + break; + case DispatchTypes::CursorStyle::SteadyBar: + fEnableBlinking = false; + actualType = CursorType::VerticalBar; + break; + } + + bool fSuccess = !!_conApi->SetCursorStyle(actualType); + if (fSuccess) + { + fSuccess = !!_conApi->PrivateAllowCursorBlinking(fEnableBlinking); + } + + return fSuccess; +} + +// Method Description: +// - Sets a single entry of the colortable to a new value +// Arguments: +// - tableIndex: The VT color table index +// - dwColor: The new RGB color value to use. +// Return Value: +// True if handled successfully. False othewise. +bool AdaptDispatch::SetCursorColor(const COLORREF cursorColor) +{ + bool isPty = false; + _conApi->IsConsolePty(&isPty); + if (isPty) + { + return false; + } + + return !!_conApi->SetCursorColor(cursorColor); +} + +// Method Description: +// - Sets a single entry of the colortable to a new value +// Arguments: +// - tableIndex: The VT color table index +// - dwColor: The new RGB color value to use. +// Return Value: +// True if handled successfully. False othewise. +bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, + const DWORD dwColor) +{ + + bool fSuccess = tableIndex < 256; + if (fSuccess) + { + const auto realIndex = ::Xterm256ToWindowsIndex(tableIndex); + fSuccess = !! _conApi->PrivateSetColorTableEntry(realIndex, dwColor); + } + + // If we're a conpty, always return false, so that we send the updated color + // value to the terminal. Still handle the sequence so apps that use + // the API or VT to query the values of the color table still read the + // correct color. + bool isPty = false; + _conApi->IsConsolePty(&isPty); + if (isPty) + { + return false; + } + + return fSuccess; +} + +//Routine Description: +// Window Manipulation - Performs a variety of actions relating to the window, +// such as moving the window position, resizing the window, querying +// window state, forcing the window to repaint, etc. +// This is kept seperate from the input version, as there may be +// codes that are supported in one direction but not the other. +//Arguments: +// - uiFunction - An identifier of the WindowManipulation function to perform +// - rgusParams - Additional parameters to pass to the function +// - cParams - size of rgusParams +// Return value: +// True if handled successfully. False othewise. +bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) +{ + bool fSuccess = false; + // Other Window Manipulation functions: + // MSFT:13271098 - QueryViewport + // MSFT:13271146 - QueryScreenSize + switch (uiFunction) + { + case DispatchTypes::WindowManipulationType::RefreshWindow: + if (cParams == 0) + { + fSuccess = DispatchCommon::s_RefreshWindow(*_conApi); + } + break; + case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: + if (cParams == 2) + { + fSuccess = DispatchCommon::s_ResizeWindow(*_conApi, rgusParams[1], rgusParams[0]); + } + break; + default: + fSuccess = false; + } + + return fSuccess; +} diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp new file mode 100644 index 000000000..b40c9f976 --- /dev/null +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -0,0 +1,172 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- adaptDispatch.hpp + +Abstract: +- This serves as the Windows Console API-specific implementation of the callbacks from our generic Virtual Terminal parser. + +Author(s): +- Michael Niksa (MiNiksa) 30-July-2015 +--*/ + +#pragma once + +#include "termDispatch.hpp" +#include "DispatchCommon.hpp" +#include "conGetSet.hpp" +#include "adaptDefaults.hpp" +#include "terminalOutput.hpp" +#include + +#define XTERM_COLOR_TABLE_SIZE (256) + + +namespace Microsoft::Console::VirtualTerminal +{ + class AdaptDispatch : public ITermDispatch + { + public: + + AdaptDispatch(ConGetSet* const pConApi, + AdaptDefaults* const pDefaults); + + virtual void Execute(const wchar_t wchControl) + { + _pDefaults->Execute(wchControl); + } + + virtual void PrintString(const wchar_t* const rgwch, const size_t cch); + virtual void Print(const wchar_t wchPrintable); + + virtual bool CursorUp(_In_ unsigned int const uiDistance); // CUU + virtual bool CursorDown(_In_ unsigned int const uiDistance); // CUD + virtual bool CursorForward(_In_ unsigned int const uiDistance); // CUF + virtual bool CursorBackward(_In_ unsigned int const uiDistance); // CUB + virtual bool CursorNextLine(_In_ unsigned int const uiDistance); // CNL + virtual bool CursorPrevLine(_In_ unsigned int const uiDistance); // CPL + virtual bool CursorHorizontalPositionAbsolute(_In_ unsigned int const uiColumn); // CHA + virtual bool VerticalLinePositionAbsolute(_In_ unsigned int const uiLine); // VPA + virtual bool CursorPosition(_In_ unsigned int const uiLine, _In_ unsigned int const uiColumn); // CUP + virtual bool CursorSavePosition(); // DECSC + virtual bool CursorRestorePosition(); // DECRC + virtual bool CursorVisibility(const bool fIsVisible); // DECTCEM + virtual bool EraseInDisplay(const DispatchTypes::EraseType eraseType); // ED + virtual bool EraseInLine(const DispatchTypes::EraseType eraseType); // EL + virtual bool EraseCharacters(_In_ unsigned int const uiNumChars); // ECH + virtual bool InsertCharacter(_In_ unsigned int const uiCount); // ICH + virtual bool DeleteCharacter(_In_ unsigned int const uiCount); // DCH + virtual bool SetGraphicsRendition(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions); // SGR + virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType); // DSR + virtual bool DeviceAttributes(); // DA + virtual bool ScrollUp(_In_ unsigned int const uiDistance); // SU + virtual bool ScrollDown(_In_ unsigned int const uiDistance); // SD + virtual bool InsertLine(_In_ unsigned int const uiDistance); // IL + virtual bool DeleteLine(_In_ unsigned int const uiDistance); // DL + virtual bool SetColumns(_In_ unsigned int const uiColumns); // DECSCPP, DECCOLM + virtual bool SetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rParams, + const size_t cParams); // DECSET + virtual bool ResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rParams, + const size_t cParams); // DECRST + virtual bool SetCursorKeysMode(const bool fApplicationMode); // DECCKM + virtual bool SetKeypadMode(const bool fApplicationMode); // DECKPAM, DECKPNM + virtual bool EnableCursorBlinking(const bool bEnable); // ATT610 + virtual bool SetTopBottomScrollingMargins(const SHORT sTopMargin, + const SHORT sBottomMargin); // DECSTBM + virtual bool ReverseLineFeed(); // RI + virtual bool SetWindowTitle(const std::wstring_view title) override; // OscWindowTitle + virtual bool UseAlternateScreenBuffer(); // ASBSET + virtual bool UseMainScreenBuffer(); // ASBRST + virtual bool HorizontalTabSet(); // HTS + virtual bool ForwardTab(const SHORT sNumTabs); // CHT + virtual bool BackwardsTab(const SHORT sNumTabs); // CBT + virtual bool TabClear(const SHORT sClearType); // TBC + virtual bool DesignateCharset(const wchar_t wchCharset); // DesignateCharset + virtual bool SoftReset(); // DECSTR + virtual bool HardReset(); // RIS + virtual bool EnableVT200MouseMode(const bool fEnabled); // ?1000 + virtual bool EnableUTF8ExtendedMouseMode(const bool fEnabled); // ?1005 + virtual bool EnableSGRExtendedMouseMode(const bool fEnabled); // ?1006 + virtual bool EnableButtonEventMouseMode(const bool fEnabled); // ?1002 + virtual bool EnableAnyEventMouseMode(const bool fEnabled); // ?1003 + virtual bool EnableAlternateScroll(const bool fEnabled); // ?1007 + virtual bool SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle); // DECSCUSR + virtual bool SetCursorColor(const COLORREF cursorColor); + + virtual bool SetColorTableEntry(const size_t tableIndex, + const DWORD dwColor); // OscColorTable + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams); // DTTERM_WindowManipulation + + private: + + enum class CursorDirection + { + Up, + Down, + Left, + Right, + NextLine, + PrevLine + }; + enum class ScrollDirection + { + Up, + Down + }; + + bool _CursorMovement(const CursorDirection dir, _In_ unsigned int const uiDistance) const; + bool _CursorMovePosition(_In_opt_ const unsigned int* const puiRow, _In_opt_ const unsigned int* const puiCol) const; + bool _EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX* const pcsbiex, const DispatchTypes::EraseType eraseType, const SHORT sLineId, const WORD wFillColor) const; + void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr); + bool _EraseAreaHelper(const COORD coordStartPosition, const COORD coordLastPosition, const WORD wFillColor); + bool _EraseSingleLineDistanceHelper(const COORD coordStartPosition, const DWORD dwLength, const WORD wFillColor) const; + bool _EraseScrollback(); + bool _EraseAll(); + void _SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr) const; + bool _InsertDeleteHelper(_In_ unsigned int const uiCount, const bool fIsInsert) const; + bool _ScrollMovement(const ScrollDirection dir, _In_ unsigned int const uiDistance) const; + static void s_DisableAllColors(_Inout_ WORD* const pAttr, const bool fIsForeground); + static void s_ApplyColors(_Inout_ WORD* const pAttr, const WORD wApplyThis, const bool fIsForeground); + + bool _DoSetTopBottomScrollingMargins(const SHORT sTopMargin, + const SHORT sBottomMargin); + bool _CursorPositionReport() const; + + bool _WriteResponse(_In_reads_(cchReply) PCWSTR pwszReply, const size_t cchReply) const; + bool _SetResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rgParams, const size_t cParams, const bool fEnable); + bool _PrivateModeParamsHelper(_In_ DispatchTypes::PrivateModeParams const param, const bool fEnable); + bool _DoDECCOLMHelper(_In_ unsigned int uiColumns); + + std::unique_ptr _conApi; + std::unique_ptr _pDefaults; + TerminalOutput _TermOutput; + + COORD _coordSavedCursor; + SMALL_RECT _srScrollMargins; + + bool _fIsSetColumnsEnabled; + + bool _fChangedForeground; + bool _fChangedBackground; + bool _fChangedMetaAttrs; + + bool _SetRgbColorsHelper(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions, + _Out_ COLORREF* const prgbColor, + _Out_ bool* const pfIsForeground, + _Out_ size_t* const pcOptionsConsumed); + + bool _SetBoldColorHelper(const DispatchTypes::GraphicsOptions option); + bool _SetDefaultColorHelper(const DispatchTypes::GraphicsOptions option); + + static bool s_IsXtermColorOption(const DispatchTypes::GraphicsOptions opt); + static bool s_IsRgbColorOption(const DispatchTypes::GraphicsOptions opt); + static bool s_IsBoldColorOption(const DispatchTypes::GraphicsOptions opt) noexcept; + static bool s_IsDefaultColorOption(const DispatchTypes::GraphicsOptions opt) noexcept; + }; +} diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp new file mode 100644 index 000000000..6950ca775 --- /dev/null +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -0,0 +1,451 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include "adaptDispatch.hpp" +#include "conGetSet.hpp" + +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS +#include + +using namespace Microsoft::Console::VirtualTerminal; +using namespace Microsoft::Console::VirtualTerminal::DispatchTypes; + +// Routine Description: +// - Small helper to disable all color flags within a given font attributes field +// Arguments: +// - pAttr - Pointer to font attributes field to adjust +// - fIsForeground - True if we're modifying the FOREGROUND colors. False if we're doing BACKGROUND. +// Return Value: +// - +void AdaptDispatch::s_DisableAllColors(_Inout_ WORD* const pAttr, const bool fIsForeground) +{ + if (fIsForeground) + { + *pAttr &= ~(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY); + } + else + { + *pAttr &= ~(BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY); + } +} + +// Routine Description: +// - Small helper to help mask off the appropriate foreground/background bits in the colors bitfield. +// Arguments: +// - pAttr - Pointer to font attributes field to adjust +// - wApplyThis - Color values to apply to the low or high word of the font attributes field. +// - fIsForeground - TRUE = foreground color. FALSE = background color. Specifies which half of the bit field to reset and then apply wApplyThis upon. +// Return Value: +// - +void AdaptDispatch::s_ApplyColors(_Inout_ WORD* const pAttr, const WORD wApplyThis, const bool fIsForeground) +{ + // Copy the new attribute to apply + WORD wNewColors = wApplyThis; + + // Mask off only the foreground or background + if (fIsForeground) + { + *pAttr &= ~(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY); + wNewColors &= (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY); + } + else + { + *pAttr &= ~(BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY); + wNewColors &= (BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY); + } + + // Apply appropriate flags. + *pAttr |= wNewColors; +} + +// Routine Description: +// - Helper to apply the actual flags to each text attributes field. +// - Placed as a helper so it can be recursive/re-entrant for some of the convenience flag methods that perform similar/multiple operations in one command. +// Arguments: +// - opt - Graphics option sent to us by the parser/requestor. +// - pAttr - Pointer to the font attribute field to adjust +// Return Value: +// - +void AdaptDispatch::_SetGraphicsOptionHelper(const DispatchTypes::GraphicsOptions opt, _Inout_ WORD* const pAttr) +{ + switch (opt) + { + case DispatchTypes::GraphicsOptions::Off: + FAIL_FAST_MSG("GraphicsOptions::Off should be handled by _SetDefaultColorHelper"); + break; + // MSFT:16398982 - These two are now handled by _SetBoldColorHelper + // case DispatchTypes::GraphicsOptions::BoldBright: + // case DispatchTypes::GraphicsOptions::UnBold: + case DispatchTypes::GraphicsOptions::Negative: + *pAttr |= COMMON_LVB_REVERSE_VIDEO; + _fChangedMetaAttrs = true; + break; + case DispatchTypes::GraphicsOptions::Underline: + *pAttr |= COMMON_LVB_UNDERSCORE; + _fChangedMetaAttrs = true; + break; + case DispatchTypes::GraphicsOptions::Positive: + *pAttr &= ~COMMON_LVB_REVERSE_VIDEO; + _fChangedMetaAttrs = true; + break; + case DispatchTypes::GraphicsOptions::NoUnderline: + *pAttr &= ~COMMON_LVB_UNDERSCORE; + _fChangedMetaAttrs = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundBlack: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundBlue: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_BLUE; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundGreen: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_GREEN; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundCyan: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_BLUE | FOREGROUND_GREEN; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundRed: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_RED; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundMagenta: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_BLUE | FOREGROUND_RED; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundYellow: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_GREEN | FOREGROUND_RED; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundWhite: + s_DisableAllColors(pAttr, true); // turn off all color flags first. + *pAttr |= FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundDefault: + FAIL_FAST_MSG("GraphicsOptions::ForegroundDefault should be handled by _SetDefaultColorHelper"); + break; + case DispatchTypes::GraphicsOptions::BackgroundBlack: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundBlue: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_BLUE; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundGreen: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_GREEN; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundCyan: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_BLUE | BACKGROUND_GREEN; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundRed: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_RED; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundMagenta: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_BLUE | BACKGROUND_RED; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundYellow: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_GREEN | BACKGROUND_RED; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundWhite: + s_DisableAllColors(pAttr, false); // turn off all color flags first. + *pAttr |= BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundDefault: + FAIL_FAST_MSG("GraphicsOptions::BackgroundDefault should be handled by _SetDefaultColorHelper"); + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlack: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundBlack, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlue: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundBlue, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundGreen: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundGreen, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundCyan: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundCyan, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundRed: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundRed, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundMagenta: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundMagenta, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundYellow: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundYellow, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundWhite: + _SetGraphicsOptionHelper(GraphicsOptions::ForegroundWhite, pAttr); + *pAttr |= FOREGROUND_INTENSITY; + _fChangedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlack: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundBlack, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlue: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundBlue, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundGreen: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundGreen, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundCyan: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundCyan, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundRed: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundRed, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundMagenta: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundMagenta, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundYellow: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundYellow, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundWhite: + _SetGraphicsOptionHelper(GraphicsOptions::BackgroundWhite, pAttr); + *pAttr |= BACKGROUND_INTENSITY; + _fChangedBackground = true; + break; + } +} + +// Routine Description: +// Returns true if the GraphicsOption represents an extended color option. +// These are followed by up to 4 more values which compose the entire option. +// Return Value: +// - true if the opt is the indicator for an extended color sequence, false otherwise. +bool AdaptDispatch::s_IsRgbColorOption(const DispatchTypes::GraphicsOptions opt) +{ + return opt == DispatchTypes::GraphicsOptions::ForegroundExtended || + opt == DispatchTypes::GraphicsOptions::BackgroundExtended; +} + +// Routine Description: +// Returns true if the GraphicsOption represents an extended color option. +// These are followed by up to 4 more values which compose the entire option. +// Return Value: +// - true if the opt is the indicator for an extended color sequence, false otherwise. +bool AdaptDispatch::s_IsBoldColorOption(const DispatchTypes::GraphicsOptions opt) noexcept +{ + return opt == DispatchTypes::GraphicsOptions::BoldBright || + opt == DispatchTypes::GraphicsOptions::UnBold; +} + +// Function Description: +// - checks if this graphics option should set either the console's FG or BG to +//the default attributes. +// Return Value: +// - true if the opt sets either/or attribute to the defaults, false otherwise. +bool AdaptDispatch::s_IsDefaultColorOption(const DispatchTypes::GraphicsOptions opt) noexcept +{ + return opt == DispatchTypes::GraphicsOptions::Off || + opt == DispatchTypes::GraphicsOptions::ForegroundDefault || + opt == DispatchTypes::GraphicsOptions::BackgroundDefault; +} + +// Routine Description: +// - Helper to parse extended graphics options, which start with 38 (FG) or 48 (BG) +// These options are followed by either a 2 (RGB) or 5 (xterm index) +// RGB sequences then take 3 MORE params to designate the R, G, B parts of the color +// Xterm index will use the param that follows to use a color from the preset 256 color xterm color table. +// Arguments: +// - rgOptions - An array of options that will be used to generate the RGB color +// - cOptions - The count of options +// - prgbColor - A pointer to place the generated RGB color into. +// - pfIsForeground - a pointer to place whether or not the parsed color is for the foreground or not. +// - pcOptionsConsumed - a pointer to place the number of options we consumed parsing this option. +// - ColorTable - the windows color table, for xterm indices < 16 +// - cColorTable - The number of elements in the windows color table. +// Return Value: +// Returns true if we successfully parsed an extended color option from the options array. +// - This corresponds to the following number of options consumed (pcOptionsConsumed): +// 1 - false, not enough options to parse. +// 2 - false, not enough options to parse. +// 3 - true, parsed an xterm index to a color +// 5 - true, parsed an RGB color. +bool AdaptDispatch::_SetRgbColorsHelper(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, + const size_t cOptions, + _Out_ COLORREF* const prgbColor, + _Out_ bool* const pfIsForeground, + _Out_ size_t* const pcOptionsConsumed) +{ + bool fSuccess = false; + *pcOptionsConsumed = 1; + if (cOptions >= 2 && s_IsRgbColorOption(rgOptions[0])) + { + *pcOptionsConsumed = 2; + DispatchTypes::GraphicsOptions extendedOpt = rgOptions[0]; + DispatchTypes::GraphicsOptions typeOpt = rgOptions[1]; + + if (extendedOpt == DispatchTypes::GraphicsOptions::ForegroundExtended) + { + *pfIsForeground = true; + } + else if (extendedOpt == DispatchTypes::GraphicsOptions::BackgroundExtended) + { + *pfIsForeground = false; + } + + if (typeOpt == DispatchTypes::GraphicsOptions::RGBColor && cOptions >= 5) + { + *pcOptionsConsumed = 5; + // ensure that each value fits in a byte + unsigned int red = rgOptions[2] > 255? 255 : rgOptions[2]; + unsigned int green = rgOptions[3] > 255? 255 : rgOptions[3]; + unsigned int blue = rgOptions[4] > 255? 255 : rgOptions[4]; + + *prgbColor = RGB(red, green, blue); + + fSuccess = !!_conApi->SetConsoleRGBTextAttribute(*prgbColor, *pfIsForeground); + } + else if (typeOpt == DispatchTypes::GraphicsOptions::Xterm256Index && cOptions >= 3) + { + *pcOptionsConsumed = 3; + if (rgOptions[2] <= 255) // ensure that the provided index is on the table + { + unsigned int tableIndex = rgOptions[2]; + + fSuccess = !!_conApi->SetConsoleXtermTextAttribute(tableIndex, *pfIsForeground); + } + } + } + return fSuccess; +} + +bool AdaptDispatch::_SetBoldColorHelper(const DispatchTypes::GraphicsOptions option) +{ + const bool bold = (option == DispatchTypes::GraphicsOptions::BoldBright); + return !!_conApi->PrivateBoldText(bold); +} + +bool AdaptDispatch::_SetDefaultColorHelper(const DispatchTypes::GraphicsOptions option) +{ + const bool fg = option == GraphicsOptions::Off || option == GraphicsOptions::ForegroundDefault; + const bool bg = option == GraphicsOptions::Off || option == GraphicsOptions::BackgroundDefault; + bool success = _conApi->PrivateSetDefaultAttributes(fg, bg); + if (success && fg && bg) + { + // If we're resetting both the FG & BG, also reset the meta attributes (underline) + // as well as the boldness + success = _conApi->PrivateSetLegacyAttributes(0, false, false, true) && + _conApi->PrivateBoldText(false); + } + return success; +} + +// Routine Description: +// - SGR - Modifies the graphical rendering options applied to the next characters written into the buffer. +// - Options include colors, invert, underlines, and other "font style" type options. +// Arguments: +// - rgOptions - An array of options that will be applied from 0 to N, in order, one at a time by setting or removing flags in the font style properties. +// - cOptions - The count of options (a.k.a. the N in the above line of comments) +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetGraphicsRendition(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, const size_t cOptions) +{ + // We use the private function here to get just the default color attributes as a performance optimization. + // Calling the public GetConsoleScreenBufferInfoEx costs a lot of performance time/power in a tight loop + // because it has to fill the Largest Window Size by asking the OS and wastes time memcpying colors and other data + // we do not need to resolve this Set Graphics Rendition request. + WORD attr; + bool fSuccess = !!_conApi->PrivateGetConsoleScreenBufferAttributes(&attr); + + if (fSuccess) + { + // Run through the graphics options and apply them + for (size_t i = 0; i < cOptions; i++) + { + DispatchTypes::GraphicsOptions opt = rgOptions[i]; + if (s_IsDefaultColorOption(opt)) + { + fSuccess = _SetDefaultColorHelper(opt); + } + else if (s_IsBoldColorOption(opt)) + { + fSuccess = _SetBoldColorHelper(rgOptions[i]); + } + else if (s_IsRgbColorOption(opt)) + { + COLORREF rgbColor; + bool fIsForeground = true; + + size_t cOptionsConsumed = 0; + + // _SetRgbColorsHelper will call the appropriate ConApi function + fSuccess = _SetRgbColorsHelper(&(rgOptions[i]), cOptions-i, &rgbColor, &fIsForeground, &cOptionsConsumed); + + i += (cOptionsConsumed - 1); // cOptionsConsumed includes the opt we're currently on. + } + else + { + _SetGraphicsOptionHelper(opt, &attr); + fSuccess = !!_conApi->PrivateSetLegacyAttributes(attr, _fChangedForeground, _fChangedBackground, _fChangedMetaAttrs); + + // Make sure we un-bold + if (fSuccess && opt == DispatchTypes::GraphicsOptions::Off) + { + fSuccess = _SetBoldColorHelper(opt); + } + + _fChangedForeground = false; + _fChangedBackground = false; + _fChangedMetaAttrs = false; + } + } + + } + + return fSuccess; +} diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp new file mode 100644 index 000000000..2f2733b03 --- /dev/null +++ b/src/terminal/adapter/conGetSet.hpp @@ -0,0 +1,112 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- conGetSet.hpp + +Abstract: +- This serves as an abstraction layer for the adapters to connect to the console API functions. +- The abstraction allows for the substitution of the functions for internal/external to Conhost.exe use as well as easy testing. + +Author(s): +- Michael Niksa (MiNiksa) 30-July-2015 +--*/ + +#pragma once + + +#include "..\..\types\inc\IInputEvent.hpp" +#include "..\..\inc\conattrs.hpp" + +#include +#include + +namespace Microsoft::Console::VirtualTerminal +{ + class ConGetSet + { + public: + virtual BOOL GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const = 0; + virtual BOOL GetConsoleScreenBufferInfoEx(_Out_ CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) const = 0; + virtual BOOL SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const pConsoleScreenBufferInfoEx) = 0; + virtual BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) = 0; + virtual BOOL SetConsoleCursorPosition(const COORD coordCursorPosition) = 0; + virtual BOOL FillConsoleOutputCharacterW(const WCHAR wch, + const DWORD nLength, + const COORD dwWriteCoord, + size_t& numberOfCharsWritten) noexcept = 0; + virtual BOOL FillConsoleOutputAttribute(const WORD wAttribute, + const DWORD nLength, + const COORD dwWriteCoord, + size_t& numberOfAttrsWritten) noexcept = 0; + virtual BOOL SetConsoleTextAttribute(const WORD wAttr) = 0; + + virtual BOOL PrivateSetLegacyAttributes(const WORD wAttr, + const bool fForeground, + const bool fBackground, + const bool fMeta) = 0; + + virtual BOOL PrivateSetDefaultAttributes(const bool fForeground, const bool fBackground) = 0; + + virtual BOOL SetConsoleXtermTextAttribute(const int iXtermTableEntry, + const bool fIsForeground) = 0; + virtual BOOL SetConsoleRGBTextAttribute(const COLORREF rgbColor, const bool fIsForeground) = 0; + virtual BOOL PrivateBoldText(const bool bolded) = 0; + + virtual BOOL PrivateWriteConsoleInputW(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) = 0; + virtual BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, + _In_opt_ const SMALL_RECT* pClipRectangle, + _In_ COORD dwDestinationOrigin, + const CHAR_INFO* pFill) = 0; + virtual BOOL SetConsoleWindowInfo(const BOOL bAbsolute, + const SMALL_RECT* const lpConsoleWindow) = 0; + virtual BOOL PrivateSetCursorKeysMode(const bool fApplicationMode) = 0; + virtual BOOL PrivateSetKeypadMode(const bool fApplicationMode) = 0; + + virtual BOOL PrivateShowCursor(const bool show) = 0; + virtual BOOL PrivateAllowCursorBlinking(const bool fEnable) = 0; + + virtual BOOL PrivateSetScrollingRegion(const SMALL_RECT* const psrScrollMargins) = 0; + virtual BOOL PrivateReverseLineFeed() = 0; + virtual BOOL SetConsoleTitleW(const std::wstring_view title) = 0; + virtual BOOL PrivateUseAlternateScreenBuffer() = 0; + virtual BOOL PrivateUseMainScreenBuffer() = 0; + virtual BOOL PrivateHorizontalTabSet() = 0; + virtual BOOL PrivateForwardTab(const SHORT sNumTabs) = 0; + virtual BOOL PrivateBackwardsTab(const SHORT sNumTabs) = 0; + virtual BOOL PrivateTabClear(const bool fClearAll) = 0; + virtual BOOL PrivateSetDefaultTabStops() = 0; + + virtual BOOL PrivateEnableVT200MouseMode(const bool fEnabled) = 0; + virtual BOOL PrivateEnableUTF8ExtendedMouseMode(const bool fEnabled) = 0; + virtual BOOL PrivateEnableSGRExtendedMouseMode(const bool fEnabled) = 0; + virtual BOOL PrivateEnableButtonEventMouseMode(const bool fEnabled) = 0; + virtual BOOL PrivateEnableAnyEventMouseMode(const bool fEnabled) = 0; + virtual BOOL PrivateEnableAlternateScroll(const bool fEnabled) = 0; + virtual BOOL PrivateEraseAll() = 0; + virtual BOOL SetCursorStyle(const CursorType cursorType) = 0; + virtual BOOL SetCursorColor(const COLORREF cursorColor) = 0; + virtual BOOL PrivateGetConsoleScreenBufferAttributes(_Out_ WORD* const pwAttributes) = 0; + virtual BOOL PrivatePrependConsoleInput(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) = 0; + virtual BOOL PrivateWriteConsoleControlInput(_In_ KeyEvent key) = 0; + virtual BOOL PrivateRefreshWindow() = 0; + + virtual BOOL GetConsoleOutputCP(_Out_ unsigned int* const puiOutputCP) = 0; + + virtual BOOL PrivateSuppressResizeRepaint() = 0; + virtual BOOL IsConsolePty(_Out_ bool* const pIsPty) const = 0; + + virtual BOOL MoveCursorVertically(const short lines) = 0; + + virtual BOOL DeleteLines(const unsigned int count) = 0; + virtual BOOL InsertLines(const unsigned int count) = 0; + + virtual BOOL MoveToBottom() const = 0; + + virtual BOOL PrivateSetColorTableEntry(const short index, const COLORREF value) const = 0; + + }; +} diff --git a/src/terminal/adapter/dirs b/src/terminal/adapter/dirs new file mode 100644 index 000000000..dc14638bf --- /dev/null +++ b/src/terminal/adapter/dirs @@ -0,0 +1,2 @@ +DIRS=lib \ + ut_adapter \ diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj new file mode 100644 index 000000000..9b511056a --- /dev/null +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {1CF55140-EF6A-4736-A403-957E4F7430BB} + + + + {DCF55140-EF6A-4736-A403-957E4F7430BB} + Win32Proj + adapter + TerminalAdapter + ConTermAdapt + + + + + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters new file mode 100644 index 000000000..72098e2e4 --- /dev/null +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -0,0 +1,72 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/adapter/lib/sources b/src/terminal/adapter/lib/sources new file mode 100644 index 000000000..629c03b0d --- /dev/null +++ b/src/terminal/adapter/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTermAdapter +TARGETTYPE = LIBRARY diff --git a/src/terminal/adapter/precomp.cpp b/src/terminal/adapter/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/terminal/adapter/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/terminal/adapter/precomp.h b/src/terminal/adapter/precomp.h new file mode 100644 index 000000000..660c85780 --- /dev/null +++ b/src/terminal/adapter/precomp.h @@ -0,0 +1,25 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS +#include + +#include + +#include "telemetry.hpp" +#include "tracing.hpp" + +#include "..\..\inc\conattrs.hpp" diff --git a/src/terminal/adapter/runtest.bat b/src/terminal/adapter/runtest.bat new file mode 100644 index 000000000..08f1a0f41 --- /dev/null +++ b/src/terminal/adapter/runtest.bat @@ -0,0 +1 @@ +.\ut_adapter\run.bat %* \ No newline at end of file diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc new file mode 100644 index 000000000..b972d0263 --- /dev/null +++ b/src/terminal/adapter/sources.inc @@ -0,0 +1,55 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Virtual Terminal Adapter +# ------------------------------------- + +# This module converts Virtual Terminal style actions into +# class Win32 API calls back into the console host application. +# In conjunction with the parser module, this allows APIs to be called +# simply using the STDOUT stream. + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES= \ + ..\adaptDispatch.cpp \ + ..\DispatchCommon.cpp \ + ..\InteractDispatch.cpp \ + ..\adaptDispatchGraphics.cpp \ + ..\MouseInput.cpp \ + ..\terminalOutput.cpp \ + ..\telemetry.cpp \ + ..\tracing.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + ..\..\parser; \ + +TARGETLIBS= \ + $(TARGETLIBS) \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + +DLOAD_ERROR_HANDLER = kernelbase + +DELAYLOAD = \ + ext-ms-win-ntuser-keyboard-l1.dll; \ diff --git a/src/terminal/adapter/telemetry.cpp b/src/terminal/adapter/telemetry.cpp new file mode 100644 index 000000000..75de2c1c2 --- /dev/null +++ b/src/terminal/adapter/telemetry.cpp @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include "telemetry.hpp" diff --git a/src/terminal/adapter/telemetry.hpp b/src/terminal/adapter/telemetry.hpp new file mode 100644 index 000000000..a1f9c7fc8 --- /dev/null +++ b/src/terminal/adapter/telemetry.hpp @@ -0,0 +1,20 @@ +/*++ +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 + +TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp new file mode 100644 index 000000000..ea3f2fce1 --- /dev/null +++ b/src/terminal/adapter/termDispatch.hpp @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- termDispatch.hpp + +Abstract: +- This is a useful default implementation of all of the ITermDispatch callbacks. + Fails on every callback, but handy for tests. +*/ +#include "ITermDispatch.hpp" +#pragma once + +namespace Microsoft::Console::VirtualTerminal +{ + class TermDispatch; +}; + +class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Console::VirtualTerminal::ITermDispatch +{ +public: + virtual void Execute(const wchar_t wchControl) = 0; + virtual void Print(const wchar_t wchPrintable) = 0; + virtual void PrintString(const wchar_t* const rgwch, const size_t cch) = 0; + + virtual bool CursorUp(const unsigned int /*uiDistance*/) { return false; }; // CUU + virtual bool CursorDown(const unsigned int /*uiDistance*/) { return false; } // CUD + virtual bool CursorForward(const unsigned int /*uiDistance*/) { return false; } // CUF + virtual bool CursorBackward(const unsigned int /*uiDistance*/) { return false; } // CUB + virtual bool CursorNextLine(const unsigned int /*uiDistance*/) { return false; } // CNL + virtual bool CursorPrevLine(const unsigned int /*uiDistance*/) { return false; } // CPL + virtual bool CursorHorizontalPositionAbsolute(const unsigned int /*uiColumn*/) { return false; } // CHA + virtual bool VerticalLinePositionAbsolute(const unsigned int /*uiLine*/) { return false; } // VPA + virtual bool CursorPosition(const unsigned int /*uiLine*/, const unsigned int /*uiColumn*/) { return false; } // CUP + virtual bool CursorSavePosition() { return false; } // DECSC + virtual bool CursorRestorePosition() { return false; } // DECRC + virtual bool CursorVisibility(const bool /*fIsVisible*/) { return false; } // DECTCEM + virtual bool InsertCharacter(const unsigned int /*uiCount*/) { return false; } // ICH + virtual bool DeleteCharacter(const unsigned int /*uiCount*/) { return false; } // DCH + virtual bool ScrollUp(const unsigned int /*uiDistance*/) { return false; } // SU + virtual bool ScrollDown(const unsigned int /*uiDistance*/) { return false; } // SD + virtual bool InsertLine(const unsigned int /*uiDistance*/) { return false; } // IL + virtual bool DeleteLine(const unsigned int /*uiDistance*/) { return false; } // DL + virtual bool SetColumns(const unsigned int /*uiColumns*/) { return false; } // DECSCPP, DECCOLM + virtual bool SetCursorKeysMode(const bool /*fApplicationMode*/) { return false; } // DECCKM + virtual bool SetKeypadMode(const bool /*fApplicationMode*/) { return false; } // DECKPAM, DECKPNM + virtual bool EnableCursorBlinking(const bool /*fEnable*/) { return false; } // ATT610 + virtual bool SetTopBottomScrollingMargins(const SHORT /*sTopMargin*/, const SHORT /*sBottomMargin*/) { return false; } // DECSTBM + virtual bool ReverseLineFeed() { return false; } // RI + virtual bool SetWindowTitle(std::wstring_view /*title*/) { return false; } // OscWindowTitle + virtual bool UseAlternateScreenBuffer() { return false; } // ASBSET + virtual bool UseMainScreenBuffer() { return false; } // ASBRST + virtual bool HorizontalTabSet() { return false; } // HTS + virtual bool ForwardTab(const SHORT /*sNumTabs*/) { return false; } // CHT + virtual bool BackwardsTab(const SHORT /*sNumTabs*/) { return false; } // CBT + virtual bool TabClear(const SHORT /*sClearType*/) { return false; } // TBC + virtual bool EnableVT200MouseMode(const bool /*fEnabled*/) { return false; } // ?1000 + virtual bool EnableUTF8ExtendedMouseMode(const bool /*fEnabled*/) { return false; } // ?1005 + virtual bool EnableSGRExtendedMouseMode(const bool /*fEnabled*/) { return false; } // ?1006 + virtual bool EnableButtonEventMouseMode(const bool /*fEnabled*/) { return false; } // ?1002 + virtual bool EnableAnyEventMouseMode(const bool /*fEnabled*/) { return false; } // ?1003 + virtual bool EnableAlternateScroll(const bool /*fEnabled*/) { return false; } // ?1007 + virtual bool SetColorTableEntry(const size_t /*tableIndex*/, const DWORD /*dwColor*/) { return false; } // OSCColorTable + + virtual bool EraseInDisplay(const DispatchTypes::EraseType /* eraseType*/) { return false; } // ED + virtual bool EraseInLine(const DispatchTypes::EraseType /* eraseType*/) { return false; } // EL + virtual bool EraseCharacters(const unsigned int /*uiNumChars*/){ return false; } // ECH + + virtual bool SetGraphicsRendition(_In_reads_(_Param_(2)) const DispatchTypes::GraphicsOptions* const /*rgOptions*/, + const size_t /*cOptions*/) { return false; } // SGR + + virtual bool SetPrivateModes(_In_reads_(_Param_(2)) const DispatchTypes::PrivateModeParams* const /*rgParams*/, + const size_t /*cParams*/) { return false; } // DECSET + + virtual bool ResetPrivateModes(_In_reads_(_Param_(2)) const DispatchTypes::PrivateModeParams* const /*rgParams*/, + const size_t /*cParams*/) { return false; } // DECRST + + virtual bool DeviceStatusReport(const DispatchTypes::AnsiStatusType /*statusType*/) { return false; } // DSR + virtual bool DeviceAttributes() { return false; } // DA + + virtual bool DesignateCharset(const wchar_t /*wchCharset*/){ return false; } // DesignateCharset + + virtual bool SoftReset(){ return false; } // DECSTR + virtual bool HardReset(){ return false; } // RIS + + virtual bool SetCursorStyle(const DispatchTypes::CursorStyle /*cursorStyle*/){ return false; } // DECSCUSR + virtual bool SetCursorColor(const COLORREF /*Color*/) { return false; } // OSCSetCursorColor, OSCResetCursorColor + + // DTTERM_WindowManipulation + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType /*uiFunction*/, + _In_reads_(_Param_(3)) const unsigned short* const /*rgusParams*/, + const size_t /*cParams*/) { return false; } + +}; + diff --git a/src/terminal/adapter/terminalOutput.cpp b/src/terminal/adapter/terminalOutput.cpp new file mode 100644 index 000000000..259565003 --- /dev/null +++ b/src/terminal/adapter/terminalOutput.cpp @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include "terminalOutput.hpp" +#include "strsafe.h" + +using namespace Microsoft::Console::VirtualTerminal; + +TerminalOutput::TerminalOutput() +{ + +} + +TerminalOutput::~TerminalOutput() +{ + +} + +// We include a full table so all we have to do is the lookup. +// The tables only ever change the values x20 - x7f, hence why the table starts at \x20 +// From http://vt100.net/docs/vt220-rm/table2-4.html +const wchar_t TerminalOutput::s_rgDECSpecialGraphicsTranslations[s_uiNumDisplayCharacters] +{ + L'\x20', L'\x21', L'\x22', L'\x23', L'\x24', L'\x25', L'\x26', L'\x27', L'\x28', L'\x29', L'\x2a', L'\x2b', L'\x2c', L'\x2d', L'\x2e', L'\x2f', + L'\x30', L'\x31', L'\x32', L'\x33', L'\x34', L'\x35', L'\x36', L'\x37', L'\x38', L'\x39', L'\x3a', L'\x3b', L'\x3c', L'\x3d', L'\x3e', L'\x3f', + L'\x40', L'\x41', L'\x42', L'\x43', L'\x44', L'\x45', L'\x46', L'\x47', L'\x48', L'\x49', L'\x4a', L'\x4b', L'\x4c', L'\x4d', L'\x4e', L'\x4f', + L'\x50', L'\x51', L'\x52', L'\x53', L'\x54', L'\x55', L'\x56', L'\x57', L'\x58', L'\x59', L'\x5a', L'\x5b', L'\x5c', L'\x5d', L'\x5e', L'\x5f', + L'\u25C6', // L'\x60', -> Diamond + L'\u2592', // L'\x61', -> Checkerboard + L'\u2409', // L'\x62', -> HT, SYMBOL FOR HORIZONTAL TABULATION + L'\u240c', // L'\x63', -> FF, SYMBOL FOR FORM FEED + L'\u240d', // L'\x64', -> CR, SYMBOL FOR CARRIAGE RETURN + L'\u240a', // L'\x65', -> LF, SYMBOL FOR LINE FEED + L'\u00B0', // L'\x66', -> Degree symbol + L'\u00B1', // L'\x67', -> Plus/minus + L'\u2424', // L'\x68', -> NL, SYMBOL FOR NEWLINE + L'\u240b', // L'\x69', -> VT, SYMBOL FOR VERTICAL TABULATION + L'\u2518', // L'\x6a', -> Lower-right corner + L'\u2510', // L'\x6b', -> Upper-right corner + L'\u250c', // L'\x6c', -> Upper-left corner + L'\u2514', // L'\x6d', -> Lower-left corner + L'\u253C', // L'\x6e', -> crossing lines + L'\u23ba', // L'\x6f', -> HORIZONTAL SCAN LINE-3 + L'\u23bb', // L'\x70', -> HORIZONTAL SCAN LINE-3 + L'\u2500', // L'\x71', -> HORIZONTAL SCAN LINE-5 + L'\u23bc', // L'\x72', -> HORIZONTAL SCAN LINE-7 + L'\u23bd', // L'\x73', -> HORIZONTAL SCAN LINE-7 + L'\u251c', // L'\x74', -> Left "T" + L'\u2524', // L'\x75', -> Right "T" + L'\u2534', // L'\x76', -> Bottom "T" + L'\u252c', // L'\x77', -> Top "T" + L'\u2502', // L'\x78', -> | Vertical bar + L'\u2264', // L'\x79', -> Less than or equal to + L'\u2265', // L'\x7a', -> Greater than or equal to + L'\u03C0', // L'\x7b', -> Pi + L'\u2260', // L'\x7c', -> Not equal to + L'\u00A3', // L'\x7d', -> UK pound sign + L'\u00B7', // L'\x7e', -> Centered dot + L'\x7f' // L'\x7f', -> DEL +}; + +bool TerminalOutput::DesignateCharset(const wchar_t wchNewCharset) +{ + bool result = false; + if (wchNewCharset == DispatchTypes::VTCharacterSets::DEC_LineDrawing || + wchNewCharset == DispatchTypes::VTCharacterSets::USASCII) + { + _wchCurrentCharset = wchNewCharset; + result = true; + } + return result; +} + +// Routine Description: +// - Returns true if the current charset isn't USASCII, indicating that text has to come through here +// Arguments: +// - +// Return Value: +// - True if the current charset is not USASCII +bool TerminalOutput::NeedToTranslate() const +{ + return _wchCurrentCharset != DispatchTypes::VTCharacterSets::USASCII; +} + +const wchar_t* TerminalOutput::_GetTranslationTable() const +{ + const wchar_t* pwchTranslation = nullptr; + switch (_wchCurrentCharset) + { + case DispatchTypes::VTCharacterSets::DEC_LineDrawing: + pwchTranslation = TerminalOutput::s_rgDECSpecialGraphicsTranslations; + break; + } + return pwchTranslation; +} + +wchar_t TerminalOutput::TranslateKey(const wchar_t wch) const +{ + wchar_t wchFound = wch; + if (_wchCurrentCharset == DispatchTypes::VTCharacterSets::USASCII || + wch < '\x60' || wch > '\x7f') // filter out the region we know is unchanged + { + ; // do nothing, these are the same as default. + } + else + { + const wchar_t* pwchTranslationTable = _GetTranslationTable(); + if (pwchTranslationTable != nullptr) + { + wchFound = (pwchTranslationTable[wch - '\x20']); + } + } + return wchFound; + +} diff --git a/src/terminal/adapter/terminalOutput.hpp b/src/terminal/adapter/terminalOutput.hpp new file mode 100644 index 000000000..365c99386 --- /dev/null +++ b/src/terminal/adapter/terminalOutput.hpp @@ -0,0 +1,44 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- terminalOutput.hpp + +Abstract: +- Provides a set of functions for translating certain characters into other + characters. There are special VT modes where the display characters (values + x20 - x7f) should be displayed as other characters. This module provides an + componentization of that logic. + +Author(s): +- Mike Griese (migrie) 03-Mar-2016 +--*/ +#pragma once + +#include "termDispatch.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class TerminalOutput sealed + { + public: + + TerminalOutput(); + ~TerminalOutput(); + + wchar_t TranslateKey(const wchar_t wch) const; + bool DesignateCharset(const wchar_t wchNewCharset); + bool NeedToTranslate() const; + + private: + wchar_t _wchCurrentCharset = DispatchTypes::VTCharacterSets::USASCII; + + // The tables only ever change the values x20 - x7f (96 display characters) + static const unsigned int s_uiNumDisplayCharacters = 96; + static const wchar_t s_rgDECSpecialGraphicsTranslations[s_uiNumDisplayCharacters]; + + const wchar_t* _GetTranslationTable() const; + + }; +} diff --git a/src/terminal/adapter/tracing.cpp b/src/terminal/adapter/tracing.cpp new file mode 100644 index 000000000..b72682fe9 --- /dev/null +++ b/src/terminal/adapter/tracing.cpp @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "tracing.hpp" diff --git a/src/terminal/adapter/tracing.hpp b/src/terminal/adapter/tracing.hpp new file mode 100644 index 000000000..dad93792a --- /dev/null +++ b/src/terminal/adapter/tracing.hpp @@ -0,0 +1,18 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- tracing.hpp + +Abstract: +- This module is used for recording tracing/debugging information to the telemetry ETW channel +- The data is not automatically broadcast to telemetry backends as it does not set the TELEMETRY keyword. +- NOTE: Many functions in this file appear to be copy/pastes. This is because the TraceLog documentation warns + to not be "cute" in trying to reduce its macro usages with variables as it can cause unexpected behavior. + +--*/ + +#pragma once + +#include "telemetry.hpp" diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj new file mode 100644 index 000000000..2a0b48f38 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj @@ -0,0 +1,66 @@ + + + + + + + + + Create + + + + + + + + {06ec74cb-9a12-429c-b551-8562ec954746} + + + {06ec74cb-9a12-429c-b551-8562ec964846} + + + {06ec74cb-9a12-429c-b551-8532ec964726} + + + {345fd5a4-b32b-4f29-bd1c-b033bd2c35cc} + + + {af0a096a-8b3a-4949-81ef-7df8f0fee91f} + + + {1c959542-bac2-4e55-9a6d-13251914cbb9} + + + {18d09a24-8240-42d6-8cb6-236eee820262} + + + {2fd12fbb-1ddb-46d8-b818-1023c624caca} + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + + {6AF01638-84CF-4B65-9870-484DFFCAC772} + Win32Proj + AdapterUnitTests + TerminalAdapter.UnitTests + ConAdapter.Unit.Tests + + + + ..;%(AdditionalIncludeDirectories) + + + + + + + \ No newline at end of file diff --git a/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters new file mode 100644 index 000000000..4e51b70ca --- /dev/null +++ b/src/terminal/adapter/ut_adapter/Adapter.UnitTests.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/adapter/ut_adapter/MouseInputTest.cpp b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp new file mode 100644 index 000000000..bf4b4510d --- /dev/null +++ b/src/terminal/adapter/ut_adapter/MouseInputTest.cpp @@ -0,0 +1,568 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "MouseInput.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class MouseInputTest; + }; + }; +}; +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 wchar_t* s_pwszInputExpected; + +static wchar_t s_pwszExpectedBuffer[BYTE_MAX]; // big enough for anything + +static COORD s_rgTestCoords [] = { + {0, 0}, + {0, 1}, + {1, 1}, + {2, 2}, + {94, 94}, // 94+1+32 = 127 + {95, 95}, // 95+1+32 = 128, this is the ascii boundary + {96, 96}, + {127,127}, + {128,128}, + {SHORT_MAX-33,SHORT_MAX-33}, + {SHORT_MAX-32,SHORT_MAX-32}, +}; + +// Note: We're going to be changing the value of the third char (the space) of +// these strings as we test things with this array, to alter the expected button value. +// The default value is the button=WM_LBUTTONDOWN case, which is element[3]=' ' +static wchar_t* s_rgDefaultTestOutput [] = { + L"\x1b[M !!", + L"\x1b[M !\"", + L"\x1b[M \"\"", + L"\x1b[M ##", + L"\x1b[M \x7f\x7f", + L"\x1b[M \x80\x80", // 95 - This and below will always fail for default (non utf8) + L"\x1b[M \x81\x81", + L"\x1b[M \x00A0\x00A0", //127 + L"\x1b[M \x00A1\x00A1", + L"\x1b[M \x7FFF\x7FFF", // FFDE + L"\x1b[M \x8000\x8000", // This one will always fail for Default and UTF8 +}; + +// Note: We're going to be changing the value of the third char (the space) of +// these strings as we test things with this array, to alter the expected button value. +// The default value is the button=WM_LBUTTONDOWN case, which is element[3]='0' +// We're also going to change the last element, for button-down (M) vs button-up (m) +static wchar_t* s_rgSgrTestOutput [] = { + L"\x1b[<%d;1;1M", + L"\x1b[<%d;1;2M", + L"\x1b[<%d;2;2M", + L"\x1b[<%d;3;3M", + L"\x1b[<%d;95;95M", + L"\x1b[<%d;96;96M", // 95 - This and below will always fail for default (non utf8) + L"\x1b[<%d;97;97M", + L"\x1b[<%d;128;128M", //127 + L"\x1b[<%d;129;129M", + L"\x1b[<%d;32735;32735M", // FFDE + L"\x1b[<%d;32736;32736M", +}; + +static int s_iTestCoordsLength = ARRAYSIZE(s_rgTestCoords); + +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); + KeyEvent 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(wchar_t* 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 + wchar_t 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; + } + + // Routine Description: + // Constructs a string from s_rgSgrTestOutput with the third and last chars + // correctly filled in to match uiButton. + wchar_t* BuildSGRTestOutput(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)); + + 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; + } + + wchar_t GetDefaultCharFromButton(unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + { + wchar_t wch = L'\x0'; + Log::Comment(NoThrowString().Format(L"uiButton '%d'", uiButton)); + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + wch = L' '; + break; + case WM_LBUTTONUP: + case WM_MBUTTONUP: + case WM_RBUTTONUP: + wch = L'#'; + break; + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + wch = L'\"'; + break; + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + wch = L'!'; + break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + Log::Comment(NoThrowString().Format(L"MOUSEWHEEL")); + wch = L'`' + (sScrollDelta > 0? 0 : 1); + break; + case WM_MOUSEMOVE: + default: + Log::Comment(NoThrowString().Format(L"DEFAULT")); + wch = L'\x0'; + break; + } + // MK_SHIFT is ignored by the translator + wch += (sModifierKeystate & MK_CONTROL) ? 0x08 : 0x00; + return wch; + } + + int GetSgrCharFromButton(unsigned int uiButton, short sModifierKeystate, short sScrollDelta) + { + int result = 0; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + result = 0; + break; + case WM_MBUTTONUP: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + result = 1; + break; + case WM_RBUTTONUP: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + result = 2; + break; + case WM_MOUSEMOVE: + result = 3 + 0x20; // we add 0x20 to hover events, which are all encoded as WM_MOUSEMOVE events + break; + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + result = (sScrollDelta > 0? 64 : 65); + break; + default: + result = 0; + break; + } + // MK_SHIFT and MK_ALT is ignored by the translator + result += (sModifierKeystate & MK_CONTROL) ? 0x08 : 0x00; + return result; + } + + bool IsButtonDown(unsigned int uiButton) + { + bool fIsDown = false; + switch (uiButton) + { + case WM_LBUTTONDBLCLK: + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_RBUTTONDBLCLK: + case WM_MBUTTONDOWN: + case WM_MBUTTONDBLCLK: + case WM_MOUSEWHEEL: + case WM_MOUSEHWHEEL: + fIsDown = true; + break; + } + return fIsDown; + } + + /* From winuser.h - Needed to manually specify the test properties + #define WM_MOUSEFIRST 0x0200 + #define WM_MOUSEMOVE 0x0200 + #define WM_LBUTTONDOWN 0x0201 + #define WM_LBUTTONUP 0x0202 + #define WM_LBUTTONDBLCLK 0x0203 + #define WM_RBUTTONDOWN 0x0204 + #define WM_RBUTTONUP 0x0205 + #define WM_RBUTTONDBLCLK 0x0206 + #define WM_MBUTTONDOWN 0x0207 + #define WM_MBUTTONUP 0x0208 + #define WM_MBUTTONDBLCLK 0x0209 + #if (_WIN32_WINNT >= 0x0400) || (_WIN32_WINDOWS > 0x0400) + #define WM_MOUSEWHEEL 0x020A + #endif + #if (_WIN32_WINNT >= 0x0500) + #define WM_XBUTTONDOWN 0x020B + #define WM_XBUTTONUP 0x020C + #define WM_XBUTTONDBLCLK 0x020D + #endif + #if (_WIN32_WINNT >= 0x0600) + #define WM_MOUSEHWHEEL 0x020E + */ + TEST_METHOD(DefaultModeTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + // TEST_METHOD_PROPERTY(L"Data:uiButton", L"{WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP}") + TEST_METHOD_PROPERTY(L"Data:uiButton", L"{0x0201, 0x0202, 0x0207, 0x0208, 0x0204, 0x0205}") + // None, MK_SHIFT, MK_CONTROL + TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0000, 0x0004, 0x0008}") + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + std::unique_ptr mouseInput = std::make_unique(s_MouseInputTestCallback); + + unsigned int uiModifierKeystate = 0; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); + short sModifierKeystate = (SHORT)uiModifierKeystate; + short sScrollDelta = 0; + + unsigned int uiButton; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); + + bool fExpectedKeyHandled = false; + s_pwszInputExpected = L"\x0"; + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({0,0}, uiButton, sModifierKeystate, sScrollDelta)); + + mouseInput->EnableDefaultTracking(true); + + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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)); + + s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + } + + mouseInput->EnableButtonEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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)); + + s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + } + + mouseInput->EnableAnyEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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)); + + s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + } + + } + TEST_METHOD(Utf8ModeTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + // TEST_METHOD_PROPERTY(L"Data:uiButton", L"{WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP}") + TEST_METHOD_PROPERTY(L"Data:uiButton", L"{0x0201, 0x0202, 0x0207, 0x0208, 0x0204, 0x0205}") + TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0000, 0x0004, 0x0008}") + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + std::unique_ptr mouseInput = std::make_unique(s_MouseInputTestCallback); + + unsigned int uiModifierKeystate = 0; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); + short sModifierKeystate = (SHORT)uiModifierKeystate; + short sScrollDelta = 0; + + unsigned int uiButton; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); + + bool fExpectedKeyHandled = false; + s_pwszInputExpected = L"\x0"; + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({0,0}, uiButton, sModifierKeystate, sScrollDelta)); + + mouseInput->SetUtf8ExtendedMode(true); + + short MaxCoord = SHORT_MAX - 33; + + mouseInput->EnableDefaultTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + mouseInput->EnableButtonEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + mouseInput->EnableAnyEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + } + + TEST_METHOD(SgrModeTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + // TEST_METHOD_PROPERTY(L"Data:uiButton", L"{WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_MOUSEMOVE}") + TEST_METHOD_PROPERTY(L"Data:uiButton", L"{0x0201, 0x0202, 0x0207, 0x0208, 0x0204, 0x0205, 0x0200}") + TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0000, 0x0004, 0x0008}") + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + std::unique_ptr mouseInput = std::make_unique(s_MouseInputTestCallback); + unsigned int uiModifierKeystate = 0; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); + short sModifierKeystate = (SHORT)uiModifierKeystate; + short sScrollDelta = 0; + + unsigned int uiButton; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiButton", uiButton)); + + bool fExpectedKeyHandled = false; + s_pwszInputExpected = L"\x0"; + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({0,0}, uiButton, sModifierKeystate, sScrollDelta)); + + mouseInput->SetSGRExtendedMode(true); + + // 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->EnableDefaultTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, + mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + mouseInput->EnableButtonEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + fExpectedKeyHandled = true; + mouseInput->EnableAnyEventTracking(true); + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + } + + TEST_METHOD(ScrollWheelTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:sScrollDelta", L"{0, -1, 1, 100, -10000, 32736}") + TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0000, 0x0004, 0x0008}") + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + std::unique_ptr mouseInput = std::make_unique(s_MouseInputTestCallback); + unsigned int uiModifierKeystate = 0; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiModifierKeystate)); + short sModifierKeystate = (SHORT)uiModifierKeystate; + + unsigned int uiButton = WM_MOUSEWHEEL; + int iScrollDelta = 0; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"sScrollDelta", iScrollDelta)); + short sScrollDelta = (short)(iScrollDelta); + + bool fExpectedKeyHandled = false; + s_pwszInputExpected = L"\x0"; + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse({0,0}, uiButton, sModifierKeystate, sScrollDelta)); + + // Default Tracking, Default Encoding + mouseInput->EnableDefaultTracking(true); + + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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)); + s_pwszInputExpected = BuildDefaultTestOutput(s_rgDefaultTestOutput[i], uiButton, sModifierKeystate, sScrollDelta); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + // Default Tracking, UTF8 Encoding + mouseInput->SetUtf8ExtendedMode(true); + short MaxCoord = SHORT_MAX - 33; + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + + + // Default Tracking, SGR Encoding + mouseInput->SetSGRExtendedMode(true); + fExpectedKeyHandled = true; // SGR Mode should be able to handle any arbitrary coords. + for (int i = 0; i < s_iTestCoordsLength; i++) + { + COORD 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); + + // validate translation + VERIFY_ARE_EQUAL(fExpectedKeyHandled, mouseInput->HandleMouse(Coord, uiButton, sModifierKeystate, sScrollDelta), + NoThrowString().Format(L"(x,y)=(%d,%d)", Coord.X, Coord.Y)); + + } + } +}; diff --git a/src/terminal/adapter/ut_adapter/WexHelpers.hpp b/src/terminal/adapter/ut_adapter/WexHelpers.hpp new file mode 100644 index 000000000..5bc14ddc3 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/WexHelpers.hpp @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace WEX { + namespace TestExecution { + template<> + class VerifyOutputTraits < SMALL_RECT > + { + public: + static WEX::Common::NoThrowString ToString(const SMALL_RECT& sr) + { + return WEX::Common::NoThrowString().Format(L"(L:%d, R:%d, T:%d, B:%d)", sr.Left, sr.Right, sr.Top, sr.Bottom); + } + }; + + template<> + class VerifyCompareTraits < SMALL_RECT, SMALL_RECT > + { + public: + static bool AreEqual(const SMALL_RECT& expected, const SMALL_RECT& actual) + { + return expected.Left == actual.Left && + expected.Right == actual.Right && + expected.Top == actual.Top && + expected.Bottom == actual.Bottom; + } + + static bool AreSame(const SMALL_RECT& expected, const SMALL_RECT& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const SMALL_RECT& /*expectedLess*/, const SMALL_RECT& /*expectedGreater*/) + { + VERIFY_FAIL(L"Less than is invalid for SMALL_RECT comparisons."); + return false; + } + + static bool IsGreaterThan(const SMALL_RECT& /*expectedGreater*/, const SMALL_RECT& /*expectedLess*/) + { + VERIFY_FAIL(L"Greater than is invalid for SMALL_RECT comparisons."); + return false; + } + + static bool IsNull(const SMALL_RECT& object) + { + return object.Left == 0 && object.Right == 0 && object.Top == 0 && object.Bottom == 0; + } + }; + + template<> + class VerifyOutputTraits < COORD > + { + public: + static WEX::Common::NoThrowString ToString(const COORD& coord) + { + return WEX::Common::NoThrowString().Format(L"(X:%d, Y:%d)", coord.X, coord.Y); + } + }; + + template<> + class VerifyCompareTraits < COORD, COORD> + { + public: + static bool AreEqual(const COORD& expected, const COORD& actual) + { + return expected.X == actual.X && + expected.Y == actual.Y; + } + + static bool AreSame(const COORD& expected, const COORD& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const COORD& expectedLess, const COORD& expectedGreater) + { + // less is on a line above greater (Y values less than) + return (expectedLess.Y < expectedGreater.Y) || + // or on the same lines and less is left of greater (X values less than) + ((expectedLess.Y == expectedGreater.Y) && (expectedLess.X < expectedGreater.X)); + } + + static bool IsGreaterThan(const COORD& expectedGreater, const COORD& expectedLess) + { + // greater is on a line below less (Y value greater than) + return (expectedGreater.Y > expectedLess.Y) || + // or on the same lines and greater is right of less (X values greater than) + ((expectedGreater.Y == expectedLess.Y) && (expectedGreater.X > expectedLess.X)); + } + + static bool IsNull(const COORD& object) + { + return object.X == 0 && object.Y == 0; + } + }; + + template<> + class VerifyOutputTraits < CONSOLE_CURSOR_INFO > + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_CURSOR_INFO& cci) + { + return WEX::Common::NoThrowString().Format(L"(Vis:%s, Size:%d)", cci.bVisible ? L"True" : L"False", cci.dwSize); + } + }; + + template<> + class VerifyCompareTraits < CONSOLE_CURSOR_INFO, CONSOLE_CURSOR_INFO > + { + public: + static bool AreEqual(const CONSOLE_CURSOR_INFO& expected, const CONSOLE_CURSOR_INFO& actual) + { + return expected.bVisible == actual.bVisible && + expected.dwSize == actual.dwSize; + } + + static bool AreSame(const CONSOLE_CURSOR_INFO& expected, const CONSOLE_CURSOR_INFO& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_CURSOR_INFO& /*expectedLess*/, const CONSOLE_CURSOR_INFO& /*expectedGreater*/) + { + VERIFY_FAIL(L"Less than is invalid for CONSOLE_CURSOR_INFO comparisons."); + return false; + } + + static bool IsGreaterThan(const CONSOLE_CURSOR_INFO& /*expectedGreater*/, + const CONSOLE_CURSOR_INFO& /*expectedLess*/) + { + VERIFY_FAIL(L"Greater than is invalid for CONSOLE_CURSOR_INFO comparisons."); + return false; + } + + static bool IsNull(const CONSOLE_CURSOR_INFO& object) + { + return object.bVisible == 0 && object.dwSize == 0; + } + }; + + template<> + class VerifyOutputTraits < CONSOLE_SCREEN_BUFFER_INFOEX > + { + public: + static WEX::Common::NoThrowString ToString(const CONSOLE_SCREEN_BUFFER_INFOEX& sbiex) + { + return WEX::Common::NoThrowString().Format(L"(Full:%s Attrs:0x%x PopupAttrs:0x%x CursorPos:%s Size:%s MaxSize:%s Viewport:%s)\r\nColors:\r\n(0:0x%x)\r\n(1:0x%x)\r\n(2:0x%x)\r\n(3:0x%x)\r\n(4:0x%x)\r\n(5:0x%x)\r\n(6:0x%x)\r\n(7:0x%x)\r\n(8:0x%x)\r\n(9:0x%x)\r\n(A:0x%x)\r\n(B:0x%x)\r\n(C:0x%x)\r\n(D:0x%x)\r\n(E:0x%x)\r\n(F:0x%x)\r\n", + sbiex.bFullscreenSupported ? L"True" : L"False", + sbiex.wAttributes, + sbiex.wPopupAttributes, + VerifyOutputTraits::ToString(sbiex.dwCursorPosition).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.dwSize).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.dwMaximumWindowSize).ToCStrWithFallbackTo(L"Fail"), + VerifyOutputTraits::ToString(sbiex.srWindow).ToCStrWithFallbackTo(L"Fail"), + sbiex.ColorTable[0], + sbiex.ColorTable[1], + sbiex.ColorTable[2], + sbiex.ColorTable[3], + sbiex.ColorTable[4], + sbiex.ColorTable[5], + sbiex.ColorTable[6], + sbiex.ColorTable[7], + sbiex.ColorTable[8], + sbiex.ColorTable[9], + sbiex.ColorTable[10], + sbiex.ColorTable[11], + sbiex.ColorTable[12], + sbiex.ColorTable[13], + sbiex.ColorTable[14], + sbiex.ColorTable[15]); + + } + }; + + template<> + class VerifyCompareTraits < CONSOLE_SCREEN_BUFFER_INFOEX, CONSOLE_SCREEN_BUFFER_INFOEX > + { + public: + static bool AreEqual(const CONSOLE_SCREEN_BUFFER_INFOEX& expected, const CONSOLE_SCREEN_BUFFER_INFOEX& actual) + { + return expected.bFullscreenSupported == actual.bFullscreenSupported && + expected.wAttributes == actual.wAttributes && + expected.wPopupAttributes == actual.wPopupAttributes && + VerifyCompareTraits::AreEqual(expected.dwCursorPosition, actual.dwCursorPosition) && + VerifyCompareTraits::AreEqual(expected.dwSize, actual.dwSize) && + VerifyCompareTraits::AreEqual(expected.dwMaximumWindowSize, actual.dwMaximumWindowSize) && + VerifyCompareTraits::AreEqual(expected.srWindow, actual.srWindow) && + expected.ColorTable[0] == actual.ColorTable[0] && + expected.ColorTable[1] == actual.ColorTable[1] && + expected.ColorTable[2] == actual.ColorTable[2] && + expected.ColorTable[3] == actual.ColorTable[3] && + expected.ColorTable[4] == actual.ColorTable[4] && + expected.ColorTable[5] == actual.ColorTable[5] && + expected.ColorTable[6] == actual.ColorTable[6] && + expected.ColorTable[7] == actual.ColorTable[7] && + expected.ColorTable[8] == actual.ColorTable[8] && + expected.ColorTable[9] == actual.ColorTable[9] && + expected.ColorTable[10] == actual.ColorTable[10] && + expected.ColorTable[11] == actual.ColorTable[11] && + expected.ColorTable[12] == actual.ColorTable[12] && + expected.ColorTable[13] == actual.ColorTable[13] && + expected.ColorTable[14] == actual.ColorTable[14] && + expected.ColorTable[15] == actual.ColorTable[15]; + } + + static bool AreSame(const CONSOLE_SCREEN_BUFFER_INFOEX& expected, const CONSOLE_SCREEN_BUFFER_INFOEX& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const CONSOLE_SCREEN_BUFFER_INFOEX& /*expectedLess*/, + const CONSOLE_SCREEN_BUFFER_INFOEX& /*expectedGreater*/) + { + VERIFY_FAIL(L"Less than is invalid for CONSOLE_SCREEN_BUFFER_INFOEX comparisons."); + return false; + } + + static bool IsGreaterThan(const CONSOLE_SCREEN_BUFFER_INFOEX& /*expectedGreater*/, + const CONSOLE_SCREEN_BUFFER_INFOEX& /*expectedLess*/) + { + VERIFY_FAIL(L"Greater than is invalid for CONSOLE_SCREEN_BUFFER_INFOEX comparisons."); + return false; + } + + static bool IsNull(const CONSOLE_SCREEN_BUFFER_INFOEX& object) + { + return object.bFullscreenSupported == 0 && + object.wAttributes == 0 && + object.wPopupAttributes == 0 && + VerifyCompareTraits::IsNull(object.dwCursorPosition) && + VerifyCompareTraits::IsNull(object.dwSize) && + VerifyCompareTraits::IsNull(object.dwMaximumWindowSize) && + VerifyCompareTraits::IsNull(object.srWindow) && + object.ColorTable[0] == 0x0 && + object.ColorTable[1] == 0x0 && + object.ColorTable[2] == 0x0 && + object.ColorTable[3] == 0x0 && + object.ColorTable[4] == 0x0 && + object.ColorTable[5] == 0x0 && + object.ColorTable[6] == 0x0 && + object.ColorTable[7] == 0x0 && + object.ColorTable[8] == 0x0 && + object.ColorTable[9] == 0x0 && + object.ColorTable[10] == 0x0 && + object.ColorTable[11] == 0x0 && + object.ColorTable[12] == 0x0 && + object.ColorTable[13] == 0x0 && + object.ColorTable[14] == 0x0 && + object.ColorTable[15] == 0x0; + } + }; + + template<> + class VerifyOutputTraits + { + public: + static WEX::Common::NoThrowString ToString(const INPUT_RECORD& ir) + { + SetVerifyOutput verifySettings(VerifyOutputSettings::LogOnlyFailures); + WCHAR szBuf[1024]; + VERIFY_SUCCEEDED(StringCchCopy(szBuf, ARRAYSIZE(szBuf), L"(ev: ")); + switch(ir.EventType) + { + case FOCUS_EVENT: + { + WCHAR szFocus[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szFocus, + ARRAYSIZE(szFocus), + L"FOCUS set: %s)", + ir.Event.FocusEvent.bSetFocus ? L"T" : L"F")); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szFocus)); + break; + } + + case KEY_EVENT: + { + WCHAR szKey[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szKey, + ARRAYSIZE(szKey), + L"KEY down: %s reps: %d kc: 0x%x sc: 0x%x uc: %d ctl: 0x%x)", + ir.Event.KeyEvent.bKeyDown ? L"T" : L"F", + ir.Event.KeyEvent.wRepeatCount, + ir.Event.KeyEvent.wVirtualKeyCode, + ir.Event.KeyEvent.wVirtualScanCode, + ir.Event.KeyEvent.uChar.UnicodeChar, + ir.Event.KeyEvent.dwControlKeyState)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szKey)); + break; + } + + case MENU_EVENT: + { + WCHAR szMenu[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szMenu, + ARRAYSIZE(szMenu), + L"MENU cmd: %d (0x%x))", + ir.Event.MenuEvent.dwCommandId, + ir.Event.MenuEvent.dwCommandId)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szMenu)); + break; + } + + case MOUSE_EVENT: + { + WCHAR szMouse[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szMouse, + ARRAYSIZE(szMouse), + L"MOUSE pos: (%d, %d) buttons: 0x%x ctl: 0x%x evflags: 0x%x)", + ir.Event.MouseEvent.dwMousePosition.X, + ir.Event.MouseEvent.dwMousePosition.Y, + ir.Event.MouseEvent.dwButtonState, + ir.Event.MouseEvent.dwControlKeyState, + ir.Event.MouseEvent.dwEventFlags)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szMouse)); + break; + } + + case WINDOW_BUFFER_SIZE_EVENT: + { + WCHAR szBufferSize[512]; + VERIFY_SUCCEEDED(StringCchPrintf(szBufferSize, + ARRAYSIZE(szBufferSize), + L"WINDOW_BUFFER_SIZE (%d, %d)", + ir.Event.WindowBufferSizeEvent.dwSize.X, + ir.Event.WindowBufferSizeEvent.dwSize.Y)); + VERIFY_SUCCEEDED(StringCchCat(szBuf, ARRAYSIZE(szBuf), szBufferSize)); + break; + } + + default: + VERIFY_FAIL(L"ERROR: unknown input event type encountered"); + } + + return WEX::Common::NoThrowString(szBuf); + } + }; + + template<> + class VerifyCompareTraits < INPUT_RECORD, INPUT_RECORD > + { + public: + static bool AreEqual(const INPUT_RECORD& expected, const INPUT_RECORD& actual) + { + bool fEqual = false; + if (expected.EventType == actual.EventType) + { + switch (expected.EventType) + { + case FOCUS_EVENT: + { + fEqual = expected.Event.FocusEvent.bSetFocus == actual.Event.FocusEvent.bSetFocus; + break; + } + + case KEY_EVENT: + { + fEqual = (expected.Event.KeyEvent.bKeyDown == actual.Event.KeyEvent.bKeyDown && + expected.Event.KeyEvent.wRepeatCount == actual.Event.KeyEvent.wRepeatCount && + expected.Event.KeyEvent.wVirtualKeyCode == actual.Event.KeyEvent.wVirtualKeyCode && + expected.Event.KeyEvent.wVirtualScanCode == actual.Event.KeyEvent.wVirtualScanCode && + expected.Event.KeyEvent.uChar.UnicodeChar == actual.Event.KeyEvent.uChar.UnicodeChar && + expected.Event.KeyEvent.dwControlKeyState == actual.Event.KeyEvent.dwControlKeyState); + break; + } + + case MENU_EVENT: + { + fEqual = expected.Event.MenuEvent.dwCommandId == actual.Event.MenuEvent.dwCommandId; + break; + } + + case MOUSE_EVENT: + { + fEqual = (expected.Event.MouseEvent.dwMousePosition.X == actual.Event.MouseEvent.dwMousePosition.X && + expected.Event.MouseEvent.dwMousePosition.Y == actual.Event.MouseEvent.dwMousePosition.Y && + expected.Event.MouseEvent.dwButtonState == actual.Event.MouseEvent.dwButtonState && + expected.Event.MouseEvent.dwControlKeyState == actual.Event.MouseEvent.dwControlKeyState && + expected.Event.MouseEvent.dwEventFlags == actual.Event.MouseEvent.dwEventFlags); + break; + } + + case WINDOW_BUFFER_SIZE_EVENT: + { + fEqual = (expected.Event.WindowBufferSizeEvent.dwSize.X == actual.Event.WindowBufferSizeEvent.dwSize.X && + expected.Event.WindowBufferSizeEvent.dwSize.Y == actual.Event.WindowBufferSizeEvent.dwSize.Y); + break; + } + + default: + VERIFY_FAIL(L"ERROR: unknown input event type encountered"); + } + } + + return fEqual; + } + + static bool AreSame(const INPUT_RECORD& expected, const INPUT_RECORD& actual) + { + return &expected == &actual; + } + + static bool IsLessThan(const INPUT_RECORD& /*expectedLess*/, const INPUT_RECORD& /*expectedGreater*/) + { + VERIFY_FAIL(L"Less than is invalid for INPUT_RECORD comparisons."); + return false; + } + + static bool IsGreaterThan(const INPUT_RECORD& /*expectedGreater*/, const INPUT_RECORD& /*expectedLess*/) + { + VERIFY_FAIL(L"Greater than is invalid for INPUT_RECORD comparisons."); + return false; + } + + static bool IsNull(const INPUT_RECORD& object) + { + return object.EventType == 0; + } + }; + } +} diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp new file mode 100644 index 000000000..84a745b5a --- /dev/null +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -0,0 +1,3512 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "adaptDispatch.hpp" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class AdapterTest; + class ConAdapterTestGetSet; + }; + }; +}; + + +enum class CursorY +{ + TOP, + BOTTOM, + YCENTER +}; + +enum class CursorX +{ + LEFT, + RIGHT, + XCENTER +}; + +enum class CursorDirection : unsigned int +{ + UP = 0, + DOWN = 1, + RIGHT = 2, + LEFT = 3, + NEXTLINE = 4, + PREVLINE = 5 +}; + +enum class ScrollDirection : unsigned int +{ + UP = 0, + DOWN = 1 +}; + +enum class AbsolutePosition : unsigned int +{ + CursorHorizontal = 0, + VerticalLine = 1, +}; + +using namespace Microsoft::Console::VirtualTerminal; + +class TestGetSet final : public ConGetSet +{ +public: + BOOL GetConsoleScreenBufferInfoEx(_Out_ CONSOLE_SCREEN_BUFFER_INFOEX* const psbiex) const override + { + Log::Comment(L"GetConsoleScreenBufferInfoEx MOCK returning data..."); + + if (_fGetConsoleScreenBufferInfoExResult) + { + psbiex->dwSize = _coordBufferSize; + psbiex->srWindow = _srViewport; + psbiex->dwCursorPosition = _coordCursorPos; + psbiex->wAttributes = _wAttribute; + } + + return _fGetConsoleScreenBufferInfoExResult; + } + BOOL SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BUFFER_INFOEX* const psbiex) override + { + Log::Comment(L"SetConsoleScreenBufferInfoEx MOCK returning data..."); + + if (_fSetConsoleScreenBufferInfoExResult) + { + VERIFY_ARE_EQUAL(_coordExpectedCursorPos, psbiex->dwCursorPosition); + VERIFY_ARE_EQUAL(_coordExpectedScreenBufferSize, psbiex->dwSize); + VERIFY_ARE_EQUAL(_srExpectedScreenBufferViewport, psbiex->srWindow); + VERIFY_ARE_EQUAL(_wExpectedAttributes, psbiex->wAttributes); + } + return _fSetConsoleScreenBufferInfoExResult; + } + BOOL SetConsoleCursorPosition(const COORD dwCursorPosition) override + { + Log::Comment(L"SetConsoleCursorPosition MOCK called..."); + + if (_fSetConsoleCursorPositionResult) + { + VERIFY_ARE_EQUAL(_coordExpectedCursorPos, dwCursorPosition); + _coordCursorPos = dwCursorPosition; + } + + return _fSetConsoleCursorPositionResult; + } + + BOOL GetConsoleCursorInfo(_In_ CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) const override + { + Log::Comment(L"GetConsoleCursorInfo MOCK called..."); + + if (_fGetConsoleCursorInfoResult) + { + pConsoleCursorInfo->dwSize = _dwCursorSize; + pConsoleCursorInfo->bVisible = _fCursorVisible; + } + + return _fGetConsoleCursorInfoResult; + } + + BOOL SetConsoleCursorInfo(const CONSOLE_CURSOR_INFO* const pConsoleCursorInfo) override + { + Log::Comment(L"SetConsoleCursorInfo MOCK called..."); + + if (_fSetConsoleCursorInfoResult) + { + VERIFY_ARE_EQUAL(_dwExpectedCursorSize, pConsoleCursorInfo->dwSize); + VERIFY_ARE_EQUAL(_fExpectedCursorVisible, pConsoleCursorInfo->bVisible); + } + + return _fSetConsoleCursorInfoResult; + } + + BOOL SetConsoleWindowInfo(const BOOL bAbsolute, const SMALL_RECT* const lpConsoleWindow) override + { + Log::Comment(L"SetConsoleWindowInfo MOCK called..."); + + if (_fSetConsoleWindowInfoResult) + { + VERIFY_ARE_EQUAL(_fExpectedWindowAbsolute, bAbsolute); + VERIFY_ARE_EQUAL(_srExpectedConsoleWindow, *lpConsoleWindow); + _srViewport = *lpConsoleWindow; + } + + return _fSetConsoleWindowInfoResult; + } + + BOOL PrivateSetCursorKeysMode(const bool fCursorKeysApplicationMode) override + { + Log::Comment(L"PrivateSetCursorKeysMode MOCK called..."); + + if (_fPrivateSetCursorKeysModeResult) + { + VERIFY_ARE_EQUAL(_fCursorKeysApplicationMode, fCursorKeysApplicationMode); + } + + return _fPrivateSetCursorKeysModeResult; + } + + BOOL PrivateSetKeypadMode(const bool fKeypadApplicationMode) override + { + Log::Comment(L"PrivateSetKeypadMode MOCK called..."); + + if (_fPrivateSetKeypadModeResult) + { + VERIFY_ARE_EQUAL(_fKeypadApplicationMode, fKeypadApplicationMode); + } + + return _fPrivateSetKeypadModeResult; + } + + BOOL PrivateShowCursor(const bool show) override + { + Log::Comment(L"PrivateShowCursor MOCK called..."); + + if (_privateShowCursorResult) + { + VERIFY_ARE_EQUAL(_expectedShowCursor, show); + } + + return _privateShowCursorResult; + } + + BOOL PrivateAllowCursorBlinking(const bool fEnable) override + { + Log::Comment(L"PrivateAllowCursorBlinking MOCK called..."); + + if (_fPrivateAllowCursorBlinkingResult) + { + VERIFY_ARE_EQUAL(_fEnable, fEnable); + } + + return _fPrivateAllowCursorBlinkingResult; + } + + BOOL FillConsoleOutputCharacterW(const WCHAR wch, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfCharsWritten) noexcept override + { + Log::Comment(L"FillConsoleOutputCharacterW MOCK called..."); + + DWORD dwCharsWritten = 0; + + if (_fFillConsoleOutputCharacterWResult) + { + Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with '%c'...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wch)); + + COORD dwCurrentPos = dwWriteCoord; + + while (dwCharsWritten < nLength) + { + CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X); + pchar->Char.UnicodeChar = wch; + dwCharsWritten++; + _IncrementCoordPos(&dwCurrentPos); + } + } + + numberOfCharsWritten = dwCharsWritten; + + Log::Comment(NoThrowString().Format(L"Fill wrote %d characters.", dwCharsWritten)); + + return _fFillConsoleOutputCharacterWResult; + } + + BOOL FillConsoleOutputAttribute(const WORD wAttribute, const DWORD nLength, const COORD dwWriteCoord, size_t& numberOfAttrsWritten) noexcept override + { + Log::Comment(L"FillConsoleOutputAttribute MOCK called..."); + + DWORD dwCharsWritten = 0; + + if (_fFillConsoleOutputAttributeResult) + { + Log::Comment(NoThrowString().Format(L"Filling (X: %d, Y:%d) for %d characters with 0x%x attribute...", dwWriteCoord.X, dwWriteCoord.Y, nLength, wAttribute)); + + COORD dwCurrentPos = dwWriteCoord; + + while (dwCharsWritten < nLength) + { + CHAR_INFO* pchar = _GetCharAt(dwCurrentPos.Y, dwCurrentPos.X); + pchar->Attributes = wAttribute; + dwCharsWritten++; + _IncrementCoordPos(&dwCurrentPos); + } + } + + numberOfAttrsWritten = dwCharsWritten; + + Log::Comment(NoThrowString().Format(L"Fill modified %d characters.", dwCharsWritten)); + + return _fFillConsoleOutputAttributeResult; + } + + BOOL SetConsoleTextAttribute(const WORD wAttr) override + { + Log::Comment(L"SetConsoleTextAttribute MOCK called..."); + + if (_fSetConsoleTextAttributeResult) + { + VERIFY_ARE_EQUAL(_wExpectedAttribute, wAttr); + _wAttribute = wAttr; + _fUsingRgbColor = false; + } + + return _fSetConsoleTextAttributeResult; + } + + BOOL PrivateSetLegacyAttributes(const WORD wAttr, const bool fForeground, const bool fBackground, const bool fMeta) override + { + Log::Comment(L"PrivateSetLegacyAttributes MOCK called..."); + if (_fPrivateSetLegacyAttributesResult) + { + VERIFY_ARE_EQUAL(_fExpectedForeground, fForeground); + VERIFY_ARE_EQUAL(_fExpectedBackground, fBackground); + VERIFY_ARE_EQUAL(_fExpectedMeta, fMeta); + if (fForeground) + { + WI_UpdateFlagsInMask(_wAttribute, FG_ATTRS, wAttr); + } + if (fBackground) + { + WI_UpdateFlagsInMask(_wAttribute, BG_ATTRS, wAttr); + } + if (fMeta) + { + WI_UpdateFlagsInMask(_wAttribute, META_ATTRS, wAttr); + } + + VERIFY_ARE_EQUAL(_wExpectedAttribute, wAttr); + + _fExpectedForeground = _fExpectedBackground = _fExpectedMeta = false; + } + + return _fPrivateSetLegacyAttributesResult; + } + + BOOL SetConsoleXtermTextAttribute(const int iXtermTableEntry, const bool fIsForeground) override + { + Log::Comment(L"SetConsoleXtermTextAttribute MOCK called..."); + + if (_fSetConsoleXtermTextAttributeResult) + { + VERIFY_ARE_EQUAL(_fExpectedIsForeground, fIsForeground); + _fIsForeground = fIsForeground; + VERIFY_ARE_EQUAL(_iExpectedXtermTableEntry, iXtermTableEntry); + _iXtermTableEntry = iXtermTableEntry; + // if the table entry is less than 16, keep using the legacy attr + _fUsingRgbColor = iXtermTableEntry > 16; + if (!_fUsingRgbColor) + { + //Convert the xterm index to the win index + bool fRed = (iXtermTableEntry & 0x01) > 0; + bool fGreen = (iXtermTableEntry & 0x02) > 0; + bool fBlue = (iXtermTableEntry & 0x04) > 0; + bool fBright = (iXtermTableEntry & 0x08) > 0; + WORD iWinEntry = (fRed ? 0x4 : 0x0) | (fGreen ? 0x2 : 0x0) | (fBlue ? 0x1 : 0x0) | (fBright ? 0x8 : 0x0); + _wAttribute = fIsForeground ? ((_wAttribute & 0xF0) | iWinEntry) + : ((iWinEntry << 4) | (_wAttribute & 0x0F)); + } + } + + return _fSetConsoleXtermTextAttributeResult; + } + + BOOL SetConsoleRGBTextAttribute(const COLORREF rgbColor, const bool fIsForeground) override + { + Log::Comment(L"SetConsoleRGBTextAttribute MOCK called..."); + if (_fSetConsoleRGBTextAttributeResult) + { + VERIFY_ARE_EQUAL(_fExpectedIsForeground, fIsForeground); + _fIsForeground = fIsForeground; + VERIFY_ARE_EQUAL(_ExpectedColor, rgbColor); + _rgbColor = rgbColor; + _fUsingRgbColor = true; + } + + return _fSetConsoleRGBTextAttributeResult; + } + + BOOL PrivateBoldText(const bool isBold) override + { + Log::Comment(L"PrivateBoldText MOCK called..."); + if (_fPrivateBoldTextResult) + { + VERIFY_ARE_EQUAL(_fExpectedIsBold, isBold); + _fIsBold = isBold; + _fExpectedIsBold = false; + } + return !!_fPrivateBoldTextResult; + } + + BOOL PrivateWriteConsoleInputW(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) override + { + Log::Comment(L"PrivateWriteConsoleInputW MOCK called..."); + + if (_fPrivateWriteConsoleInputWResult) + { + // move all the input events we were given into local storage so we can test against them + Log::Comment(NoThrowString().Format(L"Moving %zu input events into local storage...", events.size())); + + _events.clear(); + _events.swap(events); + eventsWritten = _events.size(); + } + + return _fPrivateWriteConsoleInputWResult; + } + + BOOL PrivatePrependConsoleInput(_Inout_ std::deque>& events, + _Out_ size_t& eventsWritten) override + { + Log::Comment(L"PrivatePrependConsoleInput MOCK called..."); + + if (_fPrivatePrependConsoleInputResult) + { + // move all the input events we were given into local storage so we can test against them + Log::Comment(NoThrowString().Format(L"Moving %zu input events into local storage...", events.size())); + + _events.clear(); + _events.swap(events); + eventsWritten = _events.size(); + } + + return _fPrivatePrependConsoleInputResult; + } + + BOOL PrivateWriteConsoleControlInput(_In_ KeyEvent key) override + { + Log::Comment(L"PrivateWriteConsoleControlInput MOCK called..."); + + if (_fPrivateWriteConsoleControlInputResult) + { + VERIFY_ARE_EQUAL('C', key.GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(0x3, key.GetCharData()); + VERIFY_ARE_EQUAL(true, key.IsCtrlPressed()); + } + + return _fPrivateWriteConsoleControlInputResult; + } + + bool _IsInsideClip(const SMALL_RECT* const pClipRectangle, const SHORT iRow, const SHORT iCol) + { + if (pClipRectangle == nullptr) + { + return true; + } + else + { + return iRow >= pClipRectangle->Top && iRow < pClipRectangle->Bottom && iCol >= pClipRectangle->Left && iCol < pClipRectangle->Right; + } + } + + BOOL ScrollConsoleScreenBufferW(const SMALL_RECT* pScrollRectangle, _In_opt_ const SMALL_RECT* pClipRectangle, _In_ COORD dwDestinationOrigin, const CHAR_INFO* pFill) override + { + Log::Comment(L"ScrollConsoleScreenBufferW MOCK called..."); + + if (_fScrollConsoleScreenBufferWResult) + { + if (pClipRectangle != nullptr) + { + Log::Comment(NoThrowString().Format( + L"\tScrolling Rectangle (T: %d, B: %d, L: %d, R: %d) " + L"into new top-left coordinate (X: %d, Y:%d) with Fill ('%c', 0x%x) " + L"clipping to (T: %d, B: %d, L: %d, R: %d)...", + pScrollRectangle->Top, pScrollRectangle->Bottom, pScrollRectangle->Left, pScrollRectangle->Right, + dwDestinationOrigin.X, dwDestinationOrigin.Y, pFill->Char.UnicodeChar, pFill->Attributes, + pClipRectangle->Top, pClipRectangle->Bottom, pClipRectangle->Left, pClipRectangle->Right)); + } + else + { + Log::Comment(NoThrowString().Format( + L"\tScrolling Rectangle (T: %d, B: %d, L: %d, R: %d) " + L"into new top-left coordinate (X: %d, Y:%d) with Fill ('%c', 0x%x) ", + pScrollRectangle->Top, pScrollRectangle->Bottom, pScrollRectangle->Left, pScrollRectangle->Right, + dwDestinationOrigin.X, dwDestinationOrigin.Y, pFill->Char.UnicodeChar, pFill->Attributes)); + } + + // allocate buffer space to hold scrolling rectangle + SHORT width = pScrollRectangle->Right - pScrollRectangle->Left; + SHORT height = pScrollRectangle->Bottom - pScrollRectangle->Top + 1; + size_t const cch = width * height; + CHAR_INFO* const ciBuffer = new CHAR_INFO[cch]; + size_t cciFilled = 0; + + Log::Comment(NoThrowString().Format(L"\tCopy buffer size is %zu chars", cch)); + + for (SHORT iCharY = pScrollRectangle->Top; iCharY <= pScrollRectangle->Bottom; iCharY++) + { + // back up space and fill it with the fill. + for (SHORT iCharX = pScrollRectangle->Left; iCharX < pScrollRectangle->Right; iCharX++) + { + + COORD coordTarget; + coordTarget.X = (SHORT)iCharX; + coordTarget.Y = iCharY; + + CHAR_INFO* const pciStored = _GetCharAt(coordTarget.Y, coordTarget.X); + + // back up to buffer + ciBuffer[cciFilled] = *pciStored; + cciFilled++; + + // fill with fill + if (_IsInsideClip(pClipRectangle, coordTarget.Y, coordTarget.X)) + { + *pciStored = *pFill; + } + } + + } + Log::Comment(NoThrowString().Format(L"\tCopied a total %zu chars", cciFilled)); + Log::Comment(L"\tCopying chars back"); + for (SHORT iCharY = pScrollRectangle->Top; iCharY <= pScrollRectangle->Bottom; iCharY++) + { + // back up space and fill it with the fill. + for (SHORT iCharX = pScrollRectangle->Left; iCharX < pScrollRectangle->Right; iCharX++) + { + COORD coordTarget; + coordTarget.X = dwDestinationOrigin.X + (iCharX - pScrollRectangle->Left); + coordTarget.Y = dwDestinationOrigin.Y + (iCharY - pScrollRectangle->Top); + + CHAR_INFO* const pciStored = _GetCharAt(coordTarget.Y, coordTarget.X); + + if (_IsInsideClip(pClipRectangle, coordTarget.Y, coordTarget.X) && _IsInsideClip(pClipRectangle, iCharY, iCharX)) + { + size_t index = (width) * (iCharY - pScrollRectangle->Top) + (iCharX - pScrollRectangle->Left); + CHAR_INFO charFromBuffer = ciBuffer[index]; + *pciStored = charFromBuffer; + } + } + } + + delete[] ciBuffer; + } + + return _fScrollConsoleScreenBufferWResult; + } + + BOOL PrivateSetScrollingRegion(const SMALL_RECT* const psrScrollMargins) override + { + Log::Comment(L"PrivateSetScrollingRegion MOCK called..."); + + if (_fPrivateSetScrollingRegionResult) + { + VERIFY_ARE_EQUAL(_srExpectedScrollRegion, *psrScrollMargins); + } + + return _fPrivateSetScrollingRegionResult; + } + + BOOL PrivateReverseLineFeed() override + { + Log::Comment(L"PrivateReverseLineFeed MOCK called..."); + // We made it through the adapter, woo! Return true. + return TRUE; + } + + BOOL MoveCursorVertically(const short lines) override + { + Log::Comment(L"MoveCursorVertically MOCK called..."); + if (_fMoveCursorVerticallyResult) + { + VERIFY_ARE_EQUAL(_expectedLines, lines); + _coordCursorPos = { _coordCursorPos.X, _coordCursorPos.Y + lines }; + } + return !!_fMoveCursorVerticallyResult; + } + + BOOL SetConsoleTitleW(const std::wstring_view title) + { + Log::Comment(L"SetConsoleTitleW MOCK called..."); + + if (_fSetConsoleTitleWResult) + { + VERIFY_ARE_EQUAL(_pwchExpectedWindowTitle, title.data()); + VERIFY_ARE_EQUAL(_sCchExpectedTitleLength, title.size()); + } + return TRUE; + } + + BOOL PrivateUseAlternateScreenBuffer() override + { + Log::Comment(L"PrivateUseAlternateScreenBuffer MOCK called..."); + return true; + } + + BOOL PrivateUseMainScreenBuffer() override + { + Log::Comment(L"PrivateUseMainScreenBuffer MOCK called..."); + return true; + } + + BOOL PrivateHorizontalTabSet() override + { + Log::Comment(L"PrivateHorizontalTabSet MOCK called..."); + // We made it through the adapter, woo! Return true. + return TRUE; + } + + BOOL PrivateForwardTab(const SHORT sNumTabs) override + { + Log::Comment(L"PrivateForwardTab MOCK called..."); + if (_fPrivateForwardTabResult) + { + VERIFY_ARE_EQUAL(_sExpectedNumTabs, sNumTabs); + } + return TRUE; + } + + BOOL PrivateBackwardsTab(const SHORT sNumTabs) override + { + Log::Comment(L"PrivateBackwardsTab MOCK called..."); + if (_fPrivateBackwardsTabResult) + { + VERIFY_ARE_EQUAL(_sExpectedNumTabs, sNumTabs); + } + return TRUE; + } + + BOOL PrivateTabClear(const bool fClearAll) override + { + Log::Comment(L"PrivateTabClear MOCK called..."); + if (_fPrivateTabClearResult) + { + VERIFY_ARE_EQUAL(_fExpectedClearAll, fClearAll); + } + return TRUE; + } + + BOOL PrivateSetDefaultTabStops() override + { + Log::Comment(L"PrivateSetDefaultTabStops MOCK called..."); + return TRUE; + } + + BOOL PrivateEnableVT200MouseMode(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableVT200MouseMode MOCK called..."); + if (_fPrivateEnableVT200MouseModeResult) + { + VERIFY_ARE_EQUAL(_fExpectedMouseEnabled, fEnabled); + } + return _fPrivateEnableVT200MouseModeResult; + } + + BOOL PrivateEnableUTF8ExtendedMouseMode(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableUTF8ExtendedMouseMode MOCK called..."); + if (_fPrivateEnableUTF8ExtendedMouseModeResult) + { + VERIFY_ARE_EQUAL(_fExpectedMouseEnabled, fEnabled); + } + return _fPrivateEnableUTF8ExtendedMouseModeResult; + } + + BOOL PrivateEnableSGRExtendedMouseMode(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableSGRExtendedMouseMode MOCK called..."); + if (_fPrivateEnableSGRExtendedMouseModeResult) + { + VERIFY_ARE_EQUAL(_fExpectedMouseEnabled, fEnabled); + } + return _fPrivateEnableSGRExtendedMouseModeResult; + } + + BOOL PrivateEnableButtonEventMouseMode(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableButtonEventMouseMode MOCK called..."); + if (_fPrivateEnableButtonEventMouseModeResult) + { + VERIFY_ARE_EQUAL(_fExpectedMouseEnabled, fEnabled); + } + return _fPrivateEnableButtonEventMouseModeResult; + } + + BOOL PrivateEnableAnyEventMouseMode(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableAnyEventMouseMode MOCK called..."); + if (_fPrivateEnableAnyEventMouseModeResult) + { + VERIFY_ARE_EQUAL(_fExpectedMouseEnabled, fEnabled); + } + return _fPrivateEnableAnyEventMouseModeResult; + } + + BOOL PrivateEnableAlternateScroll(const bool fEnabled) override + { + Log::Comment(L"PrivateEnableAlternateScroll MOCK called..."); + if (_fPrivateEnableAlternateScrollResult) + { + VERIFY_ARE_EQUAL(_fExpectedAlternateScrollEnabled, fEnabled); + } + return _fPrivateEnableAlternateScrollResult; + } + + BOOL PrivateEraseAll() override + { + Log::Comment(L"PrivateEraseAll MOCK called..."); + return TRUE; + } + + BOOL SetCursorStyle(const CursorType cursorType) override + { + Log::Comment(L"SetCursorStyle MOCK called..."); + if (_fSetCursorStyleResult) + { + VERIFY_ARE_EQUAL(_ExpectedCursorStyle, cursorType); + } + return _fSetCursorStyleResult; + } + + BOOL SetCursorColor(const COLORREF cursorColor) override + { + Log::Comment(L"SetCursorColor MOCK called..."); + if (_fSetCursorColorResult) + { + VERIFY_ARE_EQUAL(_ExpectedCursorColor, cursorColor); + } + return _fSetCursorColorResult; + } + + BOOL PrivateGetConsoleScreenBufferAttributes(_Out_ WORD* const pwAttributes) override + { + Log::Comment(L"PrivateGetConsoleScreenBufferAttributes MOCK returning data..."); + + if (pwAttributes != nullptr && _fPrivateGetConsoleScreenBufferAttributesResult) + { + *pwAttributes = _wAttribute; + } + + return _fPrivateGetConsoleScreenBufferAttributesResult; + } + + BOOL PrivateRefreshWindow() override + { + Log::Comment(L"PrivateRefreshWindow MOCK called..."); + // We made it through the adapter, woo! Return true. + return TRUE; + } + + BOOL PrivateSuppressResizeRepaint() override + { + Log::Comment(L"PrivateSuppressResizeRepaint MOCK called..."); + VERIFY_IS_TRUE(false, L"AdaptDispatch should never be calling this function."); + return FALSE; + } + + BOOL GetConsoleOutputCP(_Out_ unsigned int* const puiOutputCP) override + { + Log::Comment(L"GetConsoleOutputCP MOCK called..."); + if (_fGetConsoleOutputCPResult) + { + *puiOutputCP = _uiExpectedOutputCP; + } + return _fGetConsoleOutputCPResult; + } + + BOOL IsConsolePty(_Out_ bool* const isPty) const override + { + Log::Comment(L"IsConsolePty MOCK called..."); + if (_fIsConsolePtyResult) + { + *isPty = _fIsPty; + } + return _fIsConsolePtyResult; + } + + BOOL DeleteLines(const unsigned int /*count*/) override + { + Log::Comment(L"DeleteLines MOCK called..."); + return TRUE; + } + + BOOL InsertLines(const unsigned int /*count*/) override + { + Log::Comment(L"InsertLines MOCK called..."); + return TRUE; + } + + BOOL PrivateSetDefaultAttributes(const bool fForeground, + const bool fBackground) override + { + Log::Comment(L"PrivateSetDefaultAttributes MOCK called..."); + if (_fPrivateSetDefaultAttributesResult) + { + VERIFY_ARE_EQUAL(_fExpectedForeground, fForeground); + VERIFY_ARE_EQUAL(_fExpectedBackground, fBackground); + if (fForeground) + { + WI_UpdateFlagsInMask(_wAttribute, FG_ATTRS, s_wDefaultFill); + } + if (fBackground) + { + WI_UpdateFlagsInMask(_wAttribute, BG_ATTRS, s_wDefaultFill); + } + + _fExpectedForeground = _fExpectedBackground = false; + } + return _fPrivateSetDefaultAttributesResult; + } + + BOOL MoveToBottom() const override + { + Log::Comment(L"MoveToBottom MOCK called..."); + return _fMoveToBottomResult; + } + + BOOL PrivateSetColorTableEntry(const short index, const COLORREF value) const noexcept override + { + Log::Comment(L"PrivateSetColorTableEntry MOCK called..."); + if (_fPrivateSetColorTableEntryResult) + { + VERIFY_ARE_EQUAL(_expectedColorTableIndex, index); + VERIFY_ARE_EQUAL(_expectedColorValue, value); + } + + return _fPrivateSetColorTableEntryResult; + } + + void _IncrementCoordPos(_Inout_ COORD* pcoord) + { + pcoord->X++; + + if (pcoord->X >= _coordBufferSize.X) + { + pcoord->X = 0; + pcoord->Y++; + + if (pcoord->Y >= _coordBufferSize.Y) + { + pcoord->Y = _coordBufferSize.Y - 1; + } + } + } + + void PrepData() + { + PrepData(CursorDirection::UP); // if called like this, the cursor direction doesn't matter. + } + + void PrepData(CursorDirection dir) + { + switch (dir) + { + case CursorDirection::UP: + return PrepData(CursorX::LEFT, CursorY::TOP); + case CursorDirection::DOWN: + return PrepData(CursorX::LEFT, CursorY::BOTTOM); + case CursorDirection::LEFT: + return PrepData(CursorX::LEFT, CursorY::TOP); + case CursorDirection::RIGHT: + return PrepData(CursorX::RIGHT, CursorY::TOP); + case CursorDirection::NEXTLINE: + return PrepData(CursorX::LEFT, CursorY::BOTTOM); + case CursorDirection::PREVLINE: + return PrepData(CursorX::LEFT, CursorY::TOP); + } + } + + void PrepData(CursorX xact, CursorY yact) + { + PrepData(xact, yact, s_wchDefault, s_wDefaultAttribute); + } + + void PrepData(CursorX xact, CursorY yact, WCHAR wch, WORD wAttr) + { + Log::Comment(L"Resetting mock data state."); + + // APIs succeed by default + _fSetConsoleCursorPositionResult = TRUE; + _fGetConsoleScreenBufferInfoExResult = TRUE; + _fGetConsoleCursorInfoResult = TRUE; + _fSetConsoleCursorInfoResult = TRUE; + _fFillConsoleOutputCharacterWResult = TRUE; + _fFillConsoleOutputAttributeResult = TRUE; + _fSetConsoleTextAttributeResult = TRUE; + _fPrivateWriteConsoleInputWResult = TRUE; + _fPrivatePrependConsoleInputResult = TRUE; + _fPrivateWriteConsoleControlInputResult = TRUE; + _fScrollConsoleScreenBufferWResult = TRUE; + _fSetConsoleWindowInfoResult = TRUE; + _fPrivateGetConsoleScreenBufferAttributesResult = TRUE; + _fMoveToBottomResult = true; + + _PrepCharsBuffer(wch, wAttr); + + // Viewport sitting in the "middle" of the buffer somewhere (so all sides have excess buffer around them) + _srViewport.Top = 20; + _srViewport.Bottom = 49; + _srViewport.Left = 30; + _srViewport.Right = 59; + + // Call cursor positions seperately + PrepCursor(xact, yact); + + _dwCursorSize = 33; + _dwExpectedCursorSize = _dwCursorSize; + + _fCursorVisible = TRUE; + _fExpectedCursorVisible = _fCursorVisible; + + // Attribute default is gray on black. + _wAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + _wExpectedAttribute = _wAttribute; + + _expectedLines = 0; + } + + void PrepCursor(CursorX xact, CursorY yact) + { + Log::Comment(L"Adjusting cursor within viewport... Expected will match actual when done."); + + switch (xact) + { + case CursorX::LEFT: + Log::Comment(L"Cursor set to left edge of viewport."); + _coordCursorPos.X = _srViewport.Left; + break; + case CursorX::RIGHT: + Log::Comment(L"Cursor set to right edge of viewport."); + _coordCursorPos.X = _srViewport.Right - 1; + break; + case CursorX::XCENTER: + Log::Comment(L"Cursor set to centered X of viewport."); + _coordCursorPos.X = _srViewport.Left + ((_srViewport.Right - _srViewport.Left) / 2); + break; + } + + switch (yact) + { + case CursorY::TOP: + Log::Comment(L"Cursor set to top edge of viewport."); + _coordCursorPos.Y = _srViewport.Top; + break; + case CursorY::BOTTOM: + Log::Comment(L"Cursor set to bottom edge of viewport."); + _coordCursorPos.Y = _srViewport.Bottom - 1; + break; + case CursorY::YCENTER: + Log::Comment(L"Cursor set to centered Y of viewport."); + _coordCursorPos.Y = _srViewport.Top + ((_srViewport.Bottom - _srViewport.Top) / 2); + break; + } + + _coordExpectedCursorPos = _coordCursorPos; + } + + void _PrepCharsBuffer() + { + _PrepCharsBuffer(s_wchDefault, s_wDefaultAttribute); + } + + void _PrepCharsBuffer(WCHAR const wch, WORD const wAttr) + { + // Buffer large + _coordBufferSize.X = 100; + _coordBufferSize.Y = 600; + + // Buffer data + _FreeCharsBuffer(); + + DWORD const cchTotalBufferSize = _coordBufferSize.Y * _coordBufferSize.X; + + _rgchars = new CHAR_INFO[cchTotalBufferSize]; + + COORD coordStart = { 0 }; + size_t written = 0; + + // Fill buffer with Zs. + Log::Comment(L"Filling buffer with characters so we can tell what's deleted."); + FillConsoleOutputCharacterW(wch, cchTotalBufferSize, coordStart, written); + + // Fill attributes with 0s + Log::Comment(L"Filling buffer with attributes so we can tell what happened."); + FillConsoleOutputAttribute(wAttr, cchTotalBufferSize, coordStart, written); + + VERIFY_ARE_EQUAL(((DWORD)cchTotalBufferSize), ((DWORD)written), L"Ensure the writer says all characters in the buffer were filled."); + } + + void _FreeCharsBuffer() + { + if (_rgchars != nullptr) + { + delete[] _rgchars; + _rgchars = nullptr; + } + } + + void InsertString(COORD coordTarget, PWSTR pwszText, WORD wAttr) + { + Log::Comment(NoThrowString().Format(L"Writing string '%s' to target (X: %d, Y:%d) with color/attr 0x%x", pwszText, coordTarget.X, coordTarget.Y, wAttr)); + + size_t cchModified = 0; + + if (pwszText != nullptr) + { + size_t cch; + if (SUCCEEDED(StringCchLengthW(pwszText, STRSAFE_MAX_LENGTH, &cch))) + { + COORD coordInsertPoint = coordTarget; + + for (size_t i = 0; i < cch; i++) + { + CHAR_INFO* const pci = _GetCharAt(coordInsertPoint.Y, coordInsertPoint.X); + pci->Char.UnicodeChar = pwszText[i]; + pci->Attributes = wAttr; + + _IncrementCoordPos(&coordInsertPoint); + cchModified++; + } + } + } + + Log::Comment(NoThrowString().Format(L"Wrote %zu characters into buffer.", cchModified)); + } + + void FillRectangle(SMALL_RECT srRect, wchar_t wch, WORD wAttr) + { + Log::Comment(NoThrowString().Format(L"Filling area (L: %d, R: %d, T: %d, B: %d) with '%c' in attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, wch, wAttr)); + + size_t cchModified = 0; + + for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) + { + for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) + { + CHAR_INFO* const pci = _GetCharAt(iRow, iCol); + pci->Char.UnicodeChar = wch; + pci->Attributes = wAttr; + + cchModified++; + } + } + + Log::Comment(NoThrowString().Format(L"Filled %zu characters.", cchModified)); + } + + void ValidateInputEvent(_In_ PCWSTR pwszExpectedResponse) + { + size_t const cchResponse = wcslen(pwszExpectedResponse); + size_t const eventCount = _events.size(); + + VERIFY_ARE_EQUAL(cchResponse * 2, eventCount, L"We should receive TWO input records for every character in the expected string. Key down and key up."); + + for (size_t iInput = 0; iInput < eventCount; iInput++) + { + wchar_t const wch = pwszExpectedResponse[iInput / 2]; // the same portion of the string will be used twice. 0/2 = 0. 1/2 = 0. 2/2 = 1. 3/2 = 1. and so on. + + + VERIFY_ARE_EQUAL(InputEventType::KeyEvent, _events[iInput]->EventType()); + + const KeyEvent* const keyEvent = static_cast(_events[iInput].get()); + + // every even key is down. every odd key is up. DOWN = 0, UP = 1. DOWN = 2, UP = 3. and so on. + VERIFY_ARE_EQUAL((bool)!(iInput % 2), keyEvent->IsKeyDown()); + VERIFY_ARE_EQUAL(0u, keyEvent->GetActiveModifierKeys()); + Log::Comment(NoThrowString().Format(L"Comparing '%c' with '%c'...", wch, keyEvent->GetCharData())); + VERIFY_ARE_EQUAL(wch, keyEvent->GetCharData()); + VERIFY_ARE_EQUAL(1u, keyEvent->GetRepeatCount()); + VERIFY_ARE_EQUAL(0u, keyEvent->GetVirtualKeyCode()); + VERIFY_ARE_EQUAL(0u, keyEvent->GetVirtualScanCode()); + } + } + + bool ValidateString(COORD const coordTarget, PCWSTR pwszText, WORD const wAttr) + { + Log::Comment(NoThrowString().Format(L"Validating that the string %s is written starting at (X: %d, Y: %d) with the color/attr 0x%x", pwszText, coordTarget.X, coordTarget.Y, wAttr)); + + bool fSuccess = true; + + if (pwszText != nullptr) + { + size_t cch; + fSuccess = SUCCEEDED(StringCchLengthW(pwszText, STRSAFE_MAX_LENGTH, &cch)); + + if (fSuccess) + { + COORD coordGetPos = coordTarget; + + for (size_t i = 0; i < cch; i++) + { + const CHAR_INFO* const pci = _GetCharAt(coordGetPos.Y, coordGetPos.X); + + const wchar_t wchActual = pci->Char.UnicodeChar; + const wchar_t wchExpected = pwszText[i]; + + fSuccess = wchExpected == wchActual; + + if (!fSuccess) + { + Log::Comment(NoThrowString().Format(L"ValidateString failed char comparison at (X: %d, Y: %d). Expected: '%c' Actual: '%c'", coordGetPos.X, coordGetPos.Y, wchExpected, wchActual)); + break; + } + + const WORD wAttrActual = pci->Attributes; + const WORD wAttrExpected = wAttr; + + if (!fSuccess) + { + Log::Comment(NoThrowString().Format(L"ValidateString failed attr comparison at (X: %d, Y: %d). Expected: '0x%x' Actual: '0x%x'", coordGetPos.X, coordGetPos.Y, wAttrExpected, wAttrActual)); + break; + } + + _IncrementCoordPos(&coordGetPos); + } + } + } + + return fSuccess; + } + + bool ValidateRectangleContains(SMALL_RECT srRect, wchar_t wchExpected, WORD wAttrExpected) + { + Log::Comment(NoThrowString().Format(L"Validating that the area inside (L: %d, R: %d, T: %d, B: %d) char '%c' and attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, wchExpected, wAttrExpected)); + + bool fStateValid = true; + + for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) + { + Log::Comment(NoThrowString().Format(L"Validating row(y=) %d", iRow)); + for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) + { + CHAR_INFO* const pci = _GetCharAt(iRow, iCol); + + fStateValid = pci->Char.UnicodeChar == wchExpected; + if (!fStateValid) + { + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pci->Char.UnicodeChar)); + break; + } + + fStateValid = pci->Attributes == wAttrExpected; + if (!fStateValid) + { + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pci->Attributes)); + } + } + + if (!fStateValid) + { + break; + } + } + + return fStateValid; + } + + bool ValidateRectangleContains(SMALL_RECT srRect, wchar_t wchExpected, WORD wAttrExpected, SMALL_RECT srExcept) + { + bool fStateValid = true; + + Log::Comment(NoThrowString().Format(L"Validating that the area inside (L: %d, R: %d, T: %d, B: %d) but outside (L: %d, R: %d, T: %d, B: %d) contains char '%c' and attr 0x%x", srRect.Left, srRect.Right, srRect.Top, srRect.Bottom, srExcept.Left, srExcept.Right, srExcept.Top, srExcept.Bottom, wchExpected, wAttrExpected)); + + for (SHORT iRow = srRect.Top; iRow < srRect.Bottom; iRow++) + { + for (SHORT iCol = srRect.Left; iCol < srRect.Right; iCol++) + { + if (iRow >= srExcept.Top && iRow < srExcept.Bottom && iCol >= srExcept.Left && iCol < srExcept.Right) + { + // if in exception range, skip comparison. + continue; + } + else + { + CHAR_INFO* const pci = _GetCharAt(iRow, iCol); + + fStateValid = pci->Char.UnicodeChar == wchExpected; + if (!fStateValid) + { + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pci->Char.UnicodeChar)); + break; + } + + fStateValid = pci->Attributes == wAttrExpected; + if (!fStateValid) + { + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pci->Attributes)); + } + } + } + + if (!fStateValid) + { + break; + } + } + + return fStateValid; + } + + bool ValidateEraseBufferState(SMALL_RECT* rgsrRegions, size_t cRegions, wchar_t wchExpectedInRegions, WORD wAttrExpectedInRegions) + { + bool fStateValid = true; + + Log::Comment(NoThrowString().Format(L"The following %zu regions are used as in-bounds for this test:", cRegions)); + for (size_t iRegion = 0; iRegion < cRegions; iRegion++) + { + SMALL_RECT srRegion = rgsrRegions[iRegion]; + + Log::Comment(NoThrowString().Format(L"#%zu - (T: %d, B: %d, L: %d, R:%d)", iRegion, srRegion.Top, srRegion.Bottom, srRegion.Left, srRegion.Right)); + } + + Log::Comment(L"Now checking every character within the buffer..."); + for (short iRow = 0; iRow < _coordBufferSize.Y; iRow++) + { + for (short iCol = 0; iCol < _coordBufferSize.X; iCol++) + { + CHAR_INFO* pchar = _GetCharAt(iRow, iCol); + + bool const fIsInclusive = _IsAnyRegionInclusive(rgsrRegions, cRegions, iRow, iCol); + + WCHAR const wchExpected = fIsInclusive ? wchExpectedInRegions : TestGetSet::s_wchDefault; + + WORD const wAttrExpected = fIsInclusive ? wAttrExpectedInRegions : TestGetSet::s_wDefaultAttribute; + + if (pchar->Char.UnicodeChar != wchExpected) + { + fStateValid = false; + + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected: '%c'. Actual: '%c'", iCol, iRow, wchExpected, pchar->Char.UnicodeChar)); + + break; + } + + if (pchar->Attributes != wAttrExpected) + { + fStateValid = false; + + Log::Comment(NoThrowString().Format(L"Region match failed at (X: %d, Y: %d). Expected Attr: 0x%x. Actual Attr: 0x%x", iCol, iRow, wAttrExpected, pchar->Attributes)); + + break; + } + } + + if (!fStateValid) + { + break; + } + } + + return fStateValid; + } + + bool _IsAnyRegionInclusive(SMALL_RECT* rgsrRegions, size_t cRegions, short sRow, short sCol) + { + bool fIncludesChar = false; + + for (size_t iRegion = 0; iRegion < cRegions; iRegion++) + { + fIncludesChar = _IsInRegionInclusive(rgsrRegions[iRegion], sRow, sCol); + + if (fIncludesChar) + { + break; + } + } + + return fIncludesChar; + } + + bool _IsInRegionInclusive(SMALL_RECT srRegion, short sRow, short sCol) + { + return srRegion.Left <= sCol && + srRegion.Right >= sCol && + srRegion.Top <= sRow && + srRegion.Bottom >= sRow; + + } + + CHAR_INFO* _GetCharAt(size_t const iRow, size_t const iCol) + { + CHAR_INFO* pchar = nullptr; + + if (_rgchars != nullptr) + { + pchar = &(_rgchars[(iRow * _coordBufferSize.X) + iCol]); + } + + if (pchar == nullptr) + { + VERIFY_FAIL(L"Failed to retrieve character position from buffer."); + } + + return pchar; + } + + void _PrepForScroll(ScrollDirection const dir, int const distance) + { + _fExpectedWindowAbsolute = FALSE; + _srExpectedConsoleWindow.Top = (SHORT)distance; + _srExpectedConsoleWindow.Bottom = (SHORT)distance; + _srExpectedConsoleWindow.Left = 0; + _srExpectedConsoleWindow.Right = 0; + if (dir == ScrollDirection::UP) + { + _srExpectedConsoleWindow.Top *= -1; + _srExpectedConsoleWindow.Bottom *= -1; + } + } + + void _SetMarginsHelper(SMALL_RECT* rect, SHORT top, SHORT bottom) + { + rect->Top = top; + rect->Bottom = bottom; + //The rectangle is going to get converted from VT space to conhost space + _srExpectedScrollRegion.Top = (top > 0) ? rect->Top - 1 : rect->Top; + _srExpectedScrollRegion.Bottom = (bottom > 0) ? rect->Bottom - 1 : rect->Bottom; + } + + ~TestGetSet() + { + _FreeCharsBuffer(); + } + + static const WCHAR s_wchErase = (WCHAR)0x20; + static const WCHAR s_wchDefault = L'Z'; + static const WORD s_wAttrErase = FOREGROUND_BLUE | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY; + static const WORD s_wDefaultAttribute = 0; + static const WORD s_wDefaultFill = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; // dark gray on black. + + CHAR_INFO* _rgchars = nullptr; + std::deque> _events; + + COORD _coordBufferSize = { 0, 0 }; + SMALL_RECT _srViewport = { 0, 0, 0, 0 }; + SMALL_RECT _srExpectedConsoleWindow = { 0, 0, 0, 0 }; + COORD _coordCursorPos = { 0, 0 }; + SMALL_RECT _srExpectedScrollRegion = { 0, 0, 0, 0 }; + + DWORD _dwCursorSize = 0; + BOOL _fCursorVisible = false; + + COORD _coordExpectedCursorPos = { 0, 0 }; + DWORD _dwExpectedCursorSize = 0; + BOOL _fExpectedCursorVisible = false; + + WORD _wAttribute = 0; + WORD _wExpectedAttribute = 0; + int _iXtermTableEntry = 0; + int _iExpectedXtermTableEntry = 0; + COLORREF _rgbColor = 0; + COLORREF _ExpectedColor = 0; + bool _fIsForeground = false; + bool _fExpectedIsForeground = false; + bool _fUsingRgbColor = false; + bool _fExpectedForeground = false; + bool _fExpectedBackground = false; + bool _fExpectedMeta = false; + unsigned int _uiExpectedOutputCP = 0; + bool _fIsPty = false; + short _expectedLines = 0; + bool _fPrivateBoldTextResult = false; + bool _fExpectedIsBold = false; + bool _fIsBold = false; + + bool _privateShowCursorResult = false; + bool _expectedShowCursor = false; + + BOOL _fGetConsoleScreenBufferInfoExResult = false; + BOOL _fSetConsoleCursorPositionResult = false; + BOOL _fGetConsoleCursorInfoResult = false; + BOOL _fSetConsoleCursorInfoResult = false; + BOOL _fFillConsoleOutputCharacterWResult = false; + BOOL _fFillConsoleOutputAttributeResult = false; + BOOL _fSetConsoleTextAttributeResult = false; + BOOL _fPrivateWriteConsoleInputWResult = false; + BOOL _fPrivatePrependConsoleInputResult = false; + BOOL _fPrivateWriteConsoleControlInputResult = false; + BOOL _fScrollConsoleScreenBufferWResult = false; + + BOOL _fSetConsoleWindowInfoResult = false; + BOOL _fExpectedWindowAbsolute = false; + BOOL _fSetConsoleScreenBufferInfoExResult = false; + + COORD _coordExpectedScreenBufferSize = { 0, 0 }; + SMALL_RECT _srExpectedScreenBufferViewport{ 0, 0, 0, 0 }; + WORD _wExpectedAttributes = 0; + BOOL _fPrivateSetCursorKeysModeResult = false; + BOOL _fPrivateSetKeypadModeResult = false; + bool _fCursorKeysApplicationMode = false; + bool _fKeypadApplicationMode = false; + BOOL _fPrivateAllowCursorBlinkingResult = false; + bool _fEnable = false; // for cursor blinking + BOOL _fPrivateSetScrollingRegionResult = false; + BOOL _fPrivateReverseLineFeedResult = false; + + BOOL _fSetConsoleTitleWResult = false; + wchar_t* _pwchExpectedWindowTitle = nullptr; + unsigned short _sCchExpectedTitleLength = 0; + BOOL _fPrivateHorizontalTabSetResult = false; + BOOL _fPrivateForwardTabResult = false; + BOOL _fPrivateBackwardsTabResult = false; + SHORT _sExpectedNumTabs = 0; + BOOL _fPrivateTabClearResult = false; + bool _fExpectedClearAll = false; + bool _fExpectedMouseEnabled = false; + bool _fExpectedAlternateScrollEnabled = false; + BOOL _fPrivateEnableVT200MouseModeResult = false; + BOOL _fPrivateEnableUTF8ExtendedMouseModeResult = false; + BOOL _fPrivateEnableSGRExtendedMouseModeResult = false; + BOOL _fPrivateEnableButtonEventMouseModeResult = false; + BOOL _fPrivateEnableAnyEventMouseModeResult = false; + BOOL _fPrivateEnableAlternateScrollResult = false; + BOOL _fSetConsoleXtermTextAttributeResult = false; + BOOL _fSetConsoleRGBTextAttributeResult = false; + BOOL _fPrivateSetLegacyAttributesResult = false; + BOOL _fPrivateGetConsoleScreenBufferAttributesResult = false; + BOOL _fSetCursorStyleResult = false; + CursorType _ExpectedCursorStyle; + BOOL _fSetCursorColorResult = false; + COLORREF _ExpectedCursorColor = 0; + BOOL _fGetConsoleOutputCPResult = false; + BOOL _fIsConsolePtyResult = false; + bool _fMoveCursorVerticallyResult = false; + bool _fPrivateSetDefaultAttributesResult = false; + bool _fMoveToBottomResult = false; + + bool _fPrivateSetColorTableEntryResult = false; + short _expectedColorTableIndex = -1; + COLORREF _expectedColorValue = INVALID_COLOR; + +private: + HANDLE _hCon; +}; + +class DummyAdapter : public AdaptDefaults +{ + void Print(const wchar_t /*wch*/) override + { + } + + void PrintString(_In_reads_(_Param_(2)) const wchar_t* const /*rgwch*/, const size_t /*cch*/) override + { + } + + void Execute(const wchar_t /*wch*/) override + { + } +}; + +class AdapterTest +{ +public: + + TEST_CLASS(AdapterTest); + + TEST_METHOD_SETUP(SetupMethods) + { + bool fSuccess = true; + + _testGetSet = new TestGetSet; + fSuccess = _testGetSet != nullptr; + if (fSuccess) + { + // give AdaptDispatch ownership of _testGetSet + _pDispatch = new AdaptDispatch(_testGetSet, new DummyAdapter); + fSuccess = _pDispatch != nullptr; + } + return fSuccess; + } + + TEST_METHOD_CLEANUP(CleanupMethods) + { + delete _pDispatch; + _testGetSet = nullptr; + return true; + } + + TEST_METHOD(CursorMovementTest) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiDirection", L"{0, 1, 2, 3, 4, 5}") // These values align with the CursorDirection enum class to try all the directions. + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + // Used to switch between the various function options. + typedef bool(AdaptDispatch::*CursorMoveFunc)(unsigned int); + CursorMoveFunc moveFunc = nullptr; + + // Modify variables based on directionality of this test + CursorDirection direction; + unsigned int dir; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDirection", dir)); + direction = (CursorDirection)dir; + + switch (direction) + { + case CursorDirection::UP: + Log::Comment(L"Testing up direction."); + moveFunc = &AdaptDispatch::CursorUp; + break; + case CursorDirection::DOWN: + Log::Comment(L"Testing down direction."); + moveFunc = &AdaptDispatch::CursorDown; + break; + case CursorDirection::RIGHT: + Log::Comment(L"Testing right direction."); + moveFunc = &AdaptDispatch::CursorForward; + break; + case CursorDirection::LEFT: + Log::Comment(L"Testing left direction."); + moveFunc = &AdaptDispatch::CursorBackward; + break; + case CursorDirection::NEXTLINE: + Log::Comment(L"Testing next line direction."); + moveFunc = &AdaptDispatch::CursorNextLine; + break; + case CursorDirection::PREVLINE: + Log::Comment(L"Testing prev line direction."); + moveFunc = &AdaptDispatch::CursorPrevLine; + break; + } + + if (moveFunc == nullptr) + { + VERIFY_FAIL(); + return; + } + + // success cases + // place cursor in top left. moving up is expected to go nowhere (it should get bounded by the viewport) + Log::Comment(L"Test 1: Cursor doesn't move when placed in corner of viewport."); + _testGetSet->PrepData(direction); + + switch (direction) + { + case CursorDirection::UP: + Log::Comment(L"Testing up direction."); + _testGetSet->_expectedLines = -1; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + case CursorDirection::DOWN: + Log::Comment(L"Testing down direction."); + _testGetSet->_expectedLines = 1; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + default: + _testGetSet->_expectedLines = 0; + _testGetSet->_fMoveCursorVerticallyResult = false; + break; + } + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(1)); + + Log::Comment(L"Test 1b: Cursor moves to left of line with next/prev line command when cursor can't move higher/lower."); + + bool fDoTest1b = false; + + switch (direction) + { + case CursorDirection::NEXTLINE: + _testGetSet->PrepData(CursorX::RIGHT, CursorY::BOTTOM); + fDoTest1b = true; + break; + case CursorDirection::PREVLINE: + _testGetSet->PrepData(CursorX::RIGHT, CursorY::TOP); + fDoTest1b = true; + break; + } + + if (fDoTest1b) + { + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(1)); + } + else + { + Log::Comment(L"Test not applicable to direction selected. Skipping."); + } + + // place cursor lower, move up 1. + Log::Comment(L"Test 2: Cursor moves 1 in the correct direction from viewport."); + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + switch (direction) + { + case CursorDirection::UP: + _testGetSet->_coordExpectedCursorPos.Y--; + _testGetSet->_expectedLines = -1; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + case CursorDirection::DOWN: + _testGetSet->_coordExpectedCursorPos.Y++; + _testGetSet->_expectedLines = 1; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + case CursorDirection::RIGHT: + _testGetSet->_coordExpectedCursorPos.X++; + break; + case CursorDirection::LEFT: + _testGetSet->_coordExpectedCursorPos.X--; + break; + case CursorDirection::NEXTLINE: + _testGetSet->_coordExpectedCursorPos.Y++; + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + break; + case CursorDirection::PREVLINE: + _testGetSet->_coordExpectedCursorPos.Y--; + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + break; + } + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(1)); + + // place cursor and move it up too far. It should get bounded by the viewport. + Log::Comment(L"Test 3: Cursor moves and gets stuck at viewport when started away from edges and moved beyond edges."); + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Bottom and right viewports are -1 because those two sides are specified to be 1 outside the viewable area. + + switch (direction) + { + case CursorDirection::UP: + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Top; + _testGetSet->_expectedLines = -100; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + case CursorDirection::DOWN: + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Bottom - 1; + _testGetSet->_expectedLines = 100; + _testGetSet->_fMoveCursorVerticallyResult = true; + break; + case CursorDirection::RIGHT: + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Right - 1; + break; + case CursorDirection::LEFT: + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + break; + case CursorDirection::NEXTLINE: + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Bottom - 1; + break; + case CursorDirection::PREVLINE: + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Top; + break; + } + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(100)); + + // error cases + // give too large an up distance, cursor move should fail, cursor should stay the same. + Log::Comment(L"Test 4: When given invalid (massive) move distance that doesn't fit in a short, call fails and cursor doesn't move."); + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(UINT_MAX)); + VERIFY_ARE_EQUAL(_testGetSet->_coordExpectedCursorPos, _testGetSet->_coordCursorPos); + + // cause short underflow. cursor move should fail. cursor should stay the same. + Log::Comment(L"Test 5: When an over/underflow occurs in cursor math, call fails and cursor doesn't move."); + _testGetSet->PrepData(direction); + + switch (direction) + { + case CursorDirection::UP: + case CursorDirection::PREVLINE: + _testGetSet->_coordCursorPos.Y = -10; + break; + case CursorDirection::DOWN: + case CursorDirection::NEXTLINE: + _testGetSet->_coordCursorPos.Y = 10; + break; + case CursorDirection::RIGHT: + _testGetSet->_coordCursorPos.X = 10; + break; + case CursorDirection::LEFT: + _testGetSet->_coordCursorPos.X = -10; + break; + } + + _testGetSet->_coordExpectedCursorPos = _testGetSet->_coordCursorPos; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(SHRT_MAX + 1)); + VERIFY_ARE_EQUAL(_testGetSet->_coordExpectedCursorPos, _testGetSet->_coordCursorPos); + + // SetConsoleCursorPosition throws failure. Parameters are otherwise normal. + Log::Comment(L"Test 6: When SetConsoleCursorPosition throws a failure, call fails and cursor doesn't move."); + _testGetSet->PrepData(direction); + _testGetSet->_fSetConsoleCursorPositionResult = FALSE; + _testGetSet->_fMoveCursorVerticallyResult = false; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(0)); + VERIFY_ARE_EQUAL(_testGetSet->_coordExpectedCursorPos, _testGetSet->_coordCursorPos); + + // GetConsoleScreenBufferInfo throws failure. Parameters are otherwise normal. + Log::Comment(L"Test 7: When GetConsoleScreenBufferInfo throws a failure, call fails and cursor doesn't move."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + _testGetSet->_fGetConsoleScreenBufferInfoExResult = FALSE; + _testGetSet->_fMoveCursorVerticallyResult = true; + Log::Comment(NoThrowString().Format( + L"Cursor Up and Down don't need GetConsoleScreenBufferInfoEx, so they will succeed" + )); + if (direction == CursorDirection::UP || direction == CursorDirection::DOWN) + { + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(0)); + } + else + { + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(0)); + } + VERIFY_ARE_EQUAL(_testGetSet->_coordExpectedCursorPos, _testGetSet->_coordCursorPos); + } + + TEST_METHOD(CursorPositionTest) + { + Log::Comment(L"Starting test..."); + + + Log::Comment(L"Test 1: Place cursor within the viewport. Start from top left, move to middle."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + short sCol = (_testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left) / 2; + short sRow = (_testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top) / 2; + + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left + (sCol - 1); + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Top + (sRow - 1); + + VERIFY_IS_TRUE(_pDispatch->CursorPosition(sRow, sCol)); + + Log::Comment(L"Test 2: Move to 0, 0 (which is 1,1 in VT speak)"); + _testGetSet->PrepData(CursorX::RIGHT, CursorY::BOTTOM); + + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Left; + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Top; + + VERIFY_IS_TRUE(_pDispatch->CursorPosition(1, 1)); + + Log::Comment(L"Test 3: Move beyond rectangle (down/right too far). Should be bounded back in."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + sCol = (_testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left) * 2; + sRow = (_testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top) * 2; + + _testGetSet->_coordExpectedCursorPos.X = _testGetSet->_srViewport.Right - 1; + _testGetSet->_coordExpectedCursorPos.Y = _testGetSet->_srViewport.Bottom - 1; + + VERIFY_IS_TRUE(_pDispatch->CursorPosition(sRow, sCol)); + + Log::Comment(L"Test 4: Values too large for short. Cursor shouldn't move. Return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + VERIFY_IS_FALSE(_pDispatch->CursorPosition(UINT_MAX, UINT_MAX)); + + Log::Comment(L"Test 5: Overflow during addition. Cursor shouldn't move. Return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_srViewport.Left = SHRT_MAX; + _testGetSet->_srViewport.Top = SHRT_MAX; + + VERIFY_IS_FALSE(_pDispatch->CursorPosition(5, 5)); + + Log::Comment(L"Test 6: GetConsoleInfo API returns false. No move, return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_fGetConsoleScreenBufferInfoExResult = FALSE; + + VERIFY_IS_FALSE(_pDispatch->CursorPosition(1, 1)); + + Log::Comment(L"Test 7: SetCursor API returns false. No move, return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_fSetConsoleCursorPositionResult = FALSE; + + VERIFY_IS_FALSE(_pDispatch->CursorPosition(1, 1)); + + Log::Comment(L"Test 8: Move to 0,0. Cursor shouldn't move. Return false. 1,1 is the top left corner in VT100 speak. 0,0 isn't a position. The parser will give 1 for a 0 input."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + VERIFY_IS_FALSE(_pDispatch->CursorPosition(0, 0)); + + } + + TEST_METHOD(CursorSingleDimensionMoveTest) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiDirection", L"{0, 1}") // These values align with the CursorDirection enum class to try all the directions. + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + + //// Used to switch between the various function options. + typedef bool(AdaptDispatch::*CursorMoveFunc)(unsigned int); + CursorMoveFunc moveFunc = nullptr; + SHORT* psViewportEnd = nullptr; + SHORT* psViewportStart = nullptr; + SHORT* psCursorExpected = nullptr; + + // Modify variables based on directionality of this test + AbsolutePosition direction; + unsigned int dir; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDirection", dir)); + direction = (AbsolutePosition)dir; + + switch (direction) + { + case AbsolutePosition::CursorHorizontal: + Log::Comment(L"Testing cursor horizontal movement."); + psViewportEnd = &_testGetSet->_srViewport.Right; + psViewportStart = &_testGetSet->_srViewport.Left; + psCursorExpected = &_testGetSet->_coordExpectedCursorPos.X; + moveFunc = &AdaptDispatch::CursorHorizontalPositionAbsolute; + break; + case AbsolutePosition::VerticalLine: + Log::Comment(L"Testing vertical line movement."); + psViewportEnd = &_testGetSet->_srViewport.Bottom; + psViewportStart = &_testGetSet->_srViewport.Top; + psCursorExpected = &_testGetSet->_coordExpectedCursorPos.Y; + moveFunc = &AdaptDispatch::VerticalLinePositionAbsolute; + break; + } + + if (moveFunc == nullptr || psViewportEnd == nullptr || psViewportStart == nullptr || psCursorExpected == nullptr) + { + VERIFY_FAIL(); + return; + } + + Log::Comment(L"Test 1: Place cursor within the viewport. Start from top left, move to middle."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + short sVal = (*psViewportEnd - *psViewportStart) / 2; + + *psCursorExpected = *psViewportStart + (sVal - 1); + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 2: Move to 0 (which is 1 in VT speak)"); + _testGetSet->PrepData(CursorX::RIGHT, CursorY::BOTTOM); + + *psCursorExpected = *psViewportStart; + sVal = 1; + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 3: Move beyond rectangle (down/right too far). Should be bounded back in."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + sVal = (*psViewportEnd - *psViewportStart) * 2; + + *psCursorExpected = *psViewportEnd - 1; + + VERIFY_IS_TRUE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 4: Values too large for short. Cursor shouldn't move. Return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + sVal = SHORT_MAX; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 5: Overflow during addition. Cursor shouldn't move. Return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_srViewport.Left = SHRT_MAX; + + sVal = 5; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 6: GetConsoleInfo API returns false. No move, return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_fGetConsoleScreenBufferInfoExResult = FALSE; + + sVal = 1; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 7: SetCursor API returns false. No move, return false."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + _testGetSet->_fSetConsoleCursorPositionResult = FALSE; + + sVal = 1; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(sVal)); + + Log::Comment(L"Test 8: Move to 0. Cursor shouldn't move. Return false. 1 is the left edge in VT100 speak. 0 isn't a position. The parser will give 1 for a 0 input."); + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + + sVal = 0; + + VERIFY_IS_FALSE((_pDispatch->*(moveFunc))(sVal)); + } + + TEST_METHOD(CursorSaveRestoreTest) + { + Log::Comment(L"Starting test..."); + + + COORD coordExpected = { 0 }; + + Log::Comment(L"Test 1: Restore with no saved data should move to top-left corner, the null/default position."); + + // Move cursor to top left and save off expected position. + _testGetSet->PrepData(CursorX::LEFT, CursorY::TOP); + coordExpected = _testGetSet->_coordExpectedCursorPos; + + // Then move cursor to the middle and reset the expected to the top left. + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _testGetSet->_coordExpectedCursorPos = coordExpected; + + VERIFY_IS_TRUE(_pDispatch->CursorRestorePosition(), L"By default, restore to top left corner (0,0 offset from viewport)."); + + Log::Comment(L"Test 2: Place cursor in center. Save. Move cursor to corner. Restore. Should come back to center."); + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + VERIFY_IS_TRUE(_pDispatch->CursorSavePosition(), L"Succeed at saving position."); + + Log::Comment(L"Backup expected cursor (in the middle). Move cursor to corner. Then re-set expected cursor to middle."); + // save expected cursor position + coordExpected = _testGetSet->_coordExpectedCursorPos; + + // adjust cursor to corner + _testGetSet->PrepData(CursorX::LEFT, CursorY::BOTTOM); + + // restore expected cursor position to center. + _testGetSet->_coordExpectedCursorPos = coordExpected; + + VERIFY_IS_TRUE(_pDispatch->CursorRestorePosition(), L"Restoring to corner should succeed. API call inside will test that cursor matched expected position."); + } + + TEST_METHOD(CursorHideShowTest) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:fStartingVis", L"{TRUE, FALSE}") + TEST_METHOD_PROPERTY(L"Data:fEndingVis", L"{TRUE, FALSE}") + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + // Modify variables based on permutations of this test. + bool fStart; + bool fEnd; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fStartingVis", fStart)); + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fEndingVis", fEnd)); + + Log::Comment(L"Test 1: Verify successful API call modifies visibility state."); + _testGetSet->PrepData(); + _testGetSet->_fCursorVisible = fStart; + _testGetSet->_privateShowCursorResult = true; + _testGetSet->_expectedShowCursor = fEnd; + VERIFY_IS_TRUE(_pDispatch->CursorVisibility(fEnd)); + + Log::Comment(L"Test 3: When we fail to set updated cursor information, the dispatch should fail."); + _testGetSet->PrepData(); + _testGetSet->_privateShowCursorResult = false; + VERIFY_IS_FALSE(_pDispatch->CursorVisibility(fEnd)); + } + + TEST_METHOD(InsertCharacterTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: The big one. Fill the buffer with Qs. Fill the window with Rs. Write a line of ABCDE at the cursor. Then insert 5 spaces at the cursor. Watch spaces get inserted, ABCDE slide right eating up the Rs in the viewport but not modifying the Qs outside."); + + // place the cursor in the center. + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Save the cursor position. It shouldn't move for the rest of the test. + COORD coordCursorExpected = _testGetSet->_coordCursorPos; + + // Fill the entire buffer with Qs. Blue on Green. + WCHAR const wchOuterBuffer = 'Q'; + WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; + SMALL_RECT srOuterBuffer; + srOuterBuffer.Top = 0; + srOuterBuffer.Left = 0; + srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; + srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + + // Fill the viewport with Rs. Red on Blue. + WCHAR const wchViewport = 'R'; + WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; + SMALL_RECT srViewport = _testGetSet->_srViewport; + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // fill some of the text right of the cursor so we can verify it moved it and didn't overwrite it. + // change the color too so we can make sure that it's fine + + WORD const wAttrTestText = FOREGROUND_GREEN; + PWSTR const pwszTestText = L"ABCDE"; + size_t cchTestText = wcslen(pwszTestText); + SMALL_RECT srTestText; + srTestText.Top = _testGetSet->_coordCursorPos.Y; + srTestText.Bottom = srTestText.Top + 1; + srTestText.Left = _testGetSet->_coordCursorPos.X; + srTestText.Right = srTestText.Left + (SHORT)cchTestText; + _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); + + WCHAR const wchInsertExpected = L' '; + WORD const wAttrInsertExpected = _testGetSet->_wAttribute; + size_t const cchInsertSize = 5; + SMALL_RECT srInsertExpected; + srInsertExpected.Top = _testGetSet->_coordCursorPos.Y; + srInsertExpected.Bottom = srInsertExpected.Top + 1; + srInsertExpected.Left = _testGetSet->_coordCursorPos.X; + srInsertExpected.Right = srInsertExpected.Left + (SHORT)cchInsertSize; + + // the text we inserted is going to move right by the insert size, so adjust that rectangle right. + srTestText.Left += cchInsertSize; + srTestText.Right += cchInsertSize; + + // insert out 5 spots. this should clear them out with spaces and the default fill from the original cursor position + VERIFY_IS_TRUE(_pDispatch->InsertCharacter(cchInsertSize), L"Verify insert call was sucessful."); + + // the combined area of the letters + the spaces will be 10 characters wide: + SMALL_RECT srModifiedSpace; + srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; + srModifiedSpace.Bottom = srModifiedSpace.Top + 1; + srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; + srModifiedSpace.Right = srModifiedSpace.Left + (SHORT)cchInsertSize + (SHORT)cchTestText; + + // verify cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // e.g. we had this in the buffer: QQQRRRRRRABCDERRRRRRRQQQ with the cursor on the A. + // now we should have this buffer: QQQRRRRRR ABCDERRQQQ with the cursor on the first space. + + // Verify the field of Qs didn't change outside the viewport. + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Verify the field of Rs within the viewport not including the inserted range and the ABCDE shifted right. (10 characters) + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // Verify the 5 spaces inserted from the cursor. + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srInsertExpected, wchInsertExpected, wAttrInsertExpected), L"Spaces should be inserted with the proper attributes at the cursor."); + + // Verify the ABCDE sequence was shifted right. + COORD coordTestText; + coordTestText.X = srTestText.Left; + coordTestText.Y = srTestText.Top; + VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"Inserted string should have moved to the right by the number of spaces inserted, attributes and text preserved."); + + // Test case needed for exact end of line (and full line) insert/delete lengths + Log::Comment(L"Test 2: Inserting at the exact end of the line. Same field of Qs and Rs. Move cursor to right edge of window and insert > 1 space. Only 1 should be inserted, everything else unchanged."); + + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // move cursor to right edge + _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Right - 1; + coordCursorExpected = _testGetSet->_coordCursorPos; + + // the rectangle where the space should be is exactly the size of the cursor. + srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; + srModifiedSpace.Bottom = srModifiedSpace.Top + 1; + srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; + srModifiedSpace.Right = srModifiedSpace.Left + 1; + + // insert out 5 spots. this should clear them out with spaces and the default fill from the original cursor position + VERIFY_IS_TRUE(_pDispatch->InsertCharacter(cchInsertSize), L"Verify insert call was sucessful."); + + // cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // Qs are the same outside the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Entire viewport is Rs except the one space spot + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // The 5 inserted spaces at the right edge resulted in 1 space at the right edge + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchInsertExpected, wAttrInsertExpected), L"A space was inserted at the cursor position. All extra spaces were discarded as they hit the right boundary."); + + Log::Comment(L"Test 3: Inserting at the exact beginning of the line. Same field of Qs and Rs. Move cursor to left edge of window and insert > screen width of space. The whole row should be spaces but nothing outside the viewport should be changed."); + + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // move cursor to left edge + _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Left; + coordCursorExpected = _testGetSet->_coordCursorPos; + + // the rectangle of spaces should be the entire line at the cursor. + srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; + srModifiedSpace.Bottom = srModifiedSpace.Top + 1; + srModifiedSpace.Left = _testGetSet->_srViewport.Left; + srModifiedSpace.Right = _testGetSet->_srViewport.Right; + + // insert greater than the entire viewport (the entire buffer width) at the cursor position + VERIFY_IS_TRUE(_pDispatch->InsertCharacter(_testGetSet->_coordBufferSize.X), L"Verify insert call was successful."); + + // cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // Qs are the same outside the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Entire viewport is Rs except the one space spot + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // The inserted spaces at the left edge resulted in an entire line of spaces bounded by the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchInsertExpected, wAttrInsertExpected), L"A whole line of spaces was inserted at the cursor position. All extra spaces were discarded as they hit the right boundary."); + } + + TEST_METHOD(DeleteCharacterTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: The big one. Fill the buffer with Qs. Fill the window with Rs. Write a line of ABCDE at the cursor. Then insert 5 spaces at the cursor. Watch spaces get inserted, ABCDE slide right eating up the Rs in the viewport but not modifying the Qs outside."); + + // place the cursor in the center. + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Save the cursor position. It shouldn't move for the rest of the test. + COORD coordCursorExpected = _testGetSet->_coordCursorPos; + + // Fill the entire buffer with Qs. Blue on Green. + WCHAR const wchOuterBuffer = 'Q'; + WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; + SMALL_RECT srOuterBuffer; + srOuterBuffer.Top = 0; + srOuterBuffer.Left = 0; + srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; + srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + + // Fill the viewport with Rs. Red on Blue. + WCHAR const wchViewport = 'R'; + WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; + SMALL_RECT srViewport = _testGetSet->_srViewport; + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // fill some of the text right of the cursor so we can verify it moved it and wasn't deleted + // change the color too so we can make sure that it's fine + WORD const wAttrTestText = FOREGROUND_GREEN; + PWSTR const pwszTestText = L"ABCDE"; + size_t cchTestText = wcslen(pwszTestText); + SMALL_RECT srTestText; + srTestText.Top = _testGetSet->_coordCursorPos.Y; + srTestText.Bottom = srTestText.Top + 1; + srTestText.Left = _testGetSet->_coordCursorPos.X; + srTestText.Right = srTestText.Left + (SHORT)cchTestText; + _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); + + // We're going to delete "in" from the right edge, so set up that rectangle. + WCHAR const wchDeleteExpected = L' '; + WORD const wAttrDeleteExpected = _testGetSet->_wAttribute; + size_t const cchDeleteSize = 5; + SMALL_RECT srDeleteExpected; + srDeleteExpected.Top = _testGetSet->_coordCursorPos.Y; + srDeleteExpected.Bottom = srDeleteExpected.Top + 1; + srDeleteExpected.Right = _testGetSet->_srViewport.Right; + srDeleteExpected.Left = srDeleteExpected.Right - cchDeleteSize; + + // We want the ABCDE to shift left when we delete and onto the cursor. So move the cursor left 5 and adjust the srTestText rectangle left 5 to the new + // final destination of where they will be after the delete operation occurs. + _testGetSet->_coordCursorPos.X -= cchDeleteSize; + coordCursorExpected = _testGetSet->_coordCursorPos; + srTestText.Left -= cchDeleteSize; + srTestText.Right -= cchDeleteSize; + + + // delete out 5 spots. this should shift the ABCDE text left by 5 and insert 5 spaces at the end of the line + VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(cchDeleteSize), L"Verify delete call was sucessful."); + + // we're going to have ABCDERRRRRRRRRRRRR QQQQQQQ + // since this is a bit more complicated than the insert case, make this the "special" region and exempt it from the bulk "R" check + // we'll check the inside of this rect in 3 pieces, for the ABCDE, then for the inner Rs, then for the 5 spaces after. + SMALL_RECT srSpecialSpace; + srSpecialSpace.Top = _testGetSet->_coordCursorPos.Y; + srSpecialSpace.Bottom = srSpecialSpace.Top + 1; + srSpecialSpace.Left = _testGetSet->_coordCursorPos.X; + srSpecialSpace.Right = _testGetSet->_srViewport.Right; + + SMALL_RECT srGap; // gap space is the Rs between ABCDE and the spaces shifted in from the right + srGap.Left = srTestText.Right; + srGap.Right = srDeleteExpected.Left; + srGap.Top = _testGetSet->_coordCursorPos.Y; + srGap.Bottom = srGap.Top + 1; + + // verify cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // e.g. we had this in the buffer: QQQRRRRRR-RRRRABCDERRQQQ with the cursor on the -. + // now we should have this buffer: QQQRRRRRRABCDERR QQQ with the cursor on the A. + + // Verify the field of Qs didn't change outside the viewport. + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Verify the field of Rs within the viewport not including the special range of the ABCDE, the spaces shifted in from the right, and the Rs between them that went along for the ride. + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srSpecialSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // Verify the 5 spaces shifted in from the right edge due to the delete + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srDeleteExpected, wchDeleteExpected, wAttrDeleteExpected), L"Spaces should be inserted with the proper attributes from the right end of this line (viewport edge.)"); + + // Verify the ABCDE sequence was shifted left by 5 toward the cursor. + COORD coordTestText; + coordTestText.X = srTestText.Left; + coordTestText.Y = srTestText.Top; + VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"Inserted string should have moved to the left by the number of deletes, attributes and text preserved."); + + // Verify the field of Rs between the ABCDE and the spaces shifted in from the right + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srGap, wchViewport, wAttrViewport), L"Viewport Rs should be preserved/shifted left in between the ABCDE and the spaces that came in from the right edge."); + + // Test case needed for exact end of line (and full line) insert/delete lengths + Log::Comment(L"Test 2: Deleting at the exact end of the line. Same field of Qs and Rs. Move cursor to right edge of window and delete > 1 space. Only 1 should be inserted from the right edge (delete inserts from the right), everything else unchanged."); + + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // move cursor to right edge + _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Right - 1; + coordCursorExpected = _testGetSet->_coordCursorPos; + + // the rectangle where the space should be is exactly the size of the cursor. + SMALL_RECT srModifiedSpace; + srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; + srModifiedSpace.Bottom = srModifiedSpace.Top + 1; + srModifiedSpace.Left = _testGetSet->_coordCursorPos.X; + srModifiedSpace.Right = srModifiedSpace.Left + 1; + + // delete out 5 spots. this should clear them out with spaces and the default fill from the original cursor position + VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(cchDeleteSize), L"Verify delete call was sucessful."); + + // cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from delete operation."); + + // Qs are the same outside the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Entire viewport is Rs except the one space spot + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // The 5 deleted spaces at the right edge resulted in 1 space at the right edge + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchDeleteExpected, wAttrDeleteExpected), L"A space was inserted at the cursor position. All extra spaces deleted in from the right continued to cover that one space."); + + Log::Comment(L"Test 3: Deleting at the exact beginning of the line. Same field of Qs and Rs. Move cursor to left edge of window and delete > screen width of space. The whole row should be spaces but nothing outside the viewport should be changed."); + + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // move cursor to left edge + _testGetSet->_coordCursorPos.X = _testGetSet->_srViewport.Left; + coordCursorExpected = _testGetSet->_coordCursorPos; + + // the rectangle of spaces should be the entire line at the cursor. + srModifiedSpace.Top = _testGetSet->_coordCursorPos.Y; + srModifiedSpace.Bottom = srModifiedSpace.Top + 1; + srModifiedSpace.Left = _testGetSet->_srViewport.Left; + srModifiedSpace.Right = _testGetSet->_srViewport.Right; + + // delete greater than the entire viewport (the entire buffer width) at the cursor position + VERIFY_IS_TRUE(_pDispatch->DeleteCharacter(_testGetSet->_coordBufferSize.X), L"Verify delete call was successful."); + + // cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // Qs are the same outside the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), L"Field of Qs outside viewport should remain unchanged."); + + // Entire viewport is Rs except the one space spot + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srViewport, wchViewport, wAttrViewport, srModifiedSpace), L"Field of Rs in the viewport outside modified space should remain unchanged."); + + // The inserted spaces at the left edge resulted in an entire line of spaces bounded by the viewport + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srModifiedSpace, wchDeleteExpected, wAttrDeleteExpected), L"A whole line of spaces was inserted from the right (the cursor position was deleted enough times.) Extra deletes just covered up some of the spaces that were shifted in."); + } + + // Ensures that EraseScrollback (^[[3J) deletes any content from the buffer + // above the viewport, and moves the contents of the buffer in the + // viewport to 0,0. This emulates the xterm behavior of clearing any + // scrollback content. + TEST_METHOD(EraseScrollbackTests) + { + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _testGetSet->_wAttribute = _testGetSet->s_wAttrErase; + Log::Comment(L"Starting Test"); + + _testGetSet->_fSetConsoleWindowInfoResult = true; + _testGetSet->_fExpectedWindowAbsolute = true; + SMALL_RECT srRegion = { 0 }; + srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1; + srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1; + _testGetSet->_srExpectedConsoleWindow = srRegion; + + // The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0 + const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left, + _testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top }; + _testGetSet->_coordExpectedCursorPos = coordRelativeCursor; + + VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); + + // There are two portions of the screen that are cleared - + // below the viewport and to the right of the viewport. + size_t cRegionsToCheck = 2; + SMALL_RECT rgsrRegionsModified[2]; + + // Region 0 - Below the viewport + srRegion.Top = _testGetSet->_srViewport.Bottom + 1; + srRegion.Left = 0; + + srRegion.Bottom = _testGetSet->_coordBufferSize.Y; + srRegion.Right = _testGetSet->_coordBufferSize.X; + + rgsrRegionsModified[0] = srRegion; + + // Region 1 - To the right of the viewport + srRegion.Top = 0; + srRegion.Left = _testGetSet->_srViewport.Right + 1; + + srRegion.Bottom = _testGetSet->_coordBufferSize.Y; + srRegion.Right = _testGetSet->_coordBufferSize.X; + + rgsrRegionsModified[1] = srRegion; + + // Scan entire buffer and ensure only the necessary region has changed. + bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase); + VERIFY_IS_TRUE(fRegionSuccess); + + Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); + _testGetSet->PrepData(); + _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; + + VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); + + Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); + _testGetSet->PrepData(); + _testGetSet->_fFillConsoleOutputCharacterWResult = false; + + VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(DispatchTypes::EraseType::Scrollback)); + } + + TEST_METHOD(EraseTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiEraseType", L"{0, 1, 2}") // corresponds to options in DispatchTypes::EraseType + TEST_METHOD_PROPERTY(L"Data:fEraseScreen", L"{FALSE, TRUE}") // corresponds to Line (FALSE) or Screen (TRUE) + END_TEST_METHOD_PROPERTIES() + + // Modify variables based on type of this test + DispatchTypes::EraseType eraseType; + unsigned int uiEraseType; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiEraseType", uiEraseType)); + eraseType = (DispatchTypes::EraseType)uiEraseType; + + bool fEraseScreen; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fEraseScreen", fEraseScreen)); + + Log::Comment(L"Starting test..."); + + // This combiniation is a simple VT api call + // Verify that the adapter calls that function, and do nothing else. + // This functionality is covered by ScreenBufferTests::EraseAllTests + if (eraseType == DispatchTypes::EraseType::All && fEraseScreen) + { + Log::Comment(L"Testing Erase in Display - All"); + VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType)); + return; + } + + Log::Comment(L"Test 1: Perform standard erase operation."); + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + Log::Comment(L"Erasing line from beginning to cursor."); + break; + case DispatchTypes::EraseType::ToEnd: + Log::Comment(L"Erasing line from cursor to end."); + break; + case DispatchTypes::EraseType::All: + Log::Comment(L"Erasing all."); + break; + default: + VERIFY_FAIL(L"Unsupported erase type."); + } + + if (!fEraseScreen) + { + Log::Comment(L"Erasing just one line (the cursor's line)."); + } + else + { + Log::Comment(L"Erasing entire display (viewport). May be bounded by the cursor."); + } + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _testGetSet->_wAttribute = _testGetSet->s_wAttrErase; + + if (!fEraseScreen) + { + VERIFY_IS_TRUE(_pDispatch->EraseInLine(eraseType)); + } + else + { + VERIFY_IS_TRUE(_pDispatch->EraseInDisplay(eraseType)); + } + + // Will be always the region of the cursor line (minimum 1) + // and 2 more if it's the display (for the regions before and after the cursor line, total 3) + SMALL_RECT rgsrRegionsModified[3]; // max of 3 regions. + + // Determine selection rectangle for line containing the cursor. + // All sides are inclusive of modified data. (unlike viewport normally) + SMALL_RECT srRegion = { 0 }; + srRegion.Top = _testGetSet->_coordCursorPos.Y; + srRegion.Bottom = srRegion.Top; + + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + case DispatchTypes::EraseType::All: + srRegion.Left = _testGetSet->_srViewport.Left; + break; + case DispatchTypes::EraseType::ToEnd: + srRegion.Left = _testGetSet->_coordCursorPos.X; + break; + default: + VERIFY_FAIL(L"Unsupported erase type."); + break; + } + + switch (eraseType) + { + case DispatchTypes::EraseType::FromBeginning: + srRegion.Right = _testGetSet->_coordCursorPos.X; + break; + case DispatchTypes::EraseType::All: + case DispatchTypes::EraseType::ToEnd: + srRegion.Right = _testGetSet->_srViewport.Right - 1; + break; + default: + VERIFY_FAIL(L"Unsupported erase type."); + break; + } + rgsrRegionsModified[0] = srRegion; + + size_t cRegionsToCheck = 1; // start with 1 region to check from the line above. We may add up to 2 more. + + // Need to calculate up to two more regions if this is a screen erase. + if (fEraseScreen) + { + // If from beginning or all, add the region *before* the cursor line. + if (eraseType == DispatchTypes::EraseType::FromBeginning || + eraseType == DispatchTypes::EraseType::All) + { + srRegion.Left = _testGetSet->_srViewport.Left; + srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive on the right. this test is inclusive so -1. + srRegion.Top = _testGetSet->_srViewport.Top; + + srRegion.Bottom = _testGetSet->_coordCursorPos.Y - 1; // this might end up being above top. This will be checked below. + + // Only add it if this is still valid. + if (srRegion.Bottom >= srRegion.Top) + { + rgsrRegionsModified[cRegionsToCheck] = srRegion; + cRegionsToCheck++; + } + } + + // If from end or all, add the region *after* the cursor line. + if (eraseType == DispatchTypes::EraseType::ToEnd || + eraseType == DispatchTypes::EraseType::All) + { + srRegion.Left = _testGetSet->_srViewport.Left; + srRegion.Right = _testGetSet->_srViewport.Right - 1; // viewport is exclusive rectangle on the right. this test uses inclusive rectangles so -1. + srRegion.Bottom = _testGetSet->_srViewport.Bottom - 1; // viewport is exclusive rectangle on the bottom. this test uses inclusive rectangles so -1; + + srRegion.Top = _testGetSet->_coordCursorPos.Y + 1; // this might end up being below bottom. This will be checked below. + + // Only add it if this is still valid. + if (srRegion.Bottom >= srRegion.Top) + { + rgsrRegionsModified[cRegionsToCheck] = srRegion; + cRegionsToCheck++; + } + } + } + + // Scan entire buffer and ensure only the necessary region has changed. + bool fRegionSuccess = _testGetSet->ValidateEraseBufferState(rgsrRegionsModified, cRegionsToCheck, TestGetSet::s_wchErase, TestGetSet::s_wAttrErase); + VERIFY_IS_TRUE(fRegionSuccess); + + Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); + _testGetSet->PrepData(); + _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; + + if (!fEraseScreen) + { + VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType)); + } + else + { + VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType)); + } + + Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); + _testGetSet->PrepData(); + _testGetSet->_fFillConsoleOutputCharacterWResult = false; + + if (!fEraseScreen) + { + VERIFY_IS_FALSE(_pDispatch->EraseInLine(eraseType)); + } + else + { + VERIFY_IS_FALSE(_pDispatch->EraseInDisplay(eraseType)); + } + } + + TEST_METHOD(GraphicsBaseTests) + { + Log::Comment(L"Starting test..."); + + + Log::Comment(L"Test 1: Send no options."); + + _testGetSet->PrepData(); + + DispatchTypes::GraphicsOptions rgOptions[16]; + size_t cOptions = 0; + + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Test 2: Gracefully fail when getting buffer information fails."); + + _testGetSet->PrepData(); + _testGetSet->_fPrivateGetConsoleScreenBufferAttributesResult = FALSE; + + VERIFY_IS_FALSE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Test 3: Gracefully fail when setting attribute data fails."); + + _testGetSet->PrepData(); + _testGetSet->_fSetConsoleTextAttributeResult = FALSE; + // Need at least one option in order for the call to be able to fail. + rgOptions[0] = (DispatchTypes::GraphicsOptions) 0; + cOptions = 1; + VERIFY_IS_FALSE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + } + + TEST_METHOD(GraphicsSingleTests) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiGraphicsOptions", L"{0, 1, 4, 7, 24, 27, 30, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 43, 44, 45, 46, 47, 49, 90, 91, 92, 93, 94, 95, 96, 97, 100, 101, 102, 103, 104, 105, 106, 107}") // corresponds to options in DispatchTypes::GraphicsOptions + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + _testGetSet->PrepData(); + + // Modify variables based on type of this test + DispatchTypes::GraphicsOptions graphicsOption; + unsigned int uiGraphicsOption; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiGraphicsOptions", uiGraphicsOption)); + graphicsOption = (DispatchTypes::GraphicsOptions)uiGraphicsOption; + + DispatchTypes::GraphicsOptions rgOptions[16]; + size_t cOptions = 1; + rgOptions[0] = graphicsOption; + + _testGetSet->_fPrivateSetLegacyAttributesResult = TRUE; + + switch (graphicsOption) + { + case DispatchTypes::GraphicsOptions::Off: + Log::Comment(L"Testing graphics 'Off/Reset'"); + _testGetSet->_wAttribute = (WORD)~_testGetSet->s_wDefaultFill; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fExpectedBackground = true; + _testGetSet->_fExpectedMeta = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = false; + + break; + case DispatchTypes::GraphicsOptions::BoldBright: + Log::Comment(L"Testing graphics 'Bold/Bright'"); + _testGetSet->_wAttribute = 0; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = true; + break; + case DispatchTypes::GraphicsOptions::Underline: + Log::Comment(L"Testing graphics 'Underline'"); + _testGetSet->_wAttribute = 0; + _testGetSet->_wExpectedAttribute = COMMON_LVB_UNDERSCORE; + _testGetSet->_fExpectedMeta = true; + break; + case DispatchTypes::GraphicsOptions::Negative: + Log::Comment(L"Testing graphics 'Negative'"); + _testGetSet->_wAttribute = 0; + _testGetSet->_wExpectedAttribute = COMMON_LVB_REVERSE_VIDEO; + _testGetSet->_fExpectedMeta = true; + break; + case DispatchTypes::GraphicsOptions::NoUnderline: + Log::Comment(L"Testing graphics 'No Underline'"); + _testGetSet->_wAttribute = COMMON_LVB_UNDERSCORE; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedMeta = true; + break; + case DispatchTypes::GraphicsOptions::Positive: + Log::Comment(L"Testing graphics 'Positive'"); + _testGetSet->_wAttribute = COMMON_LVB_REVERSE_VIDEO; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedMeta = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundBlack: + Log::Comment(L"Testing graphics 'Foreground Color Black'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundBlue: + Log::Comment(L"Testing graphics 'Foreground Color Blue'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundGreen: + Log::Comment(L"Testing graphics 'Foreground Color Green'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundCyan: + Log::Comment(L"Testing graphics 'Foreground Color Cyan'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundRed: + Log::Comment(L"Testing graphics 'Foreground Color Red'"); + _testGetSet->_wAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundMagenta: + Log::Comment(L"Testing graphics 'Foreground Color Magenta'"); + _testGetSet->_wAttribute = FOREGROUND_GREEN | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundYellow: + Log::Comment(L"Testing graphics 'Foreground Color Yellow'"); + _testGetSet->_wAttribute = FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundWhite: + Log::Comment(L"Testing graphics 'Foreground Color White'"); + _testGetSet->_wAttribute = FOREGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::ForegroundDefault: + Log::Comment(L"Testing graphics 'Foreground Color Default'"); + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_wAttribute = (WORD)~_testGetSet->s_wDefaultAttribute; // set the current attribute to the opposite of default so we can ensure all relevant bits flip. + // To get expected value, take what we started with and change ONLY the background series of bits to what the Default says. + _testGetSet->_wExpectedAttribute = _testGetSet->_wAttribute; // expect = starting + _testGetSet->_wExpectedAttribute &= ~(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY); // turn off all bits related to the background + _testGetSet->_wExpectedAttribute |= (_testGetSet->s_wDefaultFill & (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY)); // reapply ONLY background bits from the default attribute. + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundBlack: + Log::Comment(L"Testing graphics 'Background Color Black'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundBlue: + Log::Comment(L"Testing graphics 'Background Color Blue'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_BLUE; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundGreen: + Log::Comment(L"Testing graphics 'Background Color Green'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_GREEN; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundCyan: + Log::Comment(L"Testing graphics 'Background Color Cyan'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_BLUE | BACKGROUND_GREEN; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundRed: + Log::Comment(L"Testing graphics 'Background Color Red'"); + _testGetSet->_wAttribute = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundMagenta: + Log::Comment(L"Testing graphics 'Background Color Magenta'"); + _testGetSet->_wAttribute = BACKGROUND_GREEN | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_BLUE | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundYellow: + Log::Comment(L"Testing graphics 'Background Color Yellow'"); + _testGetSet->_wAttribute = BACKGROUND_BLUE | BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_GREEN | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundWhite: + Log::Comment(L"Testing graphics 'Background Color White'"); + _testGetSet->_wAttribute = BACKGROUND_INTENSITY; + _testGetSet->_wExpectedAttribute = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BackgroundDefault: + Log::Comment(L"Testing graphics 'Background Color Default'"); + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_wAttribute = (WORD)~_testGetSet->s_wDefaultAttribute; // set the current attribute to the opposite of default so we can ensure all relevant bits flip. + // To get expected value, take what we started with and change ONLY the background series of bits to what the Default says. + _testGetSet->_wExpectedAttribute = _testGetSet->_wAttribute; // expect = starting + _testGetSet->_wExpectedAttribute &= ~(BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY); // turn off all bits related to the background + _testGetSet->_wExpectedAttribute |= (_testGetSet->s_wDefaultFill & (BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY)); // reapply ONLY background bits from the default attribute. + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlack: + Log::Comment(L"Testing graphics 'Bright Foreground Color Black'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundBlue: + Log::Comment(L"Testing graphics 'Bright Foreground Color Blue'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_GREEN; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundGreen: + Log::Comment(L"Testing graphics 'Bright Foreground Color Green'"); + _testGetSet->_wAttribute = FOREGROUND_RED | FOREGROUND_BLUE; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundCyan: + Log::Comment(L"Testing graphics 'Bright Foreground Color Cyan'"); + _testGetSet->_wAttribute = FOREGROUND_RED; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundRed: + Log::Comment(L"Testing graphics 'Bright Foreground Color Red'"); + _testGetSet->_wAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundMagenta: + Log::Comment(L"Testing graphics 'Bright Foreground Color Magenta'"); + _testGetSet->_wAttribute = FOREGROUND_GREEN; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundYellow: + Log::Comment(L"Testing graphics 'Bright Foreground Color Yellow'"); + _testGetSet->_wAttribute = FOREGROUND_BLUE; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightForegroundWhite: + Log::Comment(L"Testing graphics 'Bright Foreground Color White'"); + _testGetSet->_wAttribute = 0; + _testGetSet->_wExpectedAttribute = FOREGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; + _testGetSet->_fExpectedForeground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlack: + Log::Comment(L"Testing graphics 'Bright Background Color Black'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundBlue: + Log::Comment(L"Testing graphics 'Bright Background Color Blue'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_GREEN; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_BLUE; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundGreen: + Log::Comment(L"Testing graphics 'Bright Background Color Green'"); + _testGetSet->_wAttribute = BACKGROUND_RED | BACKGROUND_BLUE; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_GREEN; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundCyan: + Log::Comment(L"Testing graphics 'Bright Background Color Cyan'"); + _testGetSet->_wAttribute = BACKGROUND_RED; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_BLUE | BACKGROUND_GREEN; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundRed: + Log::Comment(L"Testing graphics 'Bright Background Color Red'"); + _testGetSet->_wAttribute = BACKGROUND_BLUE | BACKGROUND_GREEN; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundMagenta: + Log::Comment(L"Testing graphics 'Bright Background Color Magenta'"); + _testGetSet->_wAttribute = BACKGROUND_GREEN; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_BLUE | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundYellow: + Log::Comment(L"Testing graphics 'Bright Background Color Yellow'"); + _testGetSet->_wAttribute = BACKGROUND_BLUE; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_GREEN | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + case DispatchTypes::GraphicsOptions::BrightBackgroundWhite: + Log::Comment(L"Testing graphics 'Bright Background Color White'"); + _testGetSet->_wAttribute = 0; + _testGetSet->_wExpectedAttribute = BACKGROUND_INTENSITY | BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED; + _testGetSet->_fExpectedBackground = true; + break; + default: + VERIFY_FAIL(L"Test not implemented yet!"); + break; + } + + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + } + + TEST_METHOD(GraphicsPersistBrightnessTests) + { + Log::Comment(L"Starting test..."); + + _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + _testGetSet->_fPrivateSetLegacyAttributesResult = TRUE; + + DispatchTypes::GraphicsOptions rgOptions[16]; + size_t cOptions = 1; + + Log::Comment(L"Test 1: Basic brightness test"); + Log::Comment(L"Reseting graphics options"); + rgOptions[0] = DispatchTypes::GraphicsOptions::Off; + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fExpectedBackground = true; + _testGetSet->_fExpectedMeta = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Testing graphics 'Foreground Color Blue'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Enabling brightness"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BoldBright; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Green, with brightness'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen; + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(WI_IsFlagSet(_testGetSet->_wAttribute, FOREGROUND_GREEN)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + + Log::Comment(L"Test 2: Disable brightness, use a bright color, next normal call remains not bright"); + Log::Comment(L"Reseting graphics options"); + rgOptions[0] = DispatchTypes::GraphicsOptions::Off; + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fExpectedBackground = true; + _testGetSet->_fExpectedMeta = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(WI_IsFlagClear(_testGetSet->_wAttribute, FOREGROUND_INTENSITY)); + VERIFY_IS_FALSE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Bright Blue'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BrightForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_FALSE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Blue', brightness of 9x series doesn't persist"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_FALSE(_testGetSet->_fIsBold); + + Log::Comment(L"Test 3: Enable brightness, use a bright color, brightness persists to next normal call"); + Log::Comment(L"Reseting graphics options"); + rgOptions[0] = DispatchTypes::GraphicsOptions::Off; + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_wExpectedAttribute = 0; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fExpectedBackground = true; + _testGetSet->_fExpectedMeta = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_FALSE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Blue'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_FALSE(_testGetSet->_fIsBold); + + Log::Comment(L"Enabling brightness"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BoldBright; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedIsBold = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Bright Blue'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BrightForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE | FOREGROUND_INTENSITY; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Blue, with brightness', brightness of 9x series doesn't affect brightness"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundBlue; + _testGetSet->_wExpectedAttribute = FOREGROUND_BLUE; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + + Log::Comment(L"Testing graphics 'Foreground Color Green, with brightness'"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundGreen; + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN; + _testGetSet->_fExpectedForeground = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + VERIFY_IS_TRUE(_testGetSet->_fIsBold); + } + + TEST_METHOD(DeviceStatusReportTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify failure when using bad status."); + _testGetSet->PrepData(); + VERIFY_IS_FALSE(_pDispatch->DeviceStatusReport((DispatchTypes::AnsiStatusType) - 1)); + } + + TEST_METHOD(DeviceStatus_CursorPositionReportTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify normal cursor response position."); + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // start with the cursor position in the buffer. + COORD coordCursorExpected = _testGetSet->_coordCursorPos; + + // to get to VT, we have to adjust it to its position relative to the viewport. + coordCursorExpected.X -= _testGetSet->_srViewport.Left; + coordCursorExpected.Y -= _testGetSet->_srViewport.Top; + + // Then note that VT is 1,1 based for the top left, so add 1. (The rest of the console uses 0,0 for array index bases.) + coordCursorExpected.X++; + coordCursorExpected.Y++; + + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::AnsiStatusType::CPR_CursorPositionReport)); + + wchar_t pwszBuffer[50]; + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[%d;%dR", coordCursorExpected.Y, coordCursorExpected.X); + _testGetSet->ValidateInputEvent(pwszBuffer); + } + + TEST_METHOD(DeviceAttributesTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify normal response."); + _testGetSet->PrepData(); + VERIFY_IS_TRUE(_pDispatch->DeviceAttributes()); + + PCWSTR pwszExpectedResponse = L"\x1b[?1;0c"; + _testGetSet->ValidateInputEvent(pwszExpectedResponse); + + Log::Comment(L"Test 2: Verify failure when WriteConsoleInput doesn't work."); + _testGetSet->PrepData(); + _testGetSet->_fPrivatePrependConsoleInputResult = FALSE; + + VERIFY_IS_FALSE(_pDispatch->DeviceAttributes()); + } + + TEST_METHOD(ScrollTest) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiDirection", L"{0, 1}") // These values align with the ScrollDirection enum class to try all the directions. + TEST_METHOD_PROPERTY(L"Data:uiMagnitude", L"{1, 2, 5}") // These values align with the ScrollDirection enum class to try all the directions. + END_TEST_METHOD_PROPERTIES() + + Log::Comment(L"Starting test..."); + + // Used to switch between the various function options. + typedef bool(AdaptDispatch::*ScrollFunc)(const unsigned int); + ScrollFunc scrollFunc = nullptr; + + // Modify variables based on directionality of this test + ScrollDirection direction; + unsigned int dir; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDirection", dir)); + direction = (ScrollDirection)dir; + unsigned int uiMagnitude; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiMagnitude", uiMagnitude)); + SHORT sMagnitude = (SHORT)uiMagnitude; + + switch (direction) + { + case ScrollDirection::UP: + Log::Comment(L"Testing up direction."); + scrollFunc = &AdaptDispatch::ScrollUp; + break; + case ScrollDirection::DOWN: + Log::Comment(L"Testing down direction."); + scrollFunc = &AdaptDispatch::ScrollDown; + break; + } + Log::Comment(NoThrowString().Format(L"Scrolling by %d lines", uiMagnitude)); + if (scrollFunc == nullptr) + { + VERIFY_FAIL(); + return; + } + + // place the cursor in the center. + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Save the cursor position. It shouldn't move for the rest of the test. + COORD coordCursorExpected = _testGetSet->_coordCursorPos; + + // Fill the entire buffer with Qs. Blue on Green. + WCHAR const wchOuterBuffer = 'Q'; + WORD const wAttrOuterBuffer = FOREGROUND_BLUE | BACKGROUND_GREEN; + SMALL_RECT srOuterBuffer; + srOuterBuffer.Top = 0; + srOuterBuffer.Left = 0; + srOuterBuffer.Bottom = _testGetSet->_coordBufferSize.Y; + srOuterBuffer.Right = _testGetSet->_coordBufferSize.X; + _testGetSet->FillRectangle(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer); + + // Fill the viewport with Rs. Red on Blue. + WCHAR const wchViewport = 'R'; + WORD const wAttrViewport = FOREGROUND_RED | BACKGROUND_BLUE; + SMALL_RECT srViewport = _testGetSet->_srViewport; + _testGetSet->FillRectangle(srViewport, wchViewport, wAttrViewport); + + // Add some characters to see if they moved. + // change the color too so we can make sure that it's fine + + WORD const wAttrTestText = FOREGROUND_GREEN; + PWSTR const pwszTestText = L"ABCDE"; // Text is written at y=34, moves to y=33 + size_t cchTestText = wcslen(pwszTestText); + SMALL_RECT srTestText; + srTestText.Top = _testGetSet->_coordCursorPos.Y; + srTestText.Bottom = srTestText.Top + 1; + srTestText.Left = _testGetSet->_coordCursorPos.X; + srTestText.Right = srTestText.Left + (SHORT)cchTestText; + _testGetSet->InsertString(_testGetSet->_coordCursorPos, pwszTestText, wAttrTestText); + + //Scroll Up one line + VERIFY_IS_TRUE((_pDispatch->*(scrollFunc))(sMagnitude), L"Verify Scroll call was sucessful."); + + // verify cursor didn't move + VERIFY_ARE_EQUAL(coordCursorExpected, _testGetSet->_coordCursorPos, L"Verify cursor didn't move from insert operation."); + + // Verify the field of Qs didn't change outside the viewport. + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOuterBuffer, wchOuterBuffer, wAttrOuterBuffer, srViewport), + L"Field of Qs outside viewport should remain unchanged."); + + // Okay, this part get confusing. These change depending on the direction of the test. + // direction InViewport Outside + // UP Bottom Line Top minus One + // DOWN Top Line Bottom plus One + const bool fScrollUp = (direction == ScrollDirection::UP); + SMALL_RECT srInViewport = srViewport; + srInViewport.Top = (fScrollUp) ? (srViewport.Bottom - sMagnitude) : (srViewport.Top); + srInViewport.Bottom = srInViewport.Top + sMagnitude; + WCHAR const wchInViewport = ' '; + WORD const wAttrInViewport = _testGetSet->_wAttribute; + + // Verify the bottom line is now empty + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srInViewport, wchInViewport, wAttrInViewport), + L"InViewport line(s) should now be blank, with default buffer attributes"); + + SMALL_RECT srOutside = srViewport; + srOutside.Top = (fScrollUp) ? (srViewport.Top - sMagnitude) : (srViewport.Bottom); + srOutside.Bottom = srOutside.Top + sMagnitude; + WCHAR const wchOutside = wchOuterBuffer; + WORD const wAttrOutside = wAttrOuterBuffer; + + // Verify the line above the viewport is unchanged + VERIFY_IS_TRUE(_testGetSet->ValidateRectangleContains(srOutside, wchOutside, wAttrOutside), + L"Line(s) above the viewport is unchanged"); + + // Verify that the line where the ABCDE is now wchViewport + COORD coordTestText; + PWSTR const pwszNewTestText = L"RRRRR"; + coordTestText.X = srTestText.Left; + coordTestText.Y = srTestText.Top; + VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszNewTestText, wAttrViewport), L"Contents of viewport should have shifted to where the string used to be."); + + // Verify that the line above/below the ABCDE now has the ABCDE + coordTestText.X = srTestText.Left; + coordTestText.Y = (fScrollUp) ? (srTestText.Top - sMagnitude) : (srTestText.Top + sMagnitude); + VERIFY_IS_TRUE(_testGetSet->ValidateString(coordTestText, pwszTestText, wAttrTestText), L"String should have moved up/down by given magnitude."); + + } + + TEST_METHOD(CursorKeysModeTest) + { + + Log::Comment(L"Starting test..."); + + // success cases + // set numeric mode = true + Log::Comment(L"Test 1: application mode = false"); + _testGetSet->_fPrivateSetCursorKeysModeResult = TRUE; + _testGetSet->_fCursorKeysApplicationMode = false; + + VERIFY_IS_TRUE(_pDispatch->SetCursorKeysMode(false)); + + // set numeric mode = false + Log::Comment(L"Test 2: application mode = true"); + _testGetSet->_fPrivateSetCursorKeysModeResult = TRUE; + _testGetSet->_fCursorKeysApplicationMode = true; + + VERIFY_IS_TRUE(_pDispatch->SetCursorKeysMode(true)); + + } + + TEST_METHOD(KeypadModeTest) + { + + Log::Comment(L"Starting test..."); + + // success cases + // set numeric mode = true + Log::Comment(L"Test 1: application mode = false"); + _testGetSet->_fPrivateSetKeypadModeResult = TRUE; + _testGetSet->_fKeypadApplicationMode = false; + + VERIFY_IS_TRUE(_pDispatch->SetKeypadMode(false)); + + // set numeric mode = false + Log::Comment(L"Test 2: application mode = true"); + _testGetSet->_fPrivateSetKeypadModeResult = TRUE; + _testGetSet->_fKeypadApplicationMode = true; + + VERIFY_IS_TRUE(_pDispatch->SetKeypadMode(true)); + + } + + TEST_METHOD(AllowBlinkingTest) + { + + Log::Comment(L"Starting test..."); + + // success cases + // set numeric mode = true + Log::Comment(L"Test 1: enable blinking = true"); + _testGetSet->_fPrivateAllowCursorBlinkingResult = TRUE; + _testGetSet->_fEnable = true; + + VERIFY_IS_TRUE(_pDispatch->EnableCursorBlinking(true)); + + // set numeric mode = false + Log::Comment(L"Test 2: enable blinking = false"); + _testGetSet->_fPrivateAllowCursorBlinkingResult = TRUE; + _testGetSet->_fEnable = false; + + VERIFY_IS_TRUE(_pDispatch->EnableCursorBlinking(false)); + + } + + TEST_METHOD(ScrollMarginsTest) + { + Log::Comment(L"Starting test..."); + + SMALL_RECT srTestMargins = { 0 }; + _testGetSet->_srViewport.Right = 8; + _testGetSet->_srViewport.Bottom = 8; + _testGetSet->_fGetConsoleScreenBufferInfoExResult = TRUE; + + Log::Comment(L"Test 1: Verify having both values is valid."); + _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + _testGetSet->_fSetConsoleCursorPositionResult = true; + _testGetSet->_fMoveToBottomResult = true; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + Log::Comment(L"Test 2: Verify having only top is valid."); + + _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 0); + _testGetSet->_srExpectedScrollRegion.Bottom = _testGetSet->_srViewport.Bottom - 1; // We expect the bottom to be the bottom of the viewport, exclusive. + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + Log::Comment(L"Test 3: Verify having only bottom is valid."); + + _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 7); + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + Log::Comment(L"Test 4: Verify having no values is valid."); + + _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 0); + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + Log::Comment(L"Test 5: Verify having both values, but bad bounds is invalid."); + + _testGetSet->_SetMarginsHelper(&srTestMargins, 7, 3); + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + VERIFY_IS_FALSE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + + Log::Comment(L"Test 6: Verify Setting margins to (0, height) clears them"); + // First set, + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + // Then clear + _testGetSet->_srExpectedScrollRegion.Top = 0; + _testGetSet->_srExpectedScrollRegion.Bottom = 0; + _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 7); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + + Log::Comment(L"Test 7: Verify Setting margins to (1, height) clears them"); + // First set, + _testGetSet->_fPrivateSetScrollingRegionResult = TRUE; + _testGetSet->_SetMarginsHelper(&srTestMargins, 2, 6); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + // Then clear + _testGetSet->_srExpectedScrollRegion.Top = 0; + _testGetSet->_srExpectedScrollRegion.Bottom = 0; + _testGetSet->_SetMarginsHelper(&srTestMargins, 0, 7); + VERIFY_IS_TRUE(_pDispatch->SetTopBottomScrollingMargins(srTestMargins.Top, srTestMargins.Bottom)); + + } + + + TEST_METHOD(TabSetClearTests) + { + Log::Comment(L"Starting test..."); + + _testGetSet->_fPrivateHorizontalTabSetResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->HorizontalTabSet()); + + _testGetSet->_sExpectedNumTabs = 16; + + _testGetSet->_fPrivateForwardTabResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->ForwardTab(16)); + + _testGetSet->_fPrivateBackwardsTabResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->BackwardsTab(16)); + + _testGetSet->_fPrivateTabClearResult = TRUE; + _testGetSet->_fExpectedClearAll = true; + VERIFY_IS_TRUE(_pDispatch->TabClear(DispatchTypes::TabClearType::ClearAllColumns)); + + _testGetSet->_fExpectedClearAll = false; + VERIFY_IS_TRUE(_pDispatch->TabClear(DispatchTypes::TabClearType::ClearCurrentColumn)); + + } + + TEST_METHOD(SetConsoleTitleTest) + { + + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: set title to be non-null"); + _testGetSet->_fSetConsoleTitleWResult = TRUE; + wchar_t* pwchTestString = L"Foo bar"; + _testGetSet->_pwchExpectedWindowTitle = pwchTestString; + _testGetSet->_sCchExpectedTitleLength = 8; + + VERIFY_IS_TRUE(_pDispatch->SetWindowTitle({ pwchTestString, 8 })); + + Log::Comment(L"Test 2: set title to be null"); + _testGetSet->_fSetConsoleTitleWResult = FALSE; + _testGetSet->_pwchExpectedWindowTitle = nullptr; + + VERIFY_IS_TRUE(_pDispatch->SetWindowTitle({})); + + } + + TEST_METHOD(TestMouseModes) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Test Default Mouse Mode"); + _testGetSet->_fExpectedMouseEnabled = true; + _testGetSet->_fPrivateEnableVT200MouseModeResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableVT200MouseMode(true)); + _testGetSet->_fExpectedMouseEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableVT200MouseMode(false)); + + Log::Comment(L"Test 2: Test UTF-8 Extended Mouse Mode"); + _testGetSet->_fExpectedMouseEnabled = true; + _testGetSet->_fPrivateEnableUTF8ExtendedMouseModeResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableUTF8ExtendedMouseMode(true)); + _testGetSet->_fExpectedMouseEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableUTF8ExtendedMouseMode(false)); + + Log::Comment(L"Test 3: Test SGR Extended Mouse Mode"); + _testGetSet->_fExpectedMouseEnabled = true; + _testGetSet->_fPrivateEnableSGRExtendedMouseModeResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableSGRExtendedMouseMode(true)); + _testGetSet->_fExpectedMouseEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableSGRExtendedMouseMode(false)); + + Log::Comment(L"Test 4: Test Button-Event Mouse Mode"); + _testGetSet->_fExpectedMouseEnabled = true; + _testGetSet->_fPrivateEnableButtonEventMouseModeResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableButtonEventMouseMode(true)); + _testGetSet->_fExpectedMouseEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableButtonEventMouseMode(false)); + + Log::Comment(L"Test 5: Test Any-Event Mouse Mode"); + _testGetSet->_fExpectedMouseEnabled = true; + _testGetSet->_fPrivateEnableAnyEventMouseModeResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableAnyEventMouseMode(true)); + _testGetSet->_fExpectedMouseEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableAnyEventMouseMode(false)); + + Log::Comment(L"Test 6: Test Alt Scroll Mouse Mode"); + _testGetSet->_fExpectedAlternateScrollEnabled = true; + _testGetSet->_fPrivateEnableAlternateScrollResult = TRUE; + VERIFY_IS_TRUE(_pDispatch->EnableAlternateScroll(true)); + _testGetSet->_fExpectedAlternateScrollEnabled = false; + VERIFY_IS_TRUE(_pDispatch->EnableAlternateScroll(false)); + } + + TEST_METHOD(Xterm256ColorTest) + { + Log::Comment(L"Starting test..."); + + _testGetSet->PrepData(); // default color from here is gray on black, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED + + + DispatchTypes::GraphicsOptions rgOptions[16]; + size_t cOptions = 3; + + _testGetSet->_fSetConsoleXtermTextAttributeResult = true; + + Log::Comment(L"Test 1: Change Foreground"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::Xterm256Index; + rgOptions[2] = (DispatchTypes::GraphicsOptions)2; // Green + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN; + _testGetSet->_iExpectedXtermTableEntry = 2; + _testGetSet->_fExpectedIsForeground = true; + _testGetSet->_fUsingRgbColor = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Test 2: Change Background"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::Xterm256Index; + rgOptions[2] = (DispatchTypes::GraphicsOptions)9; // Bright Red + _testGetSet->_wExpectedAttribute = FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_INTENSITY; + _testGetSet->_iExpectedXtermTableEntry = 9; + _testGetSet->_fExpectedIsForeground = false; + _testGetSet->_fUsingRgbColor = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + + + Log::Comment(L"Test 3: Change Foreground to RGB color"); + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::Xterm256Index; + rgOptions[2] = (DispatchTypes::GraphicsOptions)42; // Arbitrary Color + _testGetSet->_iExpectedXtermTableEntry = 42; + _testGetSet->_fExpectedIsForeground = true; + _testGetSet->_fUsingRgbColor = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + + Log::Comment(L"Test 4: Change Background to RGB color"); + rgOptions[0] = DispatchTypes::GraphicsOptions::BackgroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::Xterm256Index; + rgOptions[2] = (DispatchTypes::GraphicsOptions)142; // Arbitrary Color + _testGetSet->_iExpectedXtermTableEntry = 142; + _testGetSet->_fExpectedIsForeground = false; + _testGetSet->_fUsingRgbColor = true; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + Log::Comment(L"Test 5: Change Foreground to Legacy Attr while BG is RGB color"); + // Unfortunately this test isn't all that good, because the adapterTest adapter isn't smart enough + // to have it's own color table and translate the pre-existing RGB BG into a legacy BG. + // Fortunately, the ft_api:RgbColorTests IS smart enough to test that. + rgOptions[0] = DispatchTypes::GraphicsOptions::ForegroundExtended; + rgOptions[1] = DispatchTypes::GraphicsOptions::Xterm256Index; + rgOptions[2] = (DispatchTypes::GraphicsOptions)9; // Bright Red + _testGetSet->_wExpectedAttribute = FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_INTENSITY; + _testGetSet->_iExpectedXtermTableEntry = 9; + _testGetSet->_fExpectedIsForeground = true; + _testGetSet->_fUsingRgbColor = false; + VERIFY_IS_TRUE(_pDispatch->SetGraphicsRendition(rgOptions, cOptions)); + + } + + + TEST_METHOD(HardReset) + { + Log::Comment(L"Starting test..."); + + _testGetSet->PrepData(); + + ///////////////// Components of a EraseScrollback ////////////////////// + _testGetSet->_fExpectedWindowAbsolute = true; + SMALL_RECT srRegion = { 0 }; + srRegion.Bottom = _testGetSet->_srViewport.Bottom - _testGetSet->_srViewport.Top - 1; + srRegion.Right = _testGetSet->_srViewport.Right - _testGetSet->_srViewport.Left - 1; + _testGetSet->_srExpectedConsoleWindow = srRegion; + // The cursor will be moved to the same relative location in the new viewport with origin @ 0, 0 + const COORD coordRelativeCursor = { _testGetSet->_coordCursorPos.X - _testGetSet->_srViewport.Left, + _testGetSet->_coordCursorPos.Y - _testGetSet->_srViewport.Top }; + + // Cursor to 1,1 + _testGetSet->_coordExpectedCursorPos = { 0, 0 }; + _testGetSet->_fSetConsoleCursorPositionResult = true; + _testGetSet->_fPrivateSetLegacyAttributesResult = true; + _testGetSet->_fPrivateSetDefaultAttributesResult = true; + _testGetSet->_fPrivateBoldTextResult = true; + _testGetSet->_fExpectedForeground = true; + _testGetSet->_fExpectedBackground = true; + _testGetSet->_fExpectedMeta = true; + _testGetSet->_fExpectedIsBold = false; + _testGetSet->_expectedShowCursor = true; + _testGetSet->_privateShowCursorResult = true; + const COORD coordExpectedCursorPos = { 0, 0 }; + + // We're expecting _SetDefaultColorHelper to call + // PrivateSetLegacyAttributes with 0 as the wAttr param. + _testGetSet->_wExpectedAttribute = 0; + + // Prepare the results of SoftReset api calls + _testGetSet->_fPrivateSetCursorKeysModeResult = true; + _testGetSet->_fPrivateSetKeypadModeResult = true; + _testGetSet->_fGetConsoleScreenBufferInfoExResult = true; + _testGetSet->_fPrivateSetScrollingRegionResult = true; + + VERIFY_IS_TRUE(_pDispatch->HardReset()); + VERIFY_ARE_EQUAL(_testGetSet->_coordCursorPos, coordExpectedCursorPos); + VERIFY_ARE_EQUAL(_testGetSet->_fUsingRgbColor, false); + + Log::Comment(L"Test 2: Gracefully fail when getting console information fails."); + _testGetSet->PrepData(); + _testGetSet->_fGetConsoleScreenBufferInfoExResult = false; + + VERIFY_IS_FALSE(_pDispatch->HardReset()); + + Log::Comment(L"Test 3: Gracefully fail when filling the rectangle fails."); + _testGetSet->PrepData(); + _testGetSet->_fFillConsoleOutputCharacterWResult = false; + + VERIFY_IS_FALSE(_pDispatch->HardReset()); + + Log::Comment(L"Test 4: Gracefully fail when setting the window fails."); + _testGetSet->PrepData(); + _testGetSet->_fSetConsoleWindowInfoResult = false; + + VERIFY_IS_FALSE(_pDispatch->HardReset()); + } + + TEST_METHOD(SetColorTableValue) + { + _testGetSet->PrepData(); + + _testGetSet->_fPrivateSetColorTableEntryResult = true; + const auto testColor = RGB(1, 2, 3); + _testGetSet->_expectedColorValue = testColor; + + _testGetSet->_expectedColorTableIndex = 0; // Windows DARK_BLACK + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(0, testColor)); + + _testGetSet->_expectedColorTableIndex = 4; // Windows DARK_RED + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(1, testColor)); + + _testGetSet->_expectedColorTableIndex = 2; // Windows DARK_GREEN + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(2, testColor)); + + _testGetSet->_expectedColorTableIndex = 6; // Windows DARK_YELLOW + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(3, testColor)); + + _testGetSet->_expectedColorTableIndex = 1; // Windows DARK_BLUE + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(4, testColor)); + + _testGetSet->_expectedColorTableIndex = 5; // Windows DARK_MAGENTA + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(5, testColor)); + + _testGetSet->_expectedColorTableIndex = 3; // Windows DARK_CYAN + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(6, testColor)); + + _testGetSet->_expectedColorTableIndex = 7; // Windows DARK_WHITE + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(7, testColor)); + + _testGetSet->_expectedColorTableIndex = 8; // Windows BRIGHT_BLACK + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(8, testColor)); + + _testGetSet->_expectedColorTableIndex = 12; // Windows BRIGHT_RED + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(9, testColor)); + + _testGetSet->_expectedColorTableIndex = 10; // Windows BRIGHT_GREEN + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(10, testColor)); + + _testGetSet->_expectedColorTableIndex = 14; // Windows BRIGHT_YELLOW + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(11, testColor)); + + _testGetSet->_expectedColorTableIndex = 9; // Windows BRIGHT_BLUE + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(12, testColor)); + + _testGetSet->_expectedColorTableIndex = 13; // Windows BRIGHT_MAGENTA + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(13, testColor)); + + _testGetSet->_expectedColorTableIndex = 11; // Windows BRIGHT_CYAN + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(14, testColor)); + + _testGetSet->_expectedColorTableIndex = 15; // Windows BRIGHT_WHITE + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(15, testColor)); + + for (short i = 16; i < 256; i++) + { + _testGetSet->_expectedColorTableIndex = i; + VERIFY_IS_TRUE(_pDispatch->SetColorTableEntry(i, testColor)); + } + + // Test in pty mode - we should fail, but PrivateSetColorTableEntry should still be called + _testGetSet->_fIsPty = true; + _testGetSet->_fIsConsolePtyResult = true; + + _testGetSet->_expectedColorTableIndex = 15; // Windows BRIGHT_WHITE + VERIFY_IS_FALSE(_pDispatch->SetColorTableEntry(15, testColor)); + + } + +private: + TestGetSet* _testGetSet; // non-ownership pointer + AdaptDispatch* _pDispatch; +}; diff --git a/src/terminal/adapter/ut_adapter/inputTest.cpp b/src/terminal/adapter/ut_adapter/inputTest.cpp new file mode 100644 index 000000000..21392161b --- /dev/null +++ b/src/terminal/adapter/ut_adapter/inputTest.cpp @@ -0,0 +1,689 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "..\precomp.h" +#include +#include +#include "..\..\inc\consoletaeftemplates.hpp" + +#include "..\..\input\terminalInput.hpp" + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "..\..\..\interactivity\inc\VtApiRedirection.hpp" +#endif + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class InputTest; + }; + }; +}; +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 PWSTR s_pwszInputExpected; +static wchar_t s_pwsInputBuffer[256]; +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(TerminalInputModifierKeyTests); + TEST_METHOD(TerminalInputNullKeyTests); + TEST_METHOD(DifferentModifiersTest); + + wchar_t GetModifierChar(const bool fShift, const bool fAlt, const bool fCtrl) + { + return L'1' + (fShift? 1 : 0) + (fAlt? 2 : 0) + (fCtrl? 4 : 0); + } + + bool ControlAndAltPressed(unsigned int uiKeystate) + { + return WI_IsAnyFlagSet(uiKeystate, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) + && WI_IsAnyFlagSet(uiKeystate, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); + } + + bool ControlOrAltPressed(unsigned int uiKeystate) + { + return WI_IsAnyFlagSet(uiKeystate, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED | LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); + } + + bool ControlPressed(unsigned int uiKeystate) + { + return WI_IsAnyFlagSet(uiKeystate, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); + } + + bool AltPressed(unsigned int uiKeystate) + { + return WI_IsAnyFlagSet(uiKeystate, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); + } + + bool ShiftPressed(unsigned int uiKeystate) + { + return WI_IsFlagSet(uiKeystate, SHIFT_PRESSED); + } +}; + +void InputTest::s_TerminalInputTestCallback(_In_ std::deque>& inEvents) +{ + auto records = IInputEvent::ToInputRecords(inEvents); + + size_t cInputExpected = 0; + VERIFY_SUCCEEDED(StringCchLengthW(s_pwszInputExpected, STRSAFE_MAX_CCH, &cInputExpected)); + + if (VERIFY_ARE_EQUAL(cInputExpected, 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_pwszInputExpected[i]; + VERIFY_ARE_EQUAL(irExpected, records[i], NoThrowString().Format(L"%c, %c", s_pwszInputExpected[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(VkKeyScanW(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 TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + + 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)); + + bool fExpectedKeyHandled = true; + + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = TRUE; + // MapVirtualKey's return value must be mapped to a wchar_t because + // that's what we're requesting from it, there isn't any data loss + // from the cast. + #pragma warning(push) + #pragma warning(disable:4242) + irTest.Event.KeyEvent.uChar.UnicodeChar = (wchar_t)MapVirtualKey(vkey, MAPVK_VK_TO_CHAR); + #pragma warning(pop) + + // Set up expected result + switch (vkey) + { + case VK_TAB: + s_pwszInputExpected = L"\x09"; + break; + case VK_BACK: + s_pwszInputExpected = L"\x7f"; + break; + case VK_ESCAPE: + s_pwszInputExpected = L"\x1b"; + break; + case VK_PAUSE: + s_pwszInputExpected = L"\x1a"; + break; + case VK_UP: + s_pwszInputExpected = L"\x1b[A"; + break; + case VK_DOWN: + s_pwszInputExpected = L"\x1b[B"; + break; + case VK_RIGHT: + s_pwszInputExpected = L"\x1b[C"; + break; + case VK_LEFT: + s_pwszInputExpected = L"\x1b[D"; + break; + case VK_HOME: + s_pwszInputExpected = L"\x1b[H"; + break; + case VK_INSERT: + s_pwszInputExpected = L"\x1b[2~"; + break; + case VK_DELETE: + s_pwszInputExpected = L"\x1b[3~"; + break; + case VK_END: + s_pwszInputExpected = L"\x1b[F"; + break; + case VK_PRIOR: + s_pwszInputExpected = L"\x1b[5~"; + break; + case VK_NEXT: + s_pwszInputExpected = L"\x1b[6~"; + break; + case VK_F1: + s_pwszInputExpected = L"\x1bOP"; + break; + case VK_F2: + s_pwszInputExpected = L"\x1bOQ"; + break; + case VK_F3: + s_pwszInputExpected = L"\x1bOR"; + break; + case VK_F4: + s_pwszInputExpected = L"\x1bOS"; + break; + case VK_F5: + s_pwszInputExpected = L"\x1b[15~"; + break; + case VK_F6: + s_pwszInputExpected = L"\x1b[17~"; + break; + case VK_F7: + s_pwszInputExpected = L"\x1b[18~"; + break; + case VK_F8: + s_pwszInputExpected = L"\x1b[19~"; + break; + case VK_F9: + s_pwszInputExpected = L"\x1b[20~"; + break; + case VK_F10: + s_pwszInputExpected = L"\x1b[21~"; + break; + case VK_F11: + s_pwszInputExpected = L"\x1b[23~"; + break; + case VK_F12: + s_pwszInputExpected = L"\x1b[24~"; + break; + case VK_CANCEL: + s_pwszInputExpected = L"\x3"; + break; + default: + fExpectedKeyHandled = false; + break; + } + if (!fExpectedKeyHandled && (vkey >= '0' && vkey <= 'Z')) + { + // we need to have some sort of string to compare to in the + // callback, we'll build it here. + static wchar_t keyArr[2] = { 0 }; + keyArr[0] = vkey; + s_pwszInputExpected = keyArr; + 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."); + } + + Log::Comment(L"Sending every possible VKEY at the input stream for interception during key UP."); + for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) + { + Log::Comment(NoThrowString().Format(L"Testing Key 0x%x", vkey)); + + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = FALSE; + + 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."); + } + + Log::Comment(L"Verify other types of events are not handled/intercepted."); + + INPUT_RECORD irUnhandled = { 0 }; + + 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."); + + 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."); + + 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."); + + Log::Comment(L"Testing FOCUS_EVENT"); + irUnhandled.EventType = FOCUS_EVENT; + inputEvent = IInputEvent::Create(irUnhandled); + VERIFY_ARE_EQUAL(false, pInput->HandleKey(inputEvent.get()), L"Verify FOCUS_EVENT was NOT handled."); +} + +void InputTest::TerminalInputModifierKeyTests() +{ + // Modifier key state values used in the method properties. + // #define RIGHT_ALT_PRESSED 0x0001 + // #define LEFT_ALT_PRESSED 0x0002 + // #define RIGHT_CTRL_PRESSED 0x0004 + // #define LEFT_CTRL_PRESSED 0x0008 + // #define SHIFT_PRESSED 0x0010 + Log::Comment(L"Starting test..."); + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiModifierKeystate", L"{0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x000A, 0x000C, 0x000E, 0x0010, 0x0011, 0x0012, 0x0013}") + END_TEST_METHOD_PROPERTIES() + + unsigned int uiActualKeystate; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiModifierKeystate", uiActualKeystate)); + unsigned int uiKeystate = uiActualKeystate; + + const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + + 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)); + // zero memory + memset(s_pwsInputBuffer, 0, ARRAYSIZE(s_pwsInputBuffer) * sizeof(wchar_t)); + + bool fExpectedKeyHandled = true; + bool fModifySequence = false; + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.dwControlKeyState = uiActualKeystate; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = TRUE; + + // Set up expected result + switch (vkey) + { + case '@': + case '2': + if (ControlPressed(uiKeystate)) + { + continue; + } + // C-@ gets translated to null, which doesn't play nicely with this test. + // So theres the TerminalInputNullKeyTests Test instead. + break; + case 0x20: + // Space generally gets translated to null, which again, doesn't play well. + continue; + case VK_BACK: + // Backspace is kinda different from other keys - we'll handle in another test. + case VK_DIVIDE: + case VK_OEM_2: + // Ctrl-/ is also handled in another test, because it's weird. + // VK_OEM_2 is typically the '/?' key + continue; + // wcscpy_s(s_pwsInputBuffer, L"\x7f"); + break; + case VK_ESCAPE: + wcscpy_s(s_pwsInputBuffer, L"\x1b"); + break; + case VK_PAUSE: + wcscpy_s(s_pwsInputBuffer, L"\x1a"); + break; + case VK_UP: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mA"); + break; + case VK_DOWN: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mB"); + break; + case VK_RIGHT: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mC"); + break; + case VK_LEFT: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mD"); + break; + case VK_HOME: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mH"); + break; + case VK_INSERT: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[2;m~"); + break; + case VK_DELETE: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[3;m~"); + break; + case VK_END: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mF"); + break; + case VK_PRIOR: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[5;m~"); + break; + case VK_NEXT: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[6;m~"); + break; + case VK_F1: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mP"); + break; + case VK_F2: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mQ"); + break; + case VK_F3: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mR"); + break; + case VK_F4: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[1;mS"); + break; + case VK_F5: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[15;m~"); + break; + case VK_F6: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[17;m~"); + break; + case VK_F7: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[18;m~"); + break; + case VK_F8: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[19;m~"); + break; + case VK_F9: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[20;m~"); + break; + case VK_F10: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[21;m~"); + break; + case VK_F11: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[23;m~"); + break; + case VK_F12: + fModifySequence = true; + wcscpy_s(s_pwsInputBuffer, L"\x1b[24;m~"); + break; + case VK_TAB: + if (AltPressed(uiKeystate)) + { + // Alt+Tab isn't possible - thats reserved by the system. + continue; + } + else if (ShiftPressed(uiKeystate)) + { + wcscpy_s(s_pwsInputBuffer, L"\x1b[Z"); + fExpectedKeyHandled = true; + } + else if (ControlPressed(uiKeystate)) + { + wcscpy_s(s_pwsInputBuffer, L"\t"); + fExpectedKeyHandled = true; + } + break; + default: + // Alt+Key generates [0x1b, key] into the stream + if (AltPressed(uiKeystate) && (vkey > 0x40 && vkey <= 0x5A)) + { + wcscpy_s(s_pwsInputBuffer, L"\x1bm"); + wchar_t wchShifted = vkey; + // Alt + Ctrl + key generates [0x1b, control key] in the stream. + if (ControlPressed(uiKeystate)) + { + // Generally the control key is key-0x40 + wchShifted = vkey - 0x40; + } + s_pwsInputBuffer[1] = wchShifted; + fExpectedKeyHandled = true; + } + else + { + fExpectedKeyHandled = false; + } + break; + } + if (!fExpectedKeyHandled && ((vkey >= '0' && vkey <= 'Z') || vkey == VK_CANCEL)) + { + fExpectedKeyHandled = true; + } + + if (fModifySequence) + { + size_t cch = 0; + VERIFY_SUCCEEDED(StringCchLengthW(s_pwsInputBuffer, 8, &cch)); + if (cch > 1) + { + bool fShift = !!(uiKeystate & SHIFT_PRESSED); + bool fAlt = (uiKeystate & LEFT_ALT_PRESSED) || (uiKeystate & RIGHT_ALT_PRESSED); + bool fCtrl = (uiKeystate & LEFT_CTRL_PRESSED) || (uiKeystate & RIGHT_CTRL_PRESSED); + s_pwsInputBuffer[cch-2] = L'1' + (fShift? 1 : 0) + (fAlt? 2 : 0) + (fCtrl? 4 : 0); + } + } + s_pwszInputExpected = s_pwsInputBuffer; + Log::Comment(NoThrowString().Format(L"Expected, Buffer = \"%s\", \"%s\"", s_pwszInputExpected, s_pwsInputBuffer)); + + 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."); + + } +} + +void InputTest::TerminalInputNullKeyTests() +{ + Log::Comment(L"Starting test..."); + + unsigned int uiKeystate = LEFT_CTRL_PRESSED; + + const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestNullCallback); + + Log::Comment(L"Sending every possible VKEY at the input stream for interception during key DOWN."); + + BYTE vkey = '2'; + Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); + + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = TRUE; + + // 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."); + + 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."); + + 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."); + + 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."); + + uiKeystate = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED; + // This is AltGr, this ISN'T handled. + 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(false, pInput->HandleKey(inputEvent.get()), L"Verify key was handled if it should have been."); + +} + +void TestKey(const TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey, const wchar_t wch) +{ + Log::Comment(NoThrowString().Format(L"Testing key, state =0x%x, 0x%x", vkey, uiKeystate)); + + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.dwControlKeyState = uiKeystate; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = TRUE; + irTest.Event.KeyEvent.uChar.UnicodeChar = wch; + + // 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."); +} +void TestKey(const TerminalInput* const pInput, const unsigned int uiKeystate, const BYTE vkey) +{ + // Callers of this version don't expect the wchar to matter. + TestKey(pInput, uiKeystate, vkey, 0); +} + +void InputTest::DifferentModifiersTest() +{ + Log::Comment(L"Starting test..."); + + + const TerminalInput* const pInput = new TerminalInput(s_TerminalInputTestCallback); + + Log::Comment(L"Sending a bunch of keystrokes that are a little weird."); + + unsigned int uiKeystate = 0; + BYTE vkey = VK_BACK; + s_pwszInputExpected = L"\x7f"; + TestKey(pInput, uiKeystate, vkey); + + uiKeystate = LEFT_CTRL_PRESSED; + vkey = VK_BACK; + s_pwszInputExpected = L"\x8"; + TestKey(pInput, uiKeystate, vkey, L'\x8'); + uiKeystate = RIGHT_CTRL_PRESSED; + TestKey(pInput, uiKeystate, vkey, L'\x8'); + + uiKeystate = LEFT_ALT_PRESSED; + vkey = VK_BACK; + s_pwszInputExpected = L"\x1b\x7f"; + TestKey(pInput, uiKeystate, vkey, L'\x8'); + uiKeystate = RIGHT_ALT_PRESSED; + TestKey(pInput, uiKeystate, vkey, L'\x8'); + + uiKeystate = LEFT_CTRL_PRESSED; + vkey = VK_DELETE; + s_pwszInputExpected = L"\x1b[3;5~"; + TestKey(pInput, uiKeystate, vkey); + uiKeystate = RIGHT_CTRL_PRESSED; + TestKey(pInput, uiKeystate, vkey); + + uiKeystate = LEFT_ALT_PRESSED; + vkey = VK_DELETE; + s_pwszInputExpected = L"\x1b[3;3~"; + TestKey(pInput, uiKeystate, vkey); + uiKeystate = RIGHT_ALT_PRESSED; + TestKey(pInput, uiKeystate, vkey); + + uiKeystate = LEFT_CTRL_PRESSED; + vkey = VK_TAB; + s_pwszInputExpected = L"\t"; + TestKey(pInput, uiKeystate, vkey); + uiKeystate = RIGHT_CTRL_PRESSED; + TestKey(pInput, uiKeystate, vkey); + + uiKeystate = SHIFT_PRESSED; + vkey = VK_TAB; + s_pwszInputExpected = L"\x1b[Z"; + TestKey(pInput, uiKeystate, vkey); + + // C-/ -> C-_ -> 0x1f + uiKeystate = LEFT_CTRL_PRESSED; + vkey = LOBYTE(VkKeyScan(L'/')); + s_pwszInputExpected = L"\x1f"; + TestKey(pInput, uiKeystate, vkey, L'/'); + uiKeystate = RIGHT_CTRL_PRESSED; + TestKey(pInput, uiKeystate, vkey, L'/'); + + // M-/ -> ESC / + uiKeystate = LEFT_ALT_PRESSED; + vkey = LOBYTE(VkKeyScan(L'/')); + s_pwszInputExpected = L"\x1b/"; + TestKey(pInput, uiKeystate, vkey, L'/'); + uiKeystate = RIGHT_ALT_PRESSED; + TestKey(pInput, uiKeystate, vkey, L'/'); +} diff --git a/src/terminal/adapter/ut_adapter/product.pbxproj b/src/terminal/adapter/ut_adapter/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/terminal/adapter/ut_adapter/run.bat b/src/terminal/adapter/ut_adapter/run.bat new file mode 100644 index 000000000..1666738fe --- /dev/null +++ b/src/terminal/adapter/ut_adapter/run.bat @@ -0,0 +1 @@ +te %_NTTREE%\unittests\conterm.adapter.tests.dll %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/terminal/adapter/ut_adapter/sources b/src/terminal/adapter/ut_adapter/sources new file mode 100644 index 000000000..fe0669e5c --- /dev/null +++ b/src/terminal/adapter/ut_adapter/sources @@ -0,0 +1,149 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.VirtualTerminal.Adapter.UnitTests +TARGETTYPE = DYNLINK +TARGET_DESTINATION = UnitTests +DLLDEF = + +UNIVERSAL_TEST = 1 +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + adapterTest.cpp \ + inputTest.cpp \ + MouseInputTest.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\WexTest\Cue; \ + $(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \ + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ + $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ + $(ONECORE_INTERNAL_SDK_LIB_PATH)\onecoreuuid.lib \ + $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + $(SDK_LIB_PATH)\d2d1.lib \ + $(SDK_LIB_PATH)\dwrite.lib \ + $(SDK_LIB_PATH)\dxgi.lib \ + $(SDK_LIB_PATH)\d3d11.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-mm-playsound-l1.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-dwmapi-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-edputil-policy-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-font-l1.lib \ + $(ONECOREWINDOWS_INTERNAL_LIB_PATH_L)\ext-ms-win-gdi-internal-desktop-l1-1-0.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-caret-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-dialogbox-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-gui-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-menu-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-misc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-mouse-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-rectangle-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-server-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-window-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-object-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-rgn-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-cursor-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-dc-access-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-rawinput-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-sysparams-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-window-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-shell-shell32-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uxtheme-themes-l1.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uiacore-l1.lib \ + $(WINCORE_OBJ_PATH)\console\conint\$(O)\conint.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\buffer\out\lib\$(O)\conbufferout.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\host\lib\$(O)\conhostv2.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\tsf\$(O)\contsf.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\propslib\$(O)\conprops.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\adapter\lib\$(O)\ConTermAdapter.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\input\lib\$(O)\ConTermInput.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\terminal\parser\lib\$(O)\ConTermParser.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\base\lib\$(O)\ConRenderBase.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\dx\lib\$(O)\ConRenderDx.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\gdi\lib\$(O)\ConRenderGdi.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\wddmcon\lib\$(O)\ConRenderWddmCon.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\renderer\vt\lib\$(O)\ConRenderVt.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\server\lib\$(O)\ConServer.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\base\lib\$(O)\ConInteractivityBaseLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\win32\lib\$(O)\ConInteractivityWin32Lib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\interactivity\onecore\lib\$(O)\ConInteractivityOneCoreLib.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\types\lib\$(O)\ConTypes.lib \ + +DELAYLOAD = \ + PROPSYS.dll; \ + D2D1.dll; \ + DWrite.dll; \ + DXGI.dll; \ + D3D11.dll; \ + OLEAUT32.dll; \ + api-ms-win-mm-playsound-l1.dll; \ + api-ms-win-shcore-scaling-l1.dll; \ + api-ms-win-shell-namespace-l1.dll; \ + ext-ms-win-dwmapi-ext-l1.dll; \ + ext-ms-win-edputil-policy-l1.dll; \ + ext-ms-win-gdi-dc-l1.dll; \ + ext-ms-win-gdi-dc-create-l1.dll; \ + ext-ms-win-gdi-draw-l1.dll; \ + ext-ms-win-gdi-font-l1.dll; \ + ext-ms-win-gdi-internal-desktop-l1.dll; \ + ext-ms-win-ntuser-caret-l1.dll; \ + ext-ms-win-ntuser-dialogbox-l1.dll; \ + ext-ms-win-ntuser-draw-l1.dll; \ + ext-ms-win-ntuser-keyboard-l1.dll; \ + ext-ms-win-ntuser-gui-l1.dll; \ + ext-ms-win-ntuser-menu-l1.dll; \ + ext-ms-win-ntuser-misc-l1.dll; \ + ext-ms-win-ntuser-mouse-l1.dll; \ + ext-ms-win-ntuser-rectangle-ext-l1.dll; \ + ext-ms-win-ntuser-server-l1.dll; \ + ext-ms-win-ntuser-window-l1.dll; \ + ext-ms-win-rtcore-gdi-object-l1.dll; \ + ext-ms-win-rtcore-gdi-rgn-l1.dll; \ + ext-ms-win-rtcore-ntuser-cursor-l1.dll; \ + ext-ms-win-rtcore-ntuser-dc-access-l1.dll; \ + ext-ms-win-rtcore-ntuser-rawinput-l1.dll; \ + ext-ms-win-rtcore-ntuser-sysparams-l1.dll; \ + ext-ms-win-rtcore-ntuser-window-ext-l1.dll; \ + ext-ms-win-shell-shell32-l1.dll; \ + ext-ms-win-uiacore-l1.dll; \ + ext-ms-win-uxtheme-themes-l1.dll; \ + +DLOAD_ERROR_HANDLER = kernelbase + +#INCLUDES = $(INCLUDES); \ +# ..\..\..\inc; \ +# $(SDKTOOLS_INC_PATH)\WexTest\Cue; \ +# +#SOURCES = $(SOURCES) \ +# +# +#TARGETLIBS = $(TARGETLIBS) \ +# $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Common.lib \ +# $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Wex.Logger.lib \ +# $(ONECORESDKTOOLS_INTERNAL_LIB_PATH_L)\WexTest\Cue\Te.Common.lib \ +# $(SDKTOOLS_LIB_PATH)\WexTest\Cue\Mock10.lib \ + diff --git a/src/terminal/adapter/ut_adapter/sources.dep b/src/terminal/adapter/ut_adapter/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/terminal/adapter/ut_adapter/testmd.definition b/src/terminal/adapter/ut_adapter/testmd.definition new file mode 100644 index 000000000..e67fb2208 --- /dev/null +++ b/src/terminal/adapter/ut_adapter/testmd.definition @@ -0,0 +1,18 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "VirtualTerminal-Adapter-UnitTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ ] + }, + "Logs": [ ], + "Plugins": [ ] +} diff --git a/src/terminal/dirs b/src/terminal/dirs new file mode 100644 index 000000000..21f3f4db4 --- /dev/null +++ b/src/terminal/dirs @@ -0,0 +1,4 @@ +DIRS=parser \ + adapter \ + input + diff --git a/src/terminal/input/dirs b/src/terminal/input/dirs new file mode 100644 index 000000000..dc14638bf --- /dev/null +++ b/src/terminal/input/dirs @@ -0,0 +1,2 @@ +DIRS=lib \ + ut_adapter \ diff --git a/src/terminal/input/lib/sources b/src/terminal/input/lib/sources new file mode 100644 index 000000000..cdf027f77 --- /dev/null +++ b/src/terminal/input/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTermInput +TARGETTYPE = LIBRARY diff --git a/src/terminal/input/lib/terminalinput.vcxproj b/src/terminal/input/lib/terminalinput.vcxproj new file mode 100644 index 000000000..aed8b45e4 --- /dev/null +++ b/src/terminal/input/lib/terminalinput.vcxproj @@ -0,0 +1,29 @@ + + + + + + + Create + + + + + + + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + + {1CF55140-EF6A-4736-A403-957E4F7430BB} + Win32Proj + adapter + TerminalInput + TerminalInput + + + + + diff --git a/src/terminal/input/precomp.cpp b/src/terminal/input/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/terminal/input/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/terminal/input/precomp.h b/src/terminal/input/precomp.h new file mode 100644 index 000000000..56c7167c0 --- /dev/null +++ b/src/terminal/input/precomp.h @@ -0,0 +1,16 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include "..\..\inc\conattrs.hpp" diff --git a/src/terminal/input/sources.inc b/src/terminal/input/sources.inc new file mode 100644 index 000000000..50ef9b057 --- /dev/null +++ b/src/terminal/input/sources.inc @@ -0,0 +1,45 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Virtual Terminal Input +# ------------------------------------- + +# This module converts Windows-style input into Virtual Terminal style character +# sequences for consumption from vt-aware applications. + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES= \ + ..\terminalInput.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + +TARGETLIBS= \ + $(TARGETLIBS) \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + +DLOAD_ERROR_HANDLER = kernelbase + +DELAYLOAD = \ + ext-ms-win-ntuser-keyboard-l1.dll; \ diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp new file mode 100644 index 000000000..c189928c4 --- /dev/null +++ b/src/terminal/input/terminalInput.cpp @@ -0,0 +1,553 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "terminalInput.hpp" + +#include "strsafe.h" + +#define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES +#include + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "..\..\interactivity\inc\VtApiRedirection.hpp" +#endif + +#include "..\..\inc\unicode.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +DWORD const dwAltGrFlags = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED; + +TerminalInput::TerminalInput(_In_ std::function>&)> pfn) +{ + _pfnWriteEvents = pfn; +} + +TerminalInput::~TerminalInput() +{ + +} + +// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys +// For the source for these tables. +// Also refer to the values in terminfo for kcub1, kcud1, kcuf1, kcuu1, kend, khome. +// the 'xterm' setting lists the application mode versions of these sequences. +const TerminalInput::_TermKeyMap TerminalInput::s_rgCursorKeysNormalMapping[] +{ + { VK_UP, L"\x1b[A" }, + { VK_DOWN, L"\x1b[B" }, + { VK_RIGHT, L"\x1b[C" }, + { VK_LEFT, L"\x1b[D" }, + { VK_HOME, L"\x1b[H" }, + { VK_END, L"\x1b[F" }, +}; + +const TerminalInput::_TermKeyMap TerminalInput::s_rgCursorKeysApplicationMapping[] +{ + { VK_UP, L"\x1bOA" }, + { VK_DOWN, L"\x1bOB" }, + { VK_RIGHT, L"\x1bOC" }, + { VK_LEFT, L"\x1bOD" }, + { VK_HOME, L"\x1bOH" }, + { VK_END, L"\x1bOF" }, +}; + +const TerminalInput::_TermKeyMap TerminalInput::s_rgKeypadNumericMapping[] +{ + // HEY YOU. UPDATE THE MAX LENGTH DEF WHEN YOU MAKE CHANGES HERE. + { VK_TAB, L"\x09"}, + { VK_BACK, L"\x7f"}, + { VK_PAUSE, L"\x1a" }, + { VK_ESCAPE, L"\x1b" }, + { VK_INSERT, L"\x1b[2~" }, + { VK_DELETE, L"\x1b[3~" }, + { VK_PRIOR, L"\x1b[5~" }, + { VK_NEXT, L"\x1b[6~" }, + { VK_F1, L"\x1bOP" }, // also \x1b[11~, PuTTY uses \x1b\x1b[A + { VK_F2, L"\x1bOQ" }, // also \x1b[12~, PuTTY uses \x1b\x1b[B + { VK_F3, L"\x1bOR" }, // also \x1b[13~, PuTTY uses \x1b\x1b[C + { VK_F4, L"\x1bOS" }, // also \x1b[14~, PuTTY uses \x1b\x1b[D + { VK_F5, L"\x1b[15~" }, + { VK_F6, L"\x1b[17~" }, + { VK_F7, L"\x1b[18~" }, + { VK_F8, L"\x1b[19~" }, + { VK_F9, L"\x1b[20~" }, + { VK_F10, L"\x1b[21~" }, + { VK_F11, L"\x1b[23~" }, + { VK_F12, L"\x1b[24~" }, +}; + +//Application mode - Some terminals support both a "Numeric" input mode, and an "Application" mode +// The standards vary on what each key translates to in the various modes, so I tried to make it as close +// to the VT220 standard as possible. +// The notable difference is in the arrow keys, which in application mode translate to "^[0A" (etc) as opposed to "^[[A" in numeric +//Some very unclear documentation at http://invisible-island.net/xterm/ctlseqs/ctlseqs.html also suggests alternate encodings for F1-4 +// which I have left in the comments on those entries as something to possibly add in the future, if need be. +//It seems to me as though this was used for early numpad implementations, where presently numlock would enable +// "numeric" mode, outputting the numbers on the keys, while "application" mode does things like pgup/down, arrow keys, etc. +//These keys aren't translated at all in numeric mode, so I figured I'd leave them out of the numeric table. +const TerminalInput::_TermKeyMap TerminalInput::s_rgKeypadApplicationMapping[] +{ + // HEY YOU. UPDATE THE MAX LENGTH DEF WHEN YOU MAKE CHANGES HERE. + { VK_TAB, L"\x09" }, + { VK_BACK, L"\x7f" }, + { VK_PAUSE, L"\x1a" }, + { VK_ESCAPE, L"\x1b" }, + { VK_INSERT, L"\x1b[2~" }, + { VK_DELETE, L"\x1b[3~" }, + { VK_PRIOR, L"\x1b[5~" }, + { VK_NEXT, L"\x1b[6~" }, + { VK_F1, L"\x1bOP" }, // also \x1b[11~, PuTTY uses \x1b\x1b[A + { VK_F2, L"\x1bOQ" }, // also \x1b[12~, PuTTY uses \x1b\x1b[B + { VK_F3, L"\x1bOR" }, // also \x1b[13~, PuTTY uses \x1b\x1b[C + { VK_F4, L"\x1bOS" }, // also \x1b[14~, PuTTY uses \x1b\x1b[D + { VK_F5, L"\x1b[15~" }, + { VK_F6, L"\x1b[17~" }, + { VK_F7, L"\x1b[18~" }, + { VK_F8, L"\x1b[19~" }, + { VK_F9, L"\x1b[20~" }, + { VK_F10, L"\x1b[21~" }, + { VK_F11, L"\x1b[23~" }, + { VK_F12, L"\x1b[24~" }, + // The numpad has a variety of mappings, none of which seem standard or really configurable by the OS. + // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys + // to see just how convoluted this all is. + // PuTTY uses a set of mappings that don't work in ViM without reamapping them back to the numpad + // (see http://vim.wikia.com/wiki/PuTTY_numeric_keypad_mappings#Comments) + // I think the best solution is to just not do any for the time being. + // Putty also provides configuration for choosing which of the 5 mappings it has through the settings, which is more work than we can manage now. + // { VK_MULTIPLY, L"\x1bOj" }, // PuTTY: \x1bOR (I believe putty is treating the top row of the numpad as PF1-PF4) + // { VK_ADD, L"\x1bOk" }, // PuTTY: \x1bOl, \x1bOm (with shift) + // { VK_SEPARATOR, L"\x1bOl" }, // ? I'm not sure which key this is... + // { VK_SUBTRACT, L"\x1bOm" }, // \x1bOS + // { VK_DECIMAL, L"\x1bOn" }, // \x1bOn + // { VK_DIVIDE, L"\x1bOo" }, // \x1bOQ + // { VK_NUMPAD0, L"\x1bOp" }, + // { VK_NUMPAD1, L"\x1bOq" }, + // { VK_NUMPAD2, L"\x1bOr" }, + // { VK_NUMPAD3, L"\x1bOs" }, + // { VK_NUMPAD4, L"\x1bOt" }, + // { VK_NUMPAD5, L"\x1bOu" }, // \x1b0E + // { VK_NUMPAD5, L"\x1bOE" }, // PuTTY \x1b[G + // { VK_NUMPAD6, L"\x1bOv" }, + // { VK_NUMPAD7, L"\x1bOw" }, + // { VK_NUMPAD8, L"\x1bOx" }, + // { VK_NUMPAD9, L"\x1bOy" }, + // { '=', L"\x1bOX" }, // I've also seen these codes mentioned in some documentation, + // { VK_SPACE, L"\x1bO " }, // but I wasn't really sure if they should be included or not... + // { VK_TAB, L"\x1bOI" }, // So I left them here as a reference just in case. +}; + +// Sequences to send when a modifier is pressed with any of these keys +// Basically, the 'm' will be replaced with a character indicating which +// modifier keys are pressed. +const TerminalInput::_TermKeyMap TerminalInput::s_rgModifierKeyMapping[] +{ + // HEY YOU. UPDATE THE MAX LENGTH DEF WHEN YOU MAKE CHANGES HERE. + { VK_UP, L"\x1b[1;mA" }, + { VK_DOWN, L"\x1b[1;mB" }, + { VK_RIGHT, L"\x1b[1;mC" }, + { VK_LEFT, L"\x1b[1;mD" }, + { VK_HOME, L"\x1b[1;mH" }, + { VK_END, L"\x1b[1;mF" }, + { VK_F1, L"\x1b[1;mP" }, + { VK_F2, L"\x1b[1;mQ" }, + { VK_F3, L"\x1b[1;mR" }, + { VK_F4, L"\x1b[1;mS" }, + { VK_INSERT, L"\x1b[2;m~" }, + { VK_DELETE, L"\x1b[3;m~" }, + { VK_PRIOR, L"\x1b[5;m~" }, + { VK_NEXT, L"\x1b[6;m~" }, + { VK_F5, L"\x1b[15;m~" }, + { VK_F6, L"\x1b[17;m~" }, + { VK_F7, L"\x1b[18;m~" }, + { VK_F8, L"\x1b[19;m~" }, + { VK_F9, L"\x1b[20;m~" }, + { VK_F10, L"\x1b[21;m~" }, + { VK_F11, L"\x1b[23;m~" }, + { VK_F12, L"\x1b[24;m~" }, + // Ubuntu's inputrc also defines \x1b[5C, \x1b\x1bC (and D) as 'forward/backward-word' mappings + // I believe '\x1b\x1bC' is listed because the C1 ESC (x9B) gets encoded as + // \xC2\x9B, but then translated to \x1b\x1b if the C1 codepoint isn't supported by the current encoding +}; + +// Sequences to send when a modifier is pressed with any of these keys +// These sequences are not later updated to encode the modifier state in the +// sequence itself, they are just weird exceptional cases to the general +// rules above. +const TerminalInput::_TermKeyMap TerminalInput::s_rgSimpleModifedKeyMapping[] +{ + // HEY YOU. UPDATE THE MAX LENGTH DEF WHEN YOU MAKE CHANGES HERE. + { VK_BACK, CTRL_PRESSED, L"\x8"}, + { VK_BACK, ALT_PRESSED, L"\x1b\x7f"}, + { VK_BACK, CTRL_PRESSED | ALT_PRESSED, L"\x1b\x8"}, + { VK_TAB, CTRL_PRESSED, L"\t"}, + { VK_TAB, SHIFT_PRESSED, L"\x1b[Z"}, + { VK_DIVIDE, CTRL_PRESSED, L"\x1F"}, + // These two are not implemented here, because they are system keys. + // { VK_TAB, ALT_PRESSED, L""}, This is the Windows system shortcut for switching windows. + // { VK_ESCAPE, ALT_PRESSED, L""}, This is another Windows system shortcut for switching windows. +}; + +const wchar_t* const CTRL_SLASH_SEQUENCE = L"\x1f"; + +// Do NOT include the null terminator in the count. +const size_t TerminalInput::_TermKeyMap::s_cchMaxSequenceLength = 7; // UPDATE THIS DEF WHEN THE LONGEST MAPPED STRING CHANGES + +const size_t TerminalInput::s_cCursorKeysNormalMapping = ARRAYSIZE(s_rgCursorKeysNormalMapping); +const size_t TerminalInput::s_cCursorKeysApplicationMapping = ARRAYSIZE(s_rgCursorKeysApplicationMapping); +const size_t TerminalInput::s_cKeypadNumericMapping = ARRAYSIZE(s_rgKeypadNumericMapping); +const size_t TerminalInput::s_cKeypadApplicationMapping = ARRAYSIZE(s_rgKeypadApplicationMapping); +const size_t TerminalInput::s_cModifierKeyMapping = ARRAYSIZE(s_rgModifierKeyMapping); +const size_t TerminalInput::s_cSimpleModifedKeyMapping = ARRAYSIZE(s_rgSimpleModifedKeyMapping); + +void TerminalInput::ChangeKeypadMode(const bool fApplicationMode) +{ + _fKeypadApplicationMode = fApplicationMode; +} + +void TerminalInput::ChangeCursorKeysMode(const bool fApplicationMode) +{ + _fCursorApplicationMode = fApplicationMode; +} + +const size_t TerminalInput::GetKeyMappingLength(const KeyEvent& keyEvent) const +{ + size_t length = 0; + if (keyEvent.IsCursorKey()) + { + length = (_fCursorApplicationMode) ? s_cCursorKeysApplicationMapping : s_cCursorKeysNormalMapping; + } + else + { + length = (_fKeypadApplicationMode) ? s_cKeypadApplicationMapping : s_cKeypadNumericMapping; + } + return length; +} + +const TerminalInput::_TermKeyMap* TerminalInput::GetKeyMapping(const KeyEvent& keyEvent) const +{ + const TerminalInput::_TermKeyMap* mapping = nullptr; + + if (keyEvent.IsCursorKey()) + { + mapping = (_fCursorApplicationMode) ? s_rgCursorKeysApplicationMapping : s_rgCursorKeysNormalMapping; + } + else + { + mapping = (_fKeypadApplicationMode) ? s_rgKeypadApplicationMapping : s_rgKeypadNumericMapping; + } + return mapping; +} + +// 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 +// Return Value: +// - True if there was a match to a key translation, and we successfully modified and sent it to the input +bool TerminalInput::_SearchWithModifier(const KeyEvent& keyEvent) const +{ + + const TerminalInput::_TermKeyMap* pMatchingMapping; + bool fSuccess = _SearchKeyMapping(keyEvent, + s_rgModifierKeyMapping, + s_cModifierKeyMapping, + &pMatchingMapping); + if (fSuccess) + { + size_t cch = 0; + if (SUCCEEDED(StringCchLengthW(pMatchingMapping->pwszSequence, _TermKeyMap::s_cchMaxSequenceLength + 1, &cch)) && + cch > 0) + { + wchar_t* rwchModifiedSequence = new(std::nothrow) wchar_t[cch + 1]; + if (rwchModifiedSequence != nullptr) + { + memcpy(rwchModifiedSequence, pMatchingMapping->pwszSequence, cch * sizeof(wchar_t)); + const bool fShift = keyEvent.IsShiftPressed(); + const bool fAlt = keyEvent.IsAltPressed(); + const bool fCtrl = keyEvent.IsCtrlPressed(); + rwchModifiedSequence[cch - 2] = L'1' + (fShift ? 1 : 0) + (fAlt ? 2 : 0) + (fCtrl ? 4 : 0); + rwchModifiedSequence[cch] = 0; + _SendInputSequence(rwchModifiedSequence); + fSuccess = true; + delete [] rwchModifiedSequence; + } + } + } + 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. + fSuccess = _SearchKeyMapping(keyEvent, + s_rgSimpleModifedKeyMapping, + s_cSimpleModifedKeyMapping, + &pMatchingMapping); + if (fSuccess) + { + // This mapping doesn't need to be changed at all. + _SendInputSequence(pMatchingMapping->pwszSequence); + fSuccess = true; + } + else + { + // One last check: C-/ is supposed to be C-_ + // But '/' is not the same VKEY on all keyboards. So we have to + // figure out the vkey at runtime. + const BYTE slashVkey = LOBYTE(VkKeyScan(L'/')); + if (keyEvent.GetVirtualKeyCode() == slashVkey && keyEvent.IsCtrlPressed()) + { + // This mapping doesn't need to be changed at all. + _SendInputSequence(CTRL_SLASH_SEQUENCE); + fSuccess = true; + + } + } + } + + return fSuccess; +} + +// Routine Description: +// - Searches the keyMapping for a entry corresponding to this key event, and returns it. +// Arguments: +// - keyEvent - Key event to translate +// - keyMapping - Array of key mappings to search +// - cKeyMapping - number of entries in keyMapping +// - pMatchingMapping - Where to put the pointer to the found match +// Return Value: +// - True if there was a match to a key translation +bool TerminalInput::_SearchKeyMapping(const KeyEvent& keyEvent, + _In_reads_(cKeyMapping) const TerminalInput::_TermKeyMap* keyMapping, + const size_t cKeyMapping, + _Out_ const TerminalInput::_TermKeyMap** pMatchingMapping) const +{ + bool fKeyTranslated = false; + for (size_t i = 0; i < cKeyMapping; i++) + { + const _TermKeyMap* const pMap = &(keyMapping[i]); + + if (pMap->wVirtualKey == keyEvent.GetVirtualKeyCode()) + { + // If the mapping has no modifiers set, then it doesn't really care + // what the modifiers are on the key. The caller will likely do + // something with them. + // However, if there are modifiers set, then we only want to match + // if the key's modifiers are the same as the modifiers in the + // mapping. + bool modifiersMatch = WI_AreAllFlagsClear(pMap->dwModifiers, MOD_PRESSED); + if (!modifiersMatch) + { + // The modifier mapping expects certain modifier keys to be + // pressed. Check those as well. + modifiersMatch = + (WI_IsFlagSet(pMap->dwModifiers, SHIFT_PRESSED) == keyEvent.IsShiftPressed()) && + (WI_IsAnyFlagSet(pMap->dwModifiers, ALT_PRESSED) == keyEvent.IsAltPressed()) && + (WI_IsAnyFlagSet(pMap->dwModifiers, CTRL_PRESSED) == keyEvent.IsCtrlPressed()); + } + + if (modifiersMatch) + { + fKeyTranslated = true; + *pMatchingMapping = pMap; + break; + } + } + } + return fKeyTranslated; +} + +// 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 +// - cKeyMapping - number of entries in keyMapping +// Return Value: +// - True if there was a match to a key translation, and we successfully sent it to the input +bool TerminalInput::_TranslateDefaultMapping(const KeyEvent& keyEvent, + _In_reads_(cKeyMapping) const TerminalInput::_TermKeyMap* keyMapping, + const size_t cKeyMapping) const +{ + const TerminalInput::_TermKeyMap* pMatchingMapping; + bool fSuccess = _SearchKeyMapping(keyEvent, keyMapping, cKeyMapping, &pMatchingMapping); + if (fSuccess) + { + _SendInputSequence(pMatchingMapping->pwszSequence); + fSuccess = true; + } + return fSuccess; +} + +bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) const +{ + // By default, we fail to handle the key + bool fKeyHandled = false; + + // On key presses, prepare to translate to VT compatible sequences + if (pInEvent->EventType() == InputEventType::KeyEvent) + { + KeyEvent keyEvent = *static_cast(pInEvent); + + // Only need to handle key down. See raw key handler (see RawReadWaitRoutine in stream.cpp) + if (keyEvent.IsKeyDown()) + { + // For AltGr enabled keyboards, the Windows system will + // emit Left Ctrl + Right Alt as the modifier keys and + // will have pretranslated the UnicodeChar to the proper + // alternative value. + // Through testing with Ubuntu, PuTTY, and Emacs for + // Windows, it was discovered that any instance of Left + // Ctrl + Right Alt will strip out those two modifiers and + // send the unicode value straight through to the system. + // Holding additional modifiers in addition to Left Ctrl + + // Right Alt will then light those modifiers up again for + // the unicode value. + // Therefore to handle AltGr properly, our first step + // needs to be to check if both Left Ctrl + Right Alt are + // pressed... + // ... and if they are both pressed, strip them out of the control key state. + if (keyEvent.IsAltGrPressed()) + { + keyEvent.DeactivateModifierKey(ModifierKeyState::LeftCtrl); + keyEvent.DeactivateModifierKey(ModifierKeyState::RightAlt); + } + + if (keyEvent.IsAltPressed() && + keyEvent.IsCtrlPressed() && + (keyEvent.GetCharData() == 0 || keyEvent.GetCharData() == 0x20) && + ((keyEvent.GetVirtualKeyCode() > 0x40 && keyEvent.GetVirtualKeyCode() <= 0x5A) || + keyEvent.GetVirtualKeyCode() == VK_SPACE) ) + { + // For Alt+Ctrl+Key messages, the UnicodeChar is NOT the Ctrl+key char, it's null. + // So we need to get the char from the vKey. + // EXCEPT for Alt+Ctrl+Space. Then the UnicodeChar is space, not NUL. + wchar_t wchPressedChar = static_cast(MapVirtualKeyW(keyEvent.GetVirtualKeyCode(), MAPVK_VK_TO_CHAR)); + // This is a trick - C-Spc is supposed to send NUL. So quick change space -> @ (0x40) + wchPressedChar = (wchPressedChar == UNICODE_SPACE) ? 0x40 : wchPressedChar; + if (wchPressedChar >= 0x40 && wchPressedChar < 0x7F) + { + //shift the char to the ctrl range + wchPressedChar -= 0x40; + _SendEscapedInputSequence(wchPressedChar); + fKeyHandled = true; + } + } + + // If a modifier key was pressed, then we need to try and send the modified sequence. + if (!fKeyHandled && keyEvent.IsModifierPressed()) + { + // Translate the key using the modifier table + fKeyHandled = _SearchWithModifier(keyEvent); + } + // ALT is a sequence of ESC + KEY. + if (!fKeyHandled && keyEvent.GetCharData() != 0 && keyEvent.IsAltPressed()) + { + _SendEscapedInputSequence(keyEvent.GetCharData()); + fKeyHandled = true; + } + if (!fKeyHandled && keyEvent.IsCtrlPressed()) + { + if ((keyEvent.GetCharData() == UNICODE_SPACE ) || // Ctrl+Space + // when Ctrl+@ comes through, the unicodechar + // will be '\x0' (UNICODE_NULL), and the vkey will be + // VkKeyScanW(0), the vkey for null + (keyEvent.GetCharData() == UNICODE_NULL && keyEvent.GetVirtualKeyCode() == LOBYTE(VkKeyScanW(0)))) + { + _SendNullInputSequence(keyEvent.GetActiveModifierKeys()); + fKeyHandled = true; + } + } + + if (!fKeyHandled) + { + // For perf optimization, filter out any typically printable Virtual Keys (e.g. A-Z) + // This is in lieu of an O(1) sparse table or other such less-maintanable methods. + // VK_CANCEL is an exception and we want to send the associated uChar as is. + if ((keyEvent.GetVirtualKeyCode() < '0' || keyEvent.GetVirtualKeyCode() > 'Z') && + keyEvent.GetVirtualKeyCode() != VK_CANCEL) + { + fKeyHandled = _TranslateDefaultMapping(keyEvent, GetKeyMapping(keyEvent), GetKeyMappingLength(keyEvent)); + } + else + { + WCHAR rgwchSequence[2]; + rgwchSequence[0] = keyEvent.GetCharData(); + rgwchSequence[1] = UNICODE_NULL; + _SendInputSequence(rgwchSequence); + fKeyHandled = true; + } + } + } + } + + return fKeyHandled; +} + +// 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()); + } +} + +void TerminalInput::_SendNullInputSequence(const DWORD dwControlKeyState) const +{ + try + { + std::deque> inputEvents; + inputEvents.push_back(std::make_unique(true, + 1ui16, + LOBYTE(VkKeyScanW(0)), + 0ui16, + L'\x0', + dwControlKeyState)); + _pfnWriteEvents(inputEvents); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } +} + +void TerminalInput::_SendInputSequence(_In_ PCWSTR const pwszSequence) const +{ + size_t cch = 0; + // + 1 to max sequence length for null terminator count which is required by StringCchLengthW + if (SUCCEEDED(StringCchLengthW(pwszSequence, _TermKeyMap::s_cchMaxSequenceLength + 1, &cch)) && cch > 0) + { + try + { + std::deque> inputEvents; + for (size_t i = 0; i < cch; i++) + { + inputEvents.push_back(std::make_unique(true, 1ui16, 0ui16, 0ui16, pwszSequence[i], 0)); + } + _pfnWriteEvents(inputEvents); + } + catch (...) + { + LOG_HR(wil::ResultFromCaughtException()); + } + } +} diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp new file mode 100644 index 000000000..d4a71f454 --- /dev/null +++ b/src/terminal/input/terminalInput.hpp @@ -0,0 +1,95 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- terminalInput.hpp + +Abstract: +- This serves as an adapter between virtual key input from a user and the virtual terminal sequences that are + typically emitted by an xterm-compatible console. + +Author(s): +- Michael Niksa (MiNiksa) 30-Oct-2015 +--*/ + +#include +#include "../../types/inc/IInputEvent.hpp" +#pragma once + +namespace Microsoft::Console::VirtualTerminal +{ + class TerminalInput final + { + public: + TerminalInput(_In_ std::function>&)> pfn); + ~TerminalInput(); + + bool HandleKey(const IInputEvent* const pInEvent) const; + void ChangeKeypadMode(const bool fApplicationMode); + void ChangeCursorKeysMode(const bool fApplicationMode); + + private: + + std::function>&)> _pfnWriteEvents; + bool _fKeypadApplicationMode = false; + bool _fCursorApplicationMode = false; + + void _SendNullInputSequence(const DWORD dwControlKeyState) const; + void _SendInputSequence(_In_ PCWSTR const pwszSequence) const; + void _SendEscapedInputSequence(const wchar_t wch) const; + + struct _TermKeyMap + { + WORD const wVirtualKey; + PCWSTR const pwszSequence; + DWORD const dwModifiers; + + static const size_t s_cchMaxSequenceLength; + + _TermKeyMap(const WORD wVirtualKey, _In_ PCWSTR const pwszSequence) : + wVirtualKey(wVirtualKey), + pwszSequence(pwszSequence), + dwModifiers(0) {}; + + _TermKeyMap(const WORD wVirtualKey, const DWORD dwModifiers, _In_ PCWSTR const pwszSequence) : + wVirtualKey(wVirtualKey), + pwszSequence(pwszSequence), + dwModifiers(dwModifiers) {}; + + // C++11 syntax for prohibiting assignment + // We can't assign, everything here is const. + // We also shouldn't need to, this is only for a specific table. + _TermKeyMap& operator=(const _TermKeyMap&) = delete; + }; + + static const _TermKeyMap s_rgCursorKeysNormalMapping[]; + static const _TermKeyMap s_rgCursorKeysApplicationMapping[]; + static const _TermKeyMap s_rgKeypadNumericMapping[]; + static const _TermKeyMap s_rgKeypadApplicationMapping[]; + static const _TermKeyMap s_rgModifierKeyMapping[]; + static const _TermKeyMap s_rgSimpleModifedKeyMapping[]; + + static const size_t s_cCursorKeysNormalMapping; + static const size_t s_cCursorKeysApplicationMapping; + static const size_t s_cKeypadNumericMapping; + static const size_t s_cKeypadApplicationMapping; + static const size_t s_cModifierKeyMapping; + static const size_t s_cSimpleModifedKeyMapping; + + bool _SearchKeyMapping(const KeyEvent& keyEvent, + _In_reads_(cKeyMapping) const TerminalInput::_TermKeyMap* keyMapping, + const size_t cKeyMapping, + _Out_ const TerminalInput::_TermKeyMap** pMatchingMapping) const; + bool _TranslateDefaultMapping(const KeyEvent& keyEvent, + _In_reads_(cKeyMapping) const TerminalInput::_TermKeyMap* keyMapping, + const size_t cKeyMapping) const; + bool _SearchWithModifier(const KeyEvent& keyEvent) const; + + + public: + const size_t GetKeyMappingLength(const KeyEvent& keyEvent) const; + const _TermKeyMap* GetKeyMapping(const KeyEvent& keyEvent) const; + + }; +} diff --git a/src/terminal/parser/IStateMachineEngine.hpp b/src/terminal/parser/IStateMachineEngine.hpp new file mode 100644 index 000000000..1db380f66 --- /dev/null +++ b/src/terminal/parser/IStateMachineEngine.hpp @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +/* +Module Name: +- IStateMachineEngine.hpp + +Abstract: +- This is the interface for a VT state machine language + The terminal handles input sequences and output sequences differently, + almost as two seperate grammars. This enables different grammars to leverage + the existing VT parsing. +*/ +#pragma once +namespace Microsoft::Console::VirtualTerminal +{ + class IStateMachineEngine + { + public: + + virtual ~IStateMachineEngine() = 0; + + virtual bool ActionExecute(const wchar_t wch) = 0; + virtual bool ActionExecuteFromEscape(const wchar_t wch) = 0; + virtual bool ActionPrint(const wchar_t wch) = 0; + virtual bool ActionPrintString(const wchar_t* const rgwch, + size_t const cch) = 0; + + virtual bool ActionPassThroughString(const wchar_t* const rgwch, + size_t const cch) = 0; + + virtual bool ActionEscDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate) = 0; + virtual bool ActionCsiDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) = 0; + + virtual bool ActionClear() = 0; + + virtual bool ActionIgnore() = 0; + + virtual bool ActionOscDispatch(const wchar_t wch, + const unsigned short sOscParam, + _Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString) = 0; + + virtual bool ActionSs3Dispatch(const wchar_t wch, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) = 0; + + virtual bool FlushAtEndOfString() const = 0; + virtual bool DispatchControlCharsFromEscape() const = 0; + + }; + + inline IStateMachineEngine::~IStateMachineEngine() {} +} diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp new file mode 100644 index 000000000..5ebd60834 --- /dev/null +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -0,0 +1,981 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "stateMachine.hpp" +#include "InputStateMachineEngine.hpp" + +#include "../../inc/unicode.hpp" +#include "ascii.hpp" + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "../../interactivity/inc/VtApiRedirection.hpp" +#endif + +using namespace Microsoft::Console::VirtualTerminal; + +// The values used by VkKeyScan to encode modifiers in the high order byte +const short KEYSCAN_SHIFT = 1; +const short KEYSCAN_CTRL = 2; +const short KEYSCAN_ALT = 4; + +// The values with which VT encodes modifier values. +const short VT_SHIFT = 1; +const short VT_ALT = 2; +const short VT_CTRL = 4; + +const size_t WRAPPED_SEQUENCE_MAX_LENGTH = 8; + +// For reference, the equivalent INPUT_RECORD values are: +// RIGHT_ALT_PRESSED 0x0001 +// LEFT_ALT_PRESSED 0x0002 +// RIGHT_CTRL_PRESSED 0x0004 +// LEFT_CTRL_PRESSED 0x0008 +// SHIFT_PRESSED 0x0010 +// NUMLOCK_ON 0x0020 +// SCROLLLOCK_ON 0x0040 +// CAPSLOCK_ON 0x0080 +// ENHANCED_KEY 0x0100 + +const InputStateMachineEngine::CSI_TO_VKEY InputStateMachineEngine::s_rgCsiMap[] +{ + { CsiActionCodes::ArrowUp, VK_UP }, + { CsiActionCodes::ArrowDown, VK_DOWN }, + { CsiActionCodes::ArrowRight, VK_RIGHT }, + { CsiActionCodes::ArrowLeft, VK_LEFT }, + { CsiActionCodes::Home, VK_HOME }, + { CsiActionCodes::End, VK_END }, + { CsiActionCodes::CSI_F1, VK_F1 }, + { CsiActionCodes::CSI_F2, VK_F2 }, + { CsiActionCodes::CSI_F3, VK_F3 }, + { CsiActionCodes::CSI_F4, VK_F4 }, +}; + +const InputStateMachineEngine::GENERIC_TO_VKEY InputStateMachineEngine::s_rgGenericMap[] +{ + { GenericKeyIdentifiers::GenericHome, VK_HOME }, + { GenericKeyIdentifiers::Insert, VK_INSERT }, + { GenericKeyIdentifiers::Delete, VK_DELETE }, + { GenericKeyIdentifiers::GenericEnd, VK_END }, + { GenericKeyIdentifiers::Prior, VK_PRIOR }, + { GenericKeyIdentifiers::Next, VK_NEXT }, + { GenericKeyIdentifiers::F5, VK_F5 }, + { GenericKeyIdentifiers::F6, VK_F6 }, + { GenericKeyIdentifiers::F7, VK_F7 }, + { GenericKeyIdentifiers::F8, VK_F8 }, + { GenericKeyIdentifiers::F9, VK_F9 }, + { GenericKeyIdentifiers::F10, VK_F10 }, + { GenericKeyIdentifiers::F11, VK_F11 }, + { GenericKeyIdentifiers::F12, VK_F12 }, +}; + +const InputStateMachineEngine::SS3_TO_VKEY InputStateMachineEngine::s_rgSs3Map[] +{ + { Ss3ActionCodes::SS3_F1, VK_F1 }, + { Ss3ActionCodes::SS3_F2, VK_F2 }, + { Ss3ActionCodes::SS3_F3, VK_F3 }, + { Ss3ActionCodes::SS3_F4, VK_F4 }, +}; + +InputStateMachineEngine::InputStateMachineEngine(IInteractDispatch* const pDispatch) : + InputStateMachineEngine(pDispatch, false) +{} + +InputStateMachineEngine::InputStateMachineEngine(IInteractDispatch* const pDispatch, const bool lookingForDSR) : + _pDispatch(THROW_IF_NULL_ALLOC(pDispatch)), + _lookingForDSR(lookingForDSR) +{ +} + +// Method Description: +// - Triggers the Execute action to indicate that the listener should +// immediately respond to a C0 control character. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionExecute(const wchar_t wch) +{ + return _DoControlCharacter(wch, false); +} + +bool InputStateMachineEngine::_DoControlCharacter(const wchar_t wch, const bool writeAlt) +{ + bool fSuccess = false; + if (wch == UNICODE_ETX && !writeAlt) + { + // This is Ctrl+C, which is handled specially by the host. + fSuccess = _pDispatch->WriteCtrlC(); + } + else if (wch >= '\x0' && wch < '\x20') + { + // This is a C0 Control Character. + // This should be translated as Ctrl+(wch+x40) + wchar_t actualChar = wch; + bool writeCtrl = true; + + short vkey = 0; + DWORD dwModifierState = 0; + + switch(wch) + { + case L'\b': + fSuccess = _GenerateKeyFromChar(wch+0x40, &vkey, nullptr); + break; + case L'\r': + writeCtrl = false; + fSuccess = _GenerateKeyFromChar(wch, &vkey, nullptr); + break; + case L'\x1b': + // Translate escape as the ESC key, NOT C-[. + // This means that C-[ won't insert ^[ into the buffer anymore, + // which isn't the worst tradeoff. + vkey = VK_ESCAPE; + writeCtrl = false; + fSuccess = true; + break; + case L'\t': + writeCtrl = false; + fSuccess = _GenerateKeyFromChar(actualChar, &vkey, &dwModifierState); + break; + default: + fSuccess = _GenerateKeyFromChar(actualChar, &vkey, &dwModifierState); + break; + } + + if (fSuccess) + { + if (writeCtrl) + { + WI_SetFlag(dwModifierState, LEFT_CTRL_PRESSED); + } + if (writeAlt) + { + WI_SetFlag(dwModifierState, LEFT_ALT_PRESSED); + } + + fSuccess = _WriteSingleKey(actualChar, vkey, dwModifierState); + } + } + else if (wch == '\x7f') + { + // Note: + // The windows telnet expects to send x7f as DELETE, not backspace. + // However, the windows telnetd also wouldn't let you move the + // cursor back into the input line, so it wasn't possible to + // "delete" any input at all, only backspace. + // Because of this, we're treating x7f as backspace, like most + // terminals do. + fSuccess = _WriteSingleKey('\x8', VK_BACK, writeAlt ? LEFT_ALT_PRESSED : 0); + } + else + { + fSuccess = ActionPrint(wch); + } + return fSuccess; +} + + +// Routine Description: +// - Triggers the Execute action to indicate that the listener should +// immediately respond to a C0 control character. +// This is called from the Escape state in the state machine, indicating the +// immediately previous character was an 0x1b. +// We need to override this method to properly treat 0x1b + C0 strings as +// Ctrl+Alt+ input sequences. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionExecuteFromEscape(const wchar_t wch) +{ + return _DoControlCharacter(wch, true); +} + +// Method Description: +// - Triggers the Print action to indicate that the listener should render the +// character given. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionPrint(const wchar_t wch) +{ + short vkey = 0; + DWORD dwModifierState = 0; + bool fSuccess = _GenerateKeyFromChar(wch, &vkey, &dwModifierState); + if (fSuccess) + { + fSuccess = _WriteSingleKey(wch, vkey, dwModifierState); + } + return fSuccess; +} + +// Method Description: +// - Triggers the Print action to indicate that the listener should render the +// string of characters given. +// Arguments: +// - rgwch - string to dispatch. +// - cch - length of rgwch +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionPrintString(const wchar_t* const rgwch, + const size_t cch) +{ + if (cch == 0) + { + return true; + } + return _pDispatch->WriteString(rgwch, cch); +} + +// Method Description: +// - Triggers the Print action to indicate that the listener should render the +// string of characters given. +// Arguments: +// - rgwch - string to dispatch. +// - cch - length of rgwch +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionPassThroughString(const wchar_t* const rgwch, + _In_ size_t const cch) +{ + return ActionPrintString(rgwch, cch); +} + +// Method Description: +// - Triggers the EscDispatch action to indicate that the listener should handle +// a simple escape sequence. These sequences traditionally start with ESC +// and a simple letter. No complicated parameters. +// Arguments: +// - wch - Character to dispatch. +// - cIntermediate - Number of "Intermediate" characters found - such as '!', '?' +// - wchIntermediate - Intermediate character in the sequence, if there was one. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionEscDispatch(const wchar_t wch, + const unsigned short /*cIntermediate*/, + const wchar_t /*wchIntermediate*/) +{ + bool fSuccess = false; + + // 0x7f is DEL, which we treat effectively the same as a ctrl character. + if (wch == 0x7f) + { + fSuccess = _DoControlCharacter(wch, true); + } + else + { + DWORD dwModifierState = 0; + short vk = 0; + fSuccess = _GenerateKeyFromChar(wch, &vk, &dwModifierState); + if (fSuccess) + { + // Alt is definitely pressed in the esc+key case. + dwModifierState = WI_SetFlag(dwModifierState, LEFT_ALT_PRESSED); + + fSuccess = _WriteSingleKey(wch, vk, dwModifierState); + } + } + + return fSuccess; +} + +// Method Description: +// - Triggers the CsiDispatch action to indicate that the listener should handle +// a control sequence. These sequences perform various API-type commands +// that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// - cIntermediate - Number of "Intermediate" characters found - such as '!', '?' +// - wchIntermediate - Intermediate character in the sequence, if there was one. +// - rgusParams - set of numeric parameters collected while pasring the sequence. +// - cParams - number of parameters found. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, + const unsigned short /*cIntermediate*/, + const wchar_t /*wchIntermediate*/, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) +{ + DWORD dwModifierState = 0; + short vkey = 0; + unsigned int uiFunction = 0; + unsigned int col = 0; + unsigned int row = 0; + + // This is all the args after the first arg, and the count of args not including the first one. + const unsigned short* const rgusRemainingArgs = (cParams > 1) ? rgusParams + 1 : rgusParams; + const unsigned short cRemainingArgs = (cParams >= 1) ? cParams - 1 : 0; + + bool fSuccess = false; + switch(wch) + { + case CsiActionCodes::Generic: + dwModifierState = _GetGenericKeysModifierState(rgusParams, cParams); + fSuccess = _GetGenericVkey(rgusParams, cParams, &vkey); + break; + // case CsiActionCodes::DSR_DeviceStatusReportResponse: + case CsiActionCodes::CSI_F3: + // The F3 case is special - it shares a code with the DeviceStatusResponse. + // If we're looking for that response, then do that, and break out. + // Else, fall though to the _GetCursorKeysModifierState handler. + if (_lookingForDSR) + { + fSuccess = true; + fSuccess = _GetXYPosition(rgusParams, cParams, &row, &col); + break; + } + case CsiActionCodes::ArrowUp: + case CsiActionCodes::ArrowDown: + case CsiActionCodes::ArrowRight: + case CsiActionCodes::ArrowLeft: + case CsiActionCodes::Home: + case CsiActionCodes::End: + case CsiActionCodes::CSI_F1: + case CsiActionCodes::CSI_F2: + case CsiActionCodes::CSI_F4: + dwModifierState = _GetCursorKeysModifierState(rgusParams, cParams); + fSuccess = _GetCursorKeysVkey(wch, &vkey); + break; + case CsiActionCodes::CursorBackTab: + dwModifierState = SHIFT_PRESSED; + vkey = VK_TAB; + fSuccess = true; + break; + case CsiActionCodes::DTTERM_WindowManipulation: + fSuccess = _GetWindowManipulationType(rgusParams, + cParams, + &uiFunction); + break; + default: + fSuccess = false; + break; + + } + + if (fSuccess) + { + switch(wch) + { + // case CsiActionCodes::DSR_DeviceStatusReportResponse: + case CsiActionCodes::CSI_F3: + // The F3 case is special - it shares a code with the DeviceStatusResponse. + // If we're looking for that response, then do that, and break out. + // Else, fall though to the _GetCursorKeysModifierState handler. + if (_lookingForDSR) + { + fSuccess = _pDispatch->MoveCursor(row, col); + // Right now we're only looking for on initial cursor + // position response. After that, only look for F3. + _lookingForDSR = false; + break; + } + __fallthrough; + case CsiActionCodes::Generic: + case CsiActionCodes::ArrowUp: + case CsiActionCodes::ArrowDown: + case CsiActionCodes::ArrowRight: + case CsiActionCodes::ArrowLeft: + case CsiActionCodes::Home: + case CsiActionCodes::End: + case CsiActionCodes::CSI_F1: + case CsiActionCodes::CSI_F2: + case CsiActionCodes::CSI_F4: + case CsiActionCodes::CursorBackTab: + fSuccess = _WriteSingleKey(vkey, dwModifierState); + break; + case CsiActionCodes::DTTERM_WindowManipulation: + fSuccess = _pDispatch->WindowManipulation(static_cast(uiFunction), + rgusRemainingArgs, + cRemainingArgs); + break; + default: + fSuccess = false; + break; + + } + + } + + return fSuccess; +} + +// Routine Description: +// - Triggers the Ss3Dispatch action to indicate that the listener should handle +// a control sequence. These sequences perform various API-type commands +// that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// - rgusParams - set of numeric parameters collected while pasring the sequence. +// - cParams - number of parameters found. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionSs3Dispatch(const wchar_t wch, + _In_reads_(_Param_(3)) const unsigned short* const /*rgusParams*/, + const unsigned short /*cParams*/) +{ + // Ss3 sequence keys aren't modified. + // When F1-F4 *are* modified, they're sent as CSI sequences, not SS3's. + DWORD dwModifierState = 0; + short vkey = 0; + + bool fSuccess = _GetSs3KeysVkey(wch, &vkey); + + if (fSuccess) + { + fSuccess = _WriteSingleKey(vkey, dwModifierState); + } + + return fSuccess; +} + +// Method Description: +// - Triggers the Clear action to indicate that the state machine should erase +// all internal state. +// Arguments: +// - +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionClear() +{ + return true; +} + +// Method Description: +// - Triggers the Ignore action to indicate that the state machine should eat +// this character and say nothing. +// Arguments: +// - +// Return Value: +// - true iff we successfully dispatched the sequence. +bool InputStateMachineEngine::ActionIgnore() +{ + return true; +} + +// Method Description: +// - Triggers the OscDispatch action to indicate that the listener should handle a control sequence. +// These sequences perform various API-type commands that can include many parameters. +// Arguments: +// - wch - Character to dispatch. This will be a BEL or ST char. +// - sOscParam - identifier of the OSC action to perform +// - pwchOscStringBuffer - OSC string we've collected. NOT null terminated. +// - cchOscString - length of pwchOscStringBuffer +// Return Value: +// - true if we handled the dsipatch. +bool InputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, + const unsigned short /*sOscParam*/, + _Inout_updates_(_Param_(4)) wchar_t* const /*pwchOscStringBuffer*/, + const unsigned short /*cchOscString*/) +{ + return false; +} + +// Method Description: +// - Writes a sequence of keypresses to the buffer based on the wch, +// vkey and modifiers passed in. Will create both the appropriate key downs +// and ups for that key for writing to the input. Will also generate +// keypresses for pressing the modifier keys while typing that character. +// If rgInput isn't big enough, then it will stop writing when it's filled. +// Arguments: +// - wch - the character to write to the input callback. +// - vkey - the VKEY of the key to write to the input callback. +// - dwModifierState - the modifier state to write with the key. +// - rgInput - the buffer of characters to write the keypresses to. Can write +// up to 8 records to this buffer. +// - cInput - the size of rgInput. This should be at least WRAPPED_SEQUENCE_MAX_LENGTH +// Return Value: +// - the number of records written, or 0 if the buffer wasn't big enough. +size_t InputStateMachineEngine::_GenerateWrappedSequence(const wchar_t wch, + const short vkey, + const DWORD dwModifierState, + _Inout_updates_(cInput) INPUT_RECORD* rgInput, + const size_t cInput) +{ + // TODO: Reuse the clipboard functions for generating input for characters + // that aren't on the current keyboard. + // MSFT:13994942 + if (cInput < WRAPPED_SEQUENCE_MAX_LENGTH) + { + return 0; + } + + const bool fShift = WI_IsFlagSet(dwModifierState, SHIFT_PRESSED); + const bool fCtrl = WI_IsFlagSet(dwModifierState, LEFT_CTRL_PRESSED); + const bool fAlt = WI_IsFlagSet(dwModifierState, LEFT_ALT_PRESSED); + + size_t index = 0; + INPUT_RECORD* next = &rgInput[0]; + + DWORD dwCurrentModifiers = 0; + + if (fShift) + { + WI_SetFlag(dwCurrentModifiers, SHIFT_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = TRUE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_SHIFT; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_SHIFT, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + if (fAlt) + { + WI_SetFlag(dwCurrentModifiers, LEFT_ALT_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = TRUE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_MENU; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_MENU, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + if (fCtrl) + { + WI_SetFlag(dwCurrentModifiers, LEFT_CTRL_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = TRUE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_CONTROL; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + + size_t added = _GetSingleKeypress(wch, vkey, dwCurrentModifiers, next, cInput - index); + + // if _GetSingleKeypress added more than two events we might overflow the buffer + if (added > 2) + { + return index; + } + + next += added; + index += added; + + if (fCtrl) + { + WI_ClearFlag(dwCurrentModifiers, LEFT_CTRL_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = FALSE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_CONTROL; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + if (fAlt) + { + WI_ClearFlag(dwCurrentModifiers, LEFT_ALT_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = FALSE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_MENU; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_MENU, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + if (fShift) + { + WI_ClearFlag(dwCurrentModifiers, SHIFT_PRESSED); + next->EventType = KEY_EVENT; + next->Event.KeyEvent.bKeyDown = FALSE; + next->Event.KeyEvent.dwControlKeyState = dwCurrentModifiers; + next->Event.KeyEvent.wRepeatCount = 1; + next->Event.KeyEvent.wVirtualKeyCode = VK_SHIFT; + next->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_SHIFT, MAPVK_VK_TO_VSC)); + next->Event.KeyEvent.uChar.UnicodeChar = 0x0; + next++; + index++; + } + + return index; + +} + +// Method Description: +// - Writes a single character keypress to the input buffer. This writes both +// the keydown and keyup events. +// Arguments: +// - wch - the character to write to the buffer. +// - vkey - the VKEY of the key to write to the buffer. +// - dwModifierState - the modifier state to write with the key. +// - rgInput - the buffer of characters to write the keypress to. Will always +// write to the first two positions in the buffer. +// - cRecords - the size of rgInput +// Return Value: +// - the number of input records written. +size_t InputStateMachineEngine::_GetSingleKeypress(const wchar_t wch, + const short vkey, + const DWORD dwModifierState, + _Inout_updates_(cRecords) INPUT_RECORD* const rgInput, + const size_t cRecords) +{ + FAIL_FAST_IF(!(cRecords >= 2)); + if (cRecords < 2) + { + return 0; + } + + rgInput[0].EventType = KEY_EVENT; + rgInput[0].Event.KeyEvent.bKeyDown = TRUE; + rgInput[0].Event.KeyEvent.dwControlKeyState = dwModifierState; + rgInput[0].Event.KeyEvent.wRepeatCount = 1; + rgInput[0].Event.KeyEvent.wVirtualKeyCode = vkey; + rgInput[0].Event.KeyEvent.wVirtualScanCode = (WORD)MapVirtualKey(vkey, MAPVK_VK_TO_VSC); + rgInput[0].Event.KeyEvent.uChar.UnicodeChar = wch; + + rgInput[1] = rgInput[0]; + rgInput[1].Event.KeyEvent.bKeyDown = FALSE; + + return 2; +} + +// Method Description: +// - Writes a sequence of keypresses to the input callback based on the wch, +// vkey and modifiers passed in. Will create both the appropriate key downs +// and ups for that key for writing to the input. Will also generate +// keypresses for pressing the modifier keys while typing that character. +// Arguments: +// - wch - the character to write to the input callback. +// - vkey - the VKEY of the key to write to the input callback. +// - dwModifierState - the modifier state to write with the key. +// Return Value: +// - true iff we successfully wrote the keypress to the input callback. +bool InputStateMachineEngine::_WriteSingleKey(const wchar_t wch, const short vkey, const DWORD dwModifierState) +{ + // At most 8 records - 2 for each of shift,ctrl,alt up and down, and 2 for the actual key up and down. + INPUT_RECORD rgInput[WRAPPED_SEQUENCE_MAX_LENGTH]; + size_t cInput = _GenerateWrappedSequence(wch, vkey, dwModifierState, rgInput, WRAPPED_SEQUENCE_MAX_LENGTH); + + std::deque> inputEvents = IInputEvent::Create(gsl::make_span(rgInput, cInput)); + + return _pDispatch->WriteInput(inputEvents); +} + +// Method Description: +// - Helper for writing a single key to the input when you only know the vkey. +// Will automatically get the wchar_t associated with that vkey. +// Arguments: +// - vkey - the VKEY of the key to write to the input callback. +// - dwModifierState - the modifier state to write with the key. +// Return Value: +// - true iff we successfully wrote the keypress to the input callback. +bool InputStateMachineEngine::_WriteSingleKey(const short vkey, const DWORD dwModifierState) +{ + wchar_t wch = (wchar_t)MapVirtualKey(vkey, MAPVK_VK_TO_CHAR); + return _WriteSingleKey(wch, vkey, dwModifierState); +} + +// Method Description: +// - Retrieves the modifier state from a set of parameters for a cursor keys +// sequence. This is for Arrow keys, Home, End, etc. +// Arguments: +// - rgusParams - the set of parameters to get the modifier state from. +// - cParams - the number of elements in rgusParams +// Return Value: +// - the INPUT_RECORD comaptible modifier state. +DWORD InputStateMachineEngine::_GetCursorKeysModifierState(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams) +{ + // Both Cursor keys and generic keys keep their modifiers in the same index. + return _GetGenericKeysModifierState(rgusParams, cParams); +} + +// Method Description: +// - Retrieves the modifier state from a set of parameters for a "Generic" +// keypress - one who's sequence is terminated with a '~'. +// Arguments: +// - rgusParams - the set of parameters to get the modifier state from. +// - cParams - the number of elements in rgusParams +// Return Value: +// - the INPUT_RECORD compatible modifier state. +DWORD InputStateMachineEngine::_GetGenericKeysModifierState(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams) +{ + DWORD dwModifiers = 0; + if (_IsModified(cParams) && cParams >=2) + { + dwModifiers = _GetModifier(rgusParams[1]); + } + return dwModifiers; +} + +// Method Description: +// - Determines if a set of parameters indicates a modified keypress +// Arguments: +// - cParams - the nummber of parameters we've collected in this sequence +// Return Value: +// - true iff the sequence is a modified sequence. +bool InputStateMachineEngine::_IsModified(const unsigned short cParams) +{ + // modified input either looks like + // \x1b[1;mA or \x1b[17;m~ + // Both have two parameters + return cParams == 2; +} + +// Method Description: +// - Converts a VT encoded modifier param into a INPUT_RECORD compatible one. +// Arguments: +// - modifierParam - the VT modifier value to convert +// Return Value: +// - The equivalent INPUT_RECORD modifier value. +DWORD InputStateMachineEngine::_GetModifier(const unsigned short modifierParam) +{ + // VT Modifiers are 1+(modifier flags) + unsigned short vtParam = modifierParam-1; + DWORD modifierState = modifierParam > 0 ? ENHANCED_KEY : 0; + + bool fShift = WI_IsFlagSet(vtParam, VT_SHIFT); + bool fAlt = WI_IsFlagSet(vtParam, VT_ALT); + bool fCtrl = WI_IsFlagSet(vtParam, VT_CTRL); + return modifierState | (fShift? SHIFT_PRESSED : 0) | (fAlt? LEFT_ALT_PRESSED : 0) | (fCtrl? LEFT_CTRL_PRESSED : 0); +} + +// Method Description: +// - Gets the Vkey form the generic keys table associated with a particular +// identifier code. The identifier code will be the first param in rgusParams. +// Arguments: +// - rgusParams: an array of shorts where the first is the identifier of the key +// we're looking for. +// - cParams: number of params in rgusParams +// - pVkey: Recieves the vkey +// Return Value: +// true iff we found the key +bool InputStateMachineEngine::_GetGenericVkey(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ short* const pVkey) const +{ + *pVkey = 0; + if (cParams < 1) + { + return false; + } + + const unsigned short identifier = rgusParams[0]; + for(int i = 0; i < ARRAYSIZE(s_rgGenericMap); i++) + { + GENERIC_TO_VKEY mapping = s_rgGenericMap[i]; + if (mapping.Identifier == identifier) + { + *pVkey = mapping.vkey; + return true; + } + } + return false; +} + +// Method Description: +// - Gets the Vkey from the CSI codes table associated with a particular character. +// Arguments: +// - wch: the wchar_t to get the mapped vkey of. +// - pVkey: Recieves the vkey +// Return Value: +// true iff we found the key +bool InputStateMachineEngine::_GetCursorKeysVkey(const wchar_t wch, _Out_ short* const pVkey) const +{ + *pVkey = 0; + for(int i = 0; i < ARRAYSIZE(s_rgCsiMap); i++) + { + CSI_TO_VKEY mapping = s_rgCsiMap[i]; + if (mapping.Action == wch) + { + *pVkey = mapping.vkey; + return true; + } + } + + return false; +} + +// Method Description: +// - Gets the Vkey from the SS3 codes table associated with a particular character. +// Arguments: +// - wch: the wchar_t to get the mapped vkey of. +// - pVkey: Recieves the vkey +// Return Value: +// true iff we found the key +bool InputStateMachineEngine::_GetSs3KeysVkey(const wchar_t wch, _Out_ short* const pVkey) const +{ + *pVkey = 0; + for(int i = 0; i < ARRAYSIZE(s_rgSs3Map); i++) + { + SS3_TO_VKEY mapping = s_rgSs3Map[i]; + if (mapping.Action == wch) + { + *pVkey = mapping.vkey; + return true; + } + } + + return false; +} + +// Method Description: +// - Gets the Vkey and modifier state that's associated with a particular char. +// Arguments: +// - wch: the wchar_t to get the vkey and modifier state of. +// - pVkey: Recieves the vkey +// - pdwModifierState: Recieves the modifier state +// Return Value: +// +bool InputStateMachineEngine::_GenerateKeyFromChar(const wchar_t wch, + _Out_ short* const pVkey, + _Out_ DWORD* const pdwModifierState) +{ + // Low order byte is key, high order is modifiers + short keyscan = VkKeyScanW(wch); + + short vkey = LOBYTE(keyscan); + + short keyscanModifiers = HIBYTE(keyscan); + + if (vkey == -1 && keyscanModifiers == -1) + { + return false; + } + + // Because of course, these are not the same flags. + short dwModifierState = 0 | + (WI_IsFlagSet(keyscanModifiers, KEYSCAN_SHIFT) ? SHIFT_PRESSED : 0) | + (WI_IsFlagSet(keyscanModifiers, KEYSCAN_CTRL) ? LEFT_CTRL_PRESSED : 0) | + (WI_IsFlagSet(keyscanModifiers, KEYSCAN_ALT) ? LEFT_ALT_PRESSED : 0); + + if (pVkey != nullptr) + { + *pVkey = vkey; + } + if (pdwModifierState != nullptr) + { + *pdwModifierState = dwModifierState; + } + return true; +} + +// Method Description: +// - Returns true if the engine should dispatch on the last charater of a string +// always, even if the sequence hasn't normally dispatched. +// If this is false, the engine will persist it's state across calls to +// ProcessString, and dispatch only at the end of the sequence. +// Return Value: +// - True iff we should manually dispatch on the last character of a string. +bool InputStateMachineEngine::FlushAtEndOfString() const +{ + return true; +} + +// Routine Description: +// - Returns true if the engine should dispatch control characters in the Escape +// state. Typically, control characters are immediately executed in the +// Escape state without returning to ground. If this returns true, the +// state machine will instead call ActionExecuteFromEscape and then enter +// the Ground state when a control character is encountered in the escape +// state. +// Return Value: +// - True iff we should return to the Ground state when the state machine +// encounters a Control (C0) character in the Escape state. +bool InputStateMachineEngine::DispatchControlCharsFromEscape() const +{ + return true; +} + +// Method Description: +// - Retrieves the type of window manipulation operation from the parameter pool +// stored during Param actions. +// This is kept seperate from the output version, as there may be +// codes that are supported in one direction but not the other. +// Arguments: +// - rgusParams - Array of parameters collected +// - cParams - Number of parameters we've collected +// - puiFunction - Memory location to receive the function type +// Return Value: +// - True iff we successfully pulled the function type from the parameters +bool InputStateMachineEngine::_GetWindowManipulationType(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiFunction) const +{ + bool fSuccess = false; + *puiFunction = DispatchTypes::WindowManipulationType::Invalid; + + if (cParams > 0) + { + switch(rgusParams[0]) + { + case DispatchTypes::WindowManipulationType::RefreshWindow: + *puiFunction = DispatchTypes::WindowManipulationType::RefreshWindow; + fSuccess = true; + break; + case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: + *puiFunction = DispatchTypes::WindowManipulationType::ResizeWindowInCharacters; + fSuccess = true; + break; + default: + fSuccess = false; + } + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves an X/Y coordinate pair for a cursor operation from the parameter pool stored during Param actions. +// Arguments: +// - puiLine - Memory location to receive the Y/Line/Row position +// - puiColumn - Memory location to receive the X/Column position +// Return Value: +// - True if we successfully pulled the cursor coordinates from the parameters we've stored. False otherwise. +_Success_(return) +bool InputStateMachineEngine::_GetXYPosition(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiLine, + _Out_ unsigned int* const puiColumn) const +{ + bool fSuccess = true; + *puiLine = s_uiDefaultLine; + *puiColumn = s_uiDefaultColumn; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + } + else if (cParams == 1) + { + // If there's only one param, leave the default for the column, and retrieve the specified row. + *puiLine = rgusParams[0]; + } + else if (cParams == 2) + { + // If there are exactly two parameters, use them. + *puiLine = rgusParams[0]; + *puiColumn = rgusParams[1]; + } + else + { + fSuccess = false; + } + + // Distances of 0 should be changed to 1. + if (*puiLine == 0) + { + *puiLine = s_uiDefaultLine; + } + + if (*puiColumn == 0) + { + *puiColumn = s_uiDefaultColumn; + + } + + return fSuccess; +} diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp new file mode 100644 index 000000000..0eb570da8 --- /dev/null +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -0,0 +1,192 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- InputStateMachineEngine.hpp + +Abstract: +- This is the implementation of the client VT input state machine engine. + This generates InpueEvents from a stream of VT sequences emmited by a + client "terminal" application. + +Author(s): +- Mike Griese (migrie) 18 Aug 2017 +--*/ +#pragma once + +#include "telemetry.hpp" +#include "IStateMachineEngine.hpp" +#include +#include "../../types/inc/IInputEvent.hpp" +#include "../adapter/IInteractDispatch.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class InputStateMachineEngine : public IStateMachineEngine + { + public: + InputStateMachineEngine(IInteractDispatch* const pDispatch); + InputStateMachineEngine(IInteractDispatch* const pDispatch, + const bool lookingForDSR); + + bool ActionExecute(const wchar_t wch) override; + bool ActionExecuteFromEscape(const wchar_t wch) override; + + bool ActionPrint(const wchar_t wch) override; + + bool ActionPrintString(const wchar_t* const rgwch, + const size_t cch) override; + + bool ActionPassThroughString(const wchar_t* const rgwch, + size_t const cch) override; + + bool ActionEscDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate) override; + + bool ActionCsiDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + + bool ActionClear() override; + + bool ActionIgnore() override; + + bool ActionOscDispatch(const wchar_t wch, + const unsigned short sOscParam, + _Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString) override; + + bool ActionSs3Dispatch(const wchar_t wch, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) override; + + bool FlushAtEndOfString() const override; + bool DispatchControlCharsFromEscape() const override; + + private: + + const std::unique_ptr _pDispatch; + bool _lookingForDSR; + + enum CsiActionCodes : wchar_t + { + ArrowUp = L'A', + ArrowDown = L'B', + ArrowRight = L'C', + ArrowLeft = L'D', + Home = L'H', + End = L'F', + Generic = L'~', // Used for a whole bunch of possible keys + CSI_F1 = L'P', + CSI_F2 = L'Q', + CSI_F3 = L'R', // Both F3 and DSR are on R. + // DSR_DeviceStatusReportResponse = L'R', + CSI_F4 = L'S', + DTTERM_WindowManipulation = L't', + CursorBackTab = L'Z', + }; + + enum Ss3ActionCodes : wchar_t + { + // The "Cursor Keys" are sometimes sent as a Ss3 in "application mode" + // But for now we'll only accept them as Normal Mode sequences, as CSI's. + // ArrowUp = L'A', + // ArrowDown = L'B', + // ArrowRight = L'C', + // ArrowLeft = L'D', + // Home = L'H', + // End = L'F', + SS3_F1 = L'P', + SS3_F2 = L'Q', + SS3_F3 = L'R', + SS3_F4 = L'S', + }; + + // Sequences ending in '~' use these numbers as identifiers. + enum GenericKeyIdentifiers : unsigned short + { + GenericHome = 1, + Insert = 2, + Delete = 3, + GenericEnd = 4, + Prior = 5, //PgUp + Next = 6, //PgDn + F5 = 15, + F6 = 17, + F7 = 18, + F8 = 19, + F9 = 20, + F10 = 21, + F11 = 23, + F12 = 24, + }; + + struct CSI_TO_VKEY { + CsiActionCodes Action; + short vkey; + }; + + struct GENERIC_TO_VKEY { + GenericKeyIdentifiers Identifier; + short vkey; + }; + + struct SS3_TO_VKEY { + Ss3ActionCodes Action; + short vkey; + }; + + static const CSI_TO_VKEY s_rgCsiMap[]; + static const GENERIC_TO_VKEY s_rgGenericMap[]; + static const SS3_TO_VKEY s_rgSs3Map[]; + + + DWORD _GetCursorKeysModifierState(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + DWORD _GetGenericKeysModifierState(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + bool _GenerateKeyFromChar(const wchar_t wch, _Out_ short* const pVkey, + _Out_ DWORD* const pdwModifierState); + + bool _IsModified(const unsigned short cParams); + DWORD _GetModifier(const unsigned short modifierParam); + + bool _GetGenericVkey(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ short* const pVkey) const; + bool _GetCursorKeysVkey(const wchar_t wch, _Out_ short* const pVkey) const; + bool _GetSs3KeysVkey(const wchar_t wch, _Out_ short* const pVkey) const; + + bool _WriteSingleKey(const short vkey, const DWORD dwModifierState); + bool _WriteSingleKey(const wchar_t wch, const short vkey, const DWORD dwModifierState); + + size_t _GenerateWrappedSequence(const wchar_t wch, + const short vkey, + const DWORD dwModifierState, + _Inout_updates_(cInput) INPUT_RECORD* rgInput, + const size_t cInput); + + size_t _GetSingleKeypress(const wchar_t wch, + const short vkey, + const DWORD dwModifierState, + _Inout_updates_(cRecords) INPUT_RECORD* const rgInput, + const size_t cRecords); + + bool _GetWindowManipulationType(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiFunction) const; + + static const unsigned int s_uiDefaultLine = 1; + static const unsigned int s_uiDefaultColumn = 1; + bool _GetXYPosition(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiLine, + _Out_ unsigned int* const puiColumn) const; + + bool _DoControlCharacter(const wchar_t wch, const bool writeAlt); + }; +} diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp new file mode 100644 index 000000000..ce4beac95 --- /dev/null +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -0,0 +1,1718 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "stateMachine.hpp" +#include "OutputStateMachineEngine.hpp" + +#include "ascii.hpp" +using namespace Microsoft::Console; +using namespace Microsoft::Console::VirtualTerminal; + +// takes ownership of pDispatch +OutputStateMachineEngine::OutputStateMachineEngine(ITermDispatch* const pDispatch) : + _dispatch(pDispatch), + _pfnFlushToTerminal(nullptr), + _pTtyConnection(nullptr), + _lastPrintedChar(AsciiChars::NUL) +{ +} + +OutputStateMachineEngine::~OutputStateMachineEngine() +{ + +} + +const ITermDispatch& OutputStateMachineEngine::Dispatch() const noexcept +{ + return *_dispatch; +} + +ITermDispatch& OutputStateMachineEngine::Dispatch() noexcept +{ + return *_dispatch; +} + +// Routine Description: +// - Triggers the Execute action to indicate that the listener should +// immediately respond to a C0 control character. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionExecute(const wchar_t wch) +{ + _dispatch->Execute(wch); + _ClearLastChar(); + return true; +} + +// Routine Description: +// - Triggers the Execute action to indicate that the listener should +// immediately respond to a C0 control character. +// This is called from the Escape state in the state machine, indicating the +// immediately previous character was an 0x1b. The output state machine +// does not treat this any differently than a normal ActionExecute. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionExecuteFromEscape(const wchar_t wch) +{ + return ActionExecute(wch); +} + +// Routine Description: +// - Triggers the Print action to indicate that the listener should render the +// character given. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionPrint(const wchar_t wch) +{ + // Stash the last character of the string, if it's a graphical character + if (wch >= AsciiChars::SPC) + { + _lastPrintedChar = wch; + } + + _dispatch->Print(wch); // call print + + return true; +} + +// Routine Description: +// - Triggers the Print action to indicate that the listener should render the +// string of characters given. +// Arguments: +// - rgwch - string to dispatch. +// - cch - length of rgwch +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionPrintString(const wchar_t* const rgwch, const size_t cch) +{ + if (cch == 0) + { + return true; + } + // Stash the last character of the string, if it's a graphical character + const wchar_t wch = rgwch[cch - 1]; + if (wch >= AsciiChars::SPC) + { + _lastPrintedChar = wch; + } + + _dispatch->PrintString(rgwch, cch); // call print + + return true; +} + +// Routine Description: +// This is called when we have determined that we don't understand a particular +// sequence, or the adapter has determined that the string is intended for +// the actual terminal (when we're acting as a pty). +// - Pass the string through to the target terminal application. If we're a pty, +// then we'll have a TerminalConnection that we'll write the string to. +// Otherwise, we're the terminal device, and we'll eat the string (because +// we don't know what to do with it) +// Arguments: +// - rgwch - string to dispatch. +// - cch - length of rgwch +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionPassThroughString(const wchar_t* const rgwch, + _In_ size_t const cch) +{ + bool fSuccess = true; + if (_pTtyConnection != nullptr) + { + std::wstring wstr = std::wstring(rgwch, cch); + auto hr = _pTtyConnection->WriteTerminalW(wstr); + LOG_IF_FAILED(hr); + fSuccess = SUCCEEDED(hr); + } + // If there's not a TTY connection, our previous behavior was to eat the string. + + return fSuccess; +} + +// Routine Description: +// - Triggers the EscDispatch action to indicate that the listener should handle +// a simple escape sequence. These sequences traditionally start with ESC +// and a simple letter. No complicated parameters. +// Arguments: +// - wch - Character to dispatch. +// - cIntermediate - Number of "Intermediate" characters found - such as '!', '?' +// - wchIntermediate - Intermediate character in the sequence, if there was one. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionEscDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate) +{ + bool fSuccess = false; + + // no intermediates. + if (cIntermediate == 0) + { + switch (wch) + { + case VTActionCodes::CUU_CursorUp: + fSuccess = _dispatch->CursorUp(1); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUU); + break; + case VTActionCodes::CUD_CursorDown: + fSuccess = _dispatch->CursorDown(1); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUD); + break; + case VTActionCodes::CUF_CursorForward: + fSuccess = _dispatch->CursorForward(1); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUF); + break; + case VTActionCodes::CUB_CursorBackward: + fSuccess = _dispatch->CursorBackward(1); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUB); + break; + case VTActionCodes::DECSC_CursorSave: + fSuccess = _dispatch->CursorSavePosition(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSC); + break; + case VTActionCodes::DECRC_CursorRestore: + fSuccess = _dispatch->CursorRestorePosition(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRC); + break; + case VTActionCodes::DECKPAM_KeypadApplicationMode: + fSuccess = _dispatch->SetKeypadMode(true); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPAM); + break; + case VTActionCodes::DECKPNM_KeypadNumericMode: + fSuccess = _dispatch->SetKeypadMode(false); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECKPNM); + break; + case VTActionCodes::RI_ReverseLineFeed: + fSuccess = _dispatch->ReverseLineFeed(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::RI); + break; + case VTActionCodes::HTS_HorizontalTabSet: + fSuccess = _dispatch->HorizontalTabSet(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::HTS); + break; + case VTActionCodes::RIS_ResetToInitialState: + fSuccess = _dispatch->HardReset(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::RIS); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + else if (cIntermediate == 1) + { + DesignateCharsetTypes designateType = s_DefaultDesignateCharsetType; + fSuccess = _GetDesignateType(wchIntermediate, &designateType); + if (fSuccess) + { + switch (designateType) + { + case DesignateCharsetTypes::G0: + fSuccess = _dispatch->DesignateCharset(wch); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG0); + break; + case DesignateCharsetTypes::G1: + fSuccess = false; + TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG1); + break; + case DesignateCharsetTypes::G2: + fSuccess = false; + TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG2); + break; + case DesignateCharsetTypes::G3: + fSuccess = false; + TermTelemetry::Instance().Log(TermTelemetry::Codes::DesignateG3); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + } + + _ClearLastChar(); + + return fSuccess; +} + +// Routine Description: +// - Triggers the CsiDispatch action to indicate that the listener should handle +// a control sequence. These sequences perform various API-type commands +// that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// - cIntermediate - Number of "Intermediate" characters found - such as '!', '?' +// - wchIntermediate - Intermediate character in the sequence, if there was one. +// - rgusParams - set of numeric parameters collected while pasring the sequence. +// - cParams - number of parameters found. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionCsiDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) +{ + bool fSuccess = false; + unsigned int uiDistance = 0; + unsigned int uiLine = 0; + unsigned int uiColumn = 0; + SHORT sTopMargin = 0; + SHORT sBottomMargin = 0; + SHORT sNumTabs = 0; + SHORT sClearType = 0; + unsigned int uiFunction = 0; + DispatchTypes::EraseType eraseType = DispatchTypes::EraseType::ToEnd; + DispatchTypes::GraphicsOptions rgGraphicsOptions[StateMachine::s_cParamsMax]; + size_t cOptions = StateMachine::s_cParamsMax; + DispatchTypes::AnsiStatusType deviceStatusType = (DispatchTypes::AnsiStatusType)-1; // there is no default status type. + unsigned int repeatCount = 0; + // This is all the args after the first arg, and the count of args not including the first one. + const unsigned short* const rgusRemainingArgs = (cParams > 1) ? rgusParams + 1 : rgusParams; + const unsigned short cRemainingArgs = (cParams >= 1) ? cParams - 1 : 0; + + if (cIntermediate == 0) + { + // fill params + switch (wch) + { + case VTActionCodes::CUU_CursorUp: + case VTActionCodes::CUD_CursorDown: + case VTActionCodes::CUF_CursorForward: + case VTActionCodes::CUB_CursorBackward: + case VTActionCodes::CNL_CursorNextLine: + case VTActionCodes::CPL_CursorPrevLine: + case VTActionCodes::CHA_CursorHorizontalAbsolute: + case VTActionCodes::VPA_VerticalLinePositionAbsolute: + case VTActionCodes::ICH_InsertCharacter: + case VTActionCodes::DCH_DeleteCharacter: + case VTActionCodes::ECH_EraseCharacters: + fSuccess = _GetCursorDistance(rgusParams, cParams, &uiDistance); + break; + case VTActionCodes::HVP_HorizontalVerticalPosition: + case VTActionCodes::CUP_CursorPosition: + fSuccess = _GetXYPosition(rgusParams, cParams, &uiLine, &uiColumn); + break; + case VTActionCodes::DECSTBM_SetScrollingRegion: + fSuccess = _GetTopBottomMargins(rgusParams, cParams, &sTopMargin, &sBottomMargin); + break; + case VTActionCodes::ED_EraseDisplay: + case VTActionCodes::EL_EraseLine: + fSuccess = _GetEraseOperation(rgusParams, cParams, &eraseType); + break; + case VTActionCodes::SGR_SetGraphicsRendition: + fSuccess = _GetGraphicsOptions(rgusParams, cParams, rgGraphicsOptions, &cOptions); + break; + case VTActionCodes::DSR_DeviceStatusReport: + fSuccess = _GetDeviceStatusOperation(rgusParams, cParams, &deviceStatusType); + break; + case VTActionCodes::DA_DeviceAttributes: + fSuccess = _VerifyDeviceAttributesParams(rgusParams, cParams); + break; + case VTActionCodes::SU_ScrollUp: + case VTActionCodes::SD_ScrollDown: + fSuccess = _GetScrollDistance(rgusParams, cParams, &uiDistance); + break; + case VTActionCodes::ANSISYSSC_CursorSave: + case VTActionCodes::ANSISYSRC_CursorRestore: + fSuccess = _VerifyHasNoParameters(cParams); + break; + case VTActionCodes::IL_InsertLine: + case VTActionCodes::DL_DeleteLine: + fSuccess = _GetScrollDistance(rgusParams, cParams, &uiDistance); + break; + case VTActionCodes::CHT_CursorForwardTab: + case VTActionCodes::CBT_CursorBackTab: + fSuccess = _GetTabDistance(rgusParams, cParams, &sNumTabs); + break; + case VTActionCodes::TBC_TabClear: + fSuccess = _GetTabClearType(rgusParams, cParams, &sClearType); + break; + case VTActionCodes::DTTERM_WindowManipulation: + fSuccess = _GetWindowManipulationType(rgusParams, cParams, &uiFunction); + break; + case VTActionCodes::REP_RepeatCharacter: + fSuccess = _GetRepeatCount(rgusParams, cParams, &repeatCount); + break; + default: + // If no params to fill, param filling was successful. + fSuccess = true; + break; + } + + // if param filling successful, try to dispatch + if (fSuccess) + { + switch (wch) + { + case VTActionCodes::CUU_CursorUp: + fSuccess = _dispatch->CursorUp(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUU); + break; + case VTActionCodes::CUD_CursorDown: + fSuccess = _dispatch->CursorDown(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUD); + break; + case VTActionCodes::CUF_CursorForward: + fSuccess = _dispatch->CursorForward(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUF); + break; + case VTActionCodes::CUB_CursorBackward: + fSuccess = _dispatch->CursorBackward(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUB); + break; + case VTActionCodes::CNL_CursorNextLine: + fSuccess = _dispatch->CursorNextLine(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CNL); + break; + case VTActionCodes::CPL_CursorPrevLine: + fSuccess = _dispatch->CursorPrevLine(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CPL); + break; + case VTActionCodes::CHA_CursorHorizontalAbsolute: + fSuccess = _dispatch->CursorHorizontalPositionAbsolute(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CHA); + break; + case VTActionCodes::VPA_VerticalLinePositionAbsolute: + fSuccess = _dispatch->VerticalLinePositionAbsolute(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::VPA); + break; + case VTActionCodes::CUP_CursorPosition: + case VTActionCodes::HVP_HorizontalVerticalPosition: + fSuccess = _dispatch->CursorPosition(uiLine, uiColumn); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CUP); + break; + case VTActionCodes::DECSTBM_SetScrollingRegion: + fSuccess = _dispatch->SetTopBottomScrollingMargins(sTopMargin, sBottomMargin); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTBM); + break; + case VTActionCodes::ICH_InsertCharacter: + fSuccess = _dispatch->InsertCharacter(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::ICH); + break; + case VTActionCodes::DCH_DeleteCharacter: + fSuccess = _dispatch->DeleteCharacter(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DCH); + break; + case VTActionCodes::ED_EraseDisplay: + fSuccess = _dispatch->EraseInDisplay(eraseType); + TermTelemetry::Instance().Log(TermTelemetry::Codes::ED); + break; + case VTActionCodes::EL_EraseLine: + fSuccess = _dispatch->EraseInLine(eraseType); + TermTelemetry::Instance().Log(TermTelemetry::Codes::EL); + break; + case VTActionCodes::SGR_SetGraphicsRendition: + fSuccess = _dispatch->SetGraphicsRendition(rgGraphicsOptions, cOptions); + TermTelemetry::Instance().Log(TermTelemetry::Codes::SGR); + break; + case VTActionCodes::DSR_DeviceStatusReport: + fSuccess = _dispatch->DeviceStatusReport(deviceStatusType); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DSR); + break; + case VTActionCodes::DA_DeviceAttributes: + fSuccess = _dispatch->DeviceAttributes(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DA); + break; + case VTActionCodes::SU_ScrollUp: + fSuccess = _dispatch->ScrollUp(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::SU); + break; + case VTActionCodes::SD_ScrollDown: + fSuccess = _dispatch->ScrollDown(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::SD); + break; + case VTActionCodes::ANSISYSSC_CursorSave: + fSuccess = _dispatch->CursorSavePosition(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSSC); + break; + case VTActionCodes::ANSISYSRC_CursorRestore: + fSuccess = _dispatch->CursorRestorePosition(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::ANSISYSRC); + break; + case VTActionCodes::IL_InsertLine: + fSuccess = _dispatch->InsertLine(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::IL); + break; + case VTActionCodes::DL_DeleteLine: + fSuccess = _dispatch->DeleteLine(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DL); + break; + case VTActionCodes::CHT_CursorForwardTab: + fSuccess = _dispatch->ForwardTab(sNumTabs); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CHT); + break; + case VTActionCodes::CBT_CursorBackTab: + fSuccess = _dispatch->BackwardsTab(sNumTabs); + TermTelemetry::Instance().Log(TermTelemetry::Codes::CBT); + break; + case VTActionCodes::TBC_TabClear: + fSuccess = _dispatch->TabClear(sClearType); + TermTelemetry::Instance().Log(TermTelemetry::Codes::TBC); + break; + case VTActionCodes::ECH_EraseCharacters: + fSuccess = _dispatch->EraseCharacters(uiDistance); + TermTelemetry::Instance().Log(TermTelemetry::Codes::ECH); + break; + case VTActionCodes::DTTERM_WindowManipulation: + fSuccess = _dispatch->WindowManipulation(static_cast(uiFunction), + rgusRemainingArgs, + cRemainingArgs); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DTTERM_WM); + break; + case VTActionCodes::REP_RepeatCharacter: + // Handled w/o the dispatch. This function is unique in that way + // If this were in the ITerminalDispatch, then each + // implementation would effectively be the same, calling only + // functions that are already part of the interface. + // Print the last graphical character a number of times. + if (_lastPrintedChar != AsciiChars::NUL) + { + std::wstring wstr(repeatCount, _lastPrintedChar); + _dispatch->PrintString(wstr.c_str(), wstr.length()); + } + fSuccess = true; + TermTelemetry::Instance().Log(TermTelemetry::Codes::REP); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + } + else if (cIntermediate == 1) + { + switch (wchIntermediate) + { + case L'?': + fSuccess = _IntermediateQuestionMarkDispatch(wch, rgusParams, cParams); + break; + case L'!': + fSuccess = _IntermediateExclamationDispatch(wch); + break; + case L' ': + fSuccess = _IntermediateSpaceDispatch(wch, rgusParams, cParams); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + // If we were unable to process the string, and there's a TTY attached to us, + // trigger the state machine to flush the string to the terminal. + if (_pfnFlushToTerminal != nullptr && !fSuccess) + { + fSuccess = _pfnFlushToTerminal(); + } + + _ClearLastChar(); + + return fSuccess; +} + + +// Routine Description: +// - Handles actions that have postfix params on an intermediate '?', such as DECTCEM, DECCOLM, ATT610 +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - True if handled successfully. False otherwise. +bool OutputStateMachineEngine::_IntermediateQuestionMarkDispatch(const wchar_t wchAction, _In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams) +{ + bool fSuccess = false; + + DispatchTypes::PrivateModeParams rgPrivateModeParams[StateMachine::s_cParamsMax]; + size_t cOptions = StateMachine::s_cParamsMax; + // Ensure that there was the right number of params + switch (wchAction) + { + case VTActionCodes::DECSET_PrivateModeSet: + case VTActionCodes::DECRST_PrivateModeReset: + fSuccess = _GetPrivateModeParams(rgusParams, cParams, rgPrivateModeParams, &cOptions); + break; + + default: + // If no params to fill, param filling was successful. + fSuccess = true; + break; + } + if (fSuccess) + { + switch(wchAction) + { + case VTActionCodes::DECSET_PrivateModeSet: + fSuccess = _dispatch->SetPrivateModes(rgPrivateModeParams, cOptions); + //TODO: MSFT:6367459 Add specific logging for each of the DECSET/DECRST codes + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSET); + break; + case VTActionCodes::DECRST_PrivateModeReset: + fSuccess = _dispatch->ResetPrivateModes(rgPrivateModeParams, cOptions); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECRST); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + return fSuccess; +} + + +// Routine Description: +// - Handles actions that have an intermediate '!', such as DECSTR +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - True if handled successfully. False otherwise. +bool OutputStateMachineEngine::_IntermediateExclamationDispatch(const wchar_t wchAction) +{ + bool fSuccess = false; + + switch(wchAction) + { + case VTActionCodes::DECSTR_SoftReset: + fSuccess = _dispatch->SoftReset(); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSTR); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + return fSuccess; +} + +// Routine Description: +// - Handles actions that have an intermediate ' ' (0x20), such as DECSCUSR +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - True if handled successfully. False otherwise. +bool OutputStateMachineEngine::_IntermediateSpaceDispatch(const wchar_t wchAction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) +{ + bool fSuccess = false; + DispatchTypes::CursorStyle cursorStyle = s_defaultCursorStyle; + + // Parse params + switch(wchAction) + { + case VTActionCodes::DECSCUSR_SetCursorStyle: + fSuccess = _GetCursorStyle(rgusParams, cParams, &cursorStyle); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + + // if param filling successful, try to dispatch + if (fSuccess) + { + switch(wchAction) + { + case VTActionCodes::DECSCUSR_SetCursorStyle: + fSuccess = _dispatch->SetCursorStyle(cursorStyle); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSCUSR); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + + return fSuccess; +} + +// Routine Description: +// - Triggers the Clear action to indicate that the state machine should erase +// all internal state. +// Arguments: +// - +// Return Value: +// - +bool OutputStateMachineEngine::ActionClear() +{ + // do nothing. + return true; +} + +// Routine Description: +// - Triggers the Ignore action to indicate that the state machine should eat +// this character and say nothing. +// Arguments: +// - +// Return Value: +// - +bool OutputStateMachineEngine::ActionIgnore() +{ + // do nothing. + return true; +} + +// Routine Description: +// - Triggers the OscDispatch action to indicate that the listener should handle a control sequence. +// These sequences perform various API-type commands that can include many parameters. +// Arguments: +// - wch - Character to dispatch. This will be a BEL or ST char. +// - sOscParam - identifier of the OSC action to perform +// - pwchOscStringBuffer - OSC string we've collected. NOT null terminated. +// - cchOscString - length of pwchOscStringBuffer +// Return Value: +// - true if we handled the dsipatch. +bool OutputStateMachineEngine::ActionOscDispatch(const wchar_t /*wch*/, + const unsigned short sOscParam, + _Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString) +{ + bool fSuccess = false; + wchar_t* pwchTitle = nullptr; + unsigned short sCchTitleLength = 0; + size_t tableIndex = 0; + DWORD dwColor = 0; + + switch (sOscParam) + { + case OscActionCodes::SetIconAndWindowTitle: + case OscActionCodes::SetWindowIcon: + case OscActionCodes::SetWindowTitle: + fSuccess = _GetOscTitle(pwchOscStringBuffer, cchOscString, &pwchTitle, &sCchTitleLength); + break; + case OscActionCodes::SetColor: + fSuccess = _GetOscSetColorTable(pwchOscStringBuffer, cchOscString, &tableIndex, &dwColor); + break; + case OscActionCodes::SetCursorColor: + fSuccess = _GetOscSetCursorColor(pwchOscStringBuffer, cchOscString, &dwColor); + break; + case OscActionCodes::ResetCursorColor: + // the console uses 0xffffffff as an "invalid color" value + dwColor = 0xffffffff; + fSuccess = true; + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + if (fSuccess) + { + switch (sOscParam) + { + case OscActionCodes::SetIconAndWindowTitle: + case OscActionCodes::SetWindowIcon: + case OscActionCodes::SetWindowTitle: + fSuccess = _dispatch->SetWindowTitle({ pwchTitle, sCchTitleLength }); + TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCWT); + break; + case OscActionCodes::SetColor: + fSuccess = _dispatch->SetColorTableEntry(tableIndex, dwColor); + TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCCT); + break; + case OscActionCodes::SetCursorColor: + fSuccess = _dispatch->SetCursorColor(dwColor); + TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCSCC); + break; + case OscActionCodes::ResetCursorColor: + fSuccess = _dispatch->SetCursorColor(dwColor); + TermTelemetry::Instance().Log(TermTelemetry::Codes::OSCRCC); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + } + + // If we were unable to process the string, and there's a TTY attached to us, + // trigger the state machine to flush the string to the terminal. + if (_pfnFlushToTerminal != nullptr && !fSuccess) + { + fSuccess = _pfnFlushToTerminal(); + } + + _ClearLastChar(); + + return fSuccess; +} + +// Routine Description: +// - Triggers the Ss3Dispatch action to indicate that the listener should handle +// a control sequence. These sequences perform various API-type commands +// that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// - rgusParams - set of numeric parameters collected while pasring the sequence. +// - cParams - number of parameters found. +// Return Value: +// - true iff we successfully dispatched the sequence. +bool OutputStateMachineEngine::ActionSs3Dispatch(const wchar_t /*wch*/, + _In_reads_(_Param_(3)) const unsigned short* const /*rgusParams*/, + const unsigned short /*cParams*/) +{ + // The output engine doesn't handle any SS3 sequences. + _ClearLastChar(); + return false; +} + +// Routine Description: +// - Retrieves the listed graphics options to be applied in order to the "font style" of the next characters inserted into the buffer. +// Arguments: +// - rgGraphicsOptions - Pointer to array space (expected 16 max, the max number of params this can generate) that will be filled with valid options from the GraphicsOptions enum +// - pcOptions - Pointer to the length of rgGraphicsOptions on the way in, and the count of the array used on the way out. +// Return Value: +// - True if we successfully retrieved an array of valid graphics options from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetGraphicsOptions(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_writes_(*pcOptions) DispatchTypes::GraphicsOptions* const rgGraphicsOptions, + _Inout_ size_t* const pcOptions) const +{ + bool fSuccess = false; + + if (cParams == 0) + { + if (*pcOptions >= 1) + { + rgGraphicsOptions[0] = s_defaultGraphicsOption; + *pcOptions = 1; + fSuccess = true; + } + else + { + fSuccess = false; // not enough space in buffer to hold response. + } + } + else + { + if (*pcOptions >= cParams) + { + for (size_t i = 0; i < cParams; i++) + { + // No memcpy. The parameters are shorts. The graphics options are unsigned ints. + rgGraphicsOptions[i] = (DispatchTypes::GraphicsOptions)rgusParams[i]; + } + + *pcOptions = cParams; + fSuccess = true; + } + else + { + fSuccess = false; // not enough space in buffer to hold response. + } + } + + // If we were unable to process the string, and there's a TTY attached to us, + // trigger the state machine to flush the string to the terminal. + if (_pfnFlushToTerminal != nullptr && !fSuccess) + { + fSuccess = _pfnFlushToTerminal(); + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves the erase type parameter for an upcoming operation. +// Arguments: +// - pEraseType - Memory location to receive the erase type parameter +// Return Value: +// - True if we successfully pulled an erase type from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetEraseOperation(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ DispatchTypes::EraseType* const pEraseType) const +{ + bool fSuccess = false; // If we have too many parameters or don't know what to do with the given value, return false. + *pEraseType = s_defaultEraseType; // if we fail, just put the default type in. + + if (cParams == 0) + { + // Empty parameter sequences should use the default + *pEraseType = s_defaultEraseType; + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, attempt to match it to the values we accept. + unsigned short const usParam = rgusParams[0]; + + switch (static_cast(usParam)) + { + case DispatchTypes::EraseType::ToEnd: + case DispatchTypes::EraseType::FromBeginning: + case DispatchTypes::EraseType::All: + case DispatchTypes::EraseType::Scrollback: + *pEraseType = (DispatchTypes::EraseType) usParam; + fSuccess = true; + break; + } + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves a distance for a cursor operation from the parameter pool stored during Param actions. +// Arguments: +// - puiDistance - Memory location to receive the distance +// Return Value: +// - True if we successfully pulled the cursor distance from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetCursorDistance(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ unsigned int* const puiDistance) const +{ + bool fSuccess = false; + *puiDistance = s_uiDefaultCursorDistance; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *puiDistance = rgusParams[0]; + fSuccess = true; + } + + // Distances of 0 should be changed to 1. + if (*puiDistance == 0) + { + *puiDistance = s_uiDefaultCursorDistance; + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves a distance for a scroll operation from the parameter pool stored during Param actions. +// Arguments: +// - puiDistance - Memory location to receive the distance +// Return Value: +// - True if we successfully pulled the scroll distance from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetScrollDistance(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ unsigned int* const puiDistance) const +{ + bool fSuccess = false; + *puiDistance = s_uiDefaultScrollDistance; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *puiDistance = rgusParams[0]; + fSuccess = true; + } + + // Distances of 0 should be changed to 1. + if (*puiDistance == 0) + { + *puiDistance = s_uiDefaultScrollDistance; + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves a width for the console window from the parameter pool stored during Param actions. +// Arguments: +// - puiConsoleWidth - Memory location to receive the width +// Return Value: +// - True if we successfully pulled the width from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetConsoleWidth(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ unsigned int* const puiConsoleWidth) const +{ + bool fSuccess = false; + *puiConsoleWidth = s_uiDefaultConsoleWidth; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *puiConsoleWidth = rgusParams[0]; + fSuccess = true; + } + + // Distances of 0 should be changed to 80. + if (*puiConsoleWidth == 0) + { + *puiConsoleWidth = s_uiDefaultConsoleWidth; + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves an X/Y coordinate pair for a cursor operation from the parameter pool stored during Param actions. +// Arguments: +// - puiLine - Memory location to receive the Y/Line/Row position +// - puiColumn - Memory location to receive the X/Column position +// Return Value: +// - True if we successfully pulled the cursor coordinates from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetXYPosition(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ unsigned int* const puiLine, _Out_ unsigned int* const puiColumn) const +{ + bool fSuccess = false; + *puiLine = s_uiDefaultLine; + *puiColumn = s_uiDefaultColumn; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's only one param, leave the default for the column, and retrieve the specified row. + *puiLine = rgusParams[0]; + fSuccess = true; + } + else if (cParams == 2) + { + // If there are exactly two parameters, use them. + *puiLine = rgusParams[0]; + *puiColumn = rgusParams[1]; + fSuccess = true; + } + + // Distances of 0 should be changed to 1. + if (*puiLine == 0) + { + *puiLine = s_uiDefaultLine; + } + + if (*puiColumn == 0) + { + *puiColumn = s_uiDefaultColumn; + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves a top and bottom pair for setting the margins from the parameter pool stored during Param actions +// Arguments: +// - psTopMargin - Memory location to receive the top margin +// - psBottomMargin - Memory location to receive the bottom margin +// Return Value: +// - True if we successfully pulled the margin settings from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetTopBottomMargins(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ SHORT* const psTopMargin, _Out_ SHORT* const psBottomMargin) const +{ + // Notes: (input -> state machine out) + // having only a top param is legal ([3;r -> 3,0) + // having only a bottom param is legal ([;3r -> 0,3) + // having neither uses the defaults ([;r [r -> 0,0) + // an illegal combo (eg, 3;2r) is ignored + + bool fSuccess = false; + *psTopMargin = s_sDefaultTopMargin; + *psBottomMargin = s_sDefaultBottomMargin; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + *psTopMargin = rgusParams[0]; + fSuccess = true; + } + else if (cParams == 2) + { + // If there are exactly two parameters, use them. + *psTopMargin = rgusParams[0]; + *psBottomMargin = rgusParams[1]; + fSuccess = true; + } + + if (*psBottomMargin > 0 && *psBottomMargin < *psTopMargin) + { + fSuccess = false; + } + return fSuccess; +} +// Routine Description: +// - Retrieves the status type parameter for an upcoming device query operation +// Arguments: +// - pStatusType - Memory location to receive the Status Type parameter +// Return Value: +// - True if we successfully found a device operation in the parameters stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetDeviceStatusOperation(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ DispatchTypes::AnsiStatusType* const pStatusType) const +{ + bool fSuccess = false; + *pStatusType = (DispatchTypes::AnsiStatusType)0; + + if (cParams == 1) + { + // If there's one parameter, attempt to match it to the values we accept. + unsigned short const usParam = rgusParams[0]; + + switch (usParam) + { + // This looks kinda silly, but I want the parser to reject (fSuccess = false) any status types we haven't put here. + case (unsigned short)DispatchTypes::AnsiStatusType::CPR_CursorPositionReport: + *pStatusType = DispatchTypes::AnsiStatusType::CPR_CursorPositionReport; + fSuccess = true; + break; + } + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves the listed private mode params be set/reset by DECSET/DECRST +// Arguments: +// - rPrivateModeParams - Pointer to array space (expected 16 max, the max number of params this can generate) that will be filled with valid params from the PrivateModeParams enum +// - pcParams - Pointer to the length of rPrivateModeParams on the way in, and the count of the array used on the way out. +// Return Value: +// - True if we successfully retrieved an array of private mode params from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetPrivateModeParams(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_writes_(*pcParams) DispatchTypes::PrivateModeParams* const rgPrivateModeParams, + _Inout_ size_t* const pcParams) const +{ + bool fSuccess = false; + // Can't just set nothing at all + if (cParams > 0) + { + if (*pcParams >= cParams) + { + for (size_t i = 0; i < cParams; i++) + { + // No memcpy. The parameters are shorts. The graphics options are unsigned ints. + rgPrivateModeParams[i] = (DispatchTypes::PrivateModeParams)rgusParams[i]; + } + *pcParams = cParams; + fSuccess = true; + } + else + { + fSuccess = false; // not enough space in buffer to hold response. + } + } + return fSuccess; +} + +// - Verifies that no parameters were parsed for the current CSI sequence +// Arguments: +// - +// Return Value: +// - True if there were no parameters. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_VerifyHasNoParameters(const unsigned short cParams) const +{ + return cParams == 0; +} + +// Routine Description: +// - Validates that we received the correct parameter sequence for the Device Attributes command. +// - For DA, we should have received either NO parameters or just one 0 parameter. Anything else is not acceptable. +// Arguments: +// - +// Return Value: +// - True if the DA params were valid. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_VerifyDeviceAttributesParams(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams) const +{ + bool fSuccess = false; + + if (cParams == 0) + { + fSuccess = true; + } + else if (cParams == 1) + { + if (rgusParams[0] == 0) + { + fSuccess = true; + } + } + + return fSuccess; +} + +// Routine Description: +// - Null terminates, then returns, the string that we've collected as part of the OSC string. +// Arguments: +// - ppwchTitle - a pointer to point to the Osc String to use as a title. +// - pcchTitleLength - a pointer place the length of ppwchTitle into. +// Return Value: +// - True if there was a title to output. (a title with length=0 is still valid) +_Success_(return) +bool OutputStateMachineEngine::_GetOscTitle(_Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString, + _Outptr_result_buffer_(*pcchTitle) wchar_t** const ppwchTitle, + _Out_ unsigned short * pcchTitle) const +{ + *ppwchTitle = pwchOscStringBuffer; + *pcchTitle = cchOscString; + + return pwchOscStringBuffer != nullptr; +} + +// Routine Description: +// - Retrieves a distance for a tab operation from the parameter pool stored during Param actions. +// Arguments: +// - psDistance - Memory location to receive the distance +// Return Value: +// - True if we successfully pulled the tab distance from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetTabDistance(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ SHORT* const psDistance) const +{ + bool fSuccess = false; + *psDistance = s_sDefaultTabDistance; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *psDistance = rgusParams[0]; + fSuccess = true; + } + + // Distances of 0 should be changed to 1. + if (*psDistance == 0) + { + *psDistance = s_sDefaultTabDistance; + } + + return fSuccess; +} + +// Routine Description: +// - Retrieves the type of tab clearing operation from the parameter pool stored during Param actions. +// Arguments: +// - psClearType - Memory location to receive the clear type +// Return Value: +// - True if we successfully pulled the tab clear type from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetTabClearType(_In_reads_(cParams) const unsigned short* const rgusParams, const unsigned short cParams, _Out_ SHORT* const psClearType) const +{ + bool fSuccess = false; + *psClearType = s_sDefaultTabClearType; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *psClearType = rgusParams[0]; + fSuccess = true; + } + return fSuccess; +} + +// Routine Description: +// - Retrieves a designate charset type from the intermediate we've stored. False otherwise. +// Arguments: +// - pDesignateType - Memory location to receive the designate type. +// Return Value: +// - True if we successfully pulled the designate type from the intermediate we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetDesignateType(const wchar_t wchIntermediate, _Out_ DesignateCharsetTypes* const pDesignateType) const +{ + bool fSuccess = false; + *pDesignateType = s_DefaultDesignateCharsetType; + + switch(wchIntermediate) + { + case '(': + *pDesignateType = DesignateCharsetTypes::G0; + fSuccess = true; + break; + case ')': + case '-': + *pDesignateType = DesignateCharsetTypes::G1; + fSuccess = true; + break; + case '*': + case '.': + *pDesignateType = DesignateCharsetTypes::G2; + fSuccess = true; + break; + case '+': + case '/': + *pDesignateType = DesignateCharsetTypes::G3; + fSuccess = true; + break; + } + + return fSuccess; +} + +// Routine Description: +// - Returns true if the engine should dispatch on the last charater of a string +// always, even if the sequence hasn't normally dispatched. +// If this is false, the engine will persist it's state across calls to +// ProcessString, and dispatch only at the end of the sequence. +// Return Value: +// - True iff we should manually dispatch on the last character of a string. +bool OutputStateMachineEngine::FlushAtEndOfString() const +{ + return false; +} + +// Routine Description: +// - Returns true if the engine should dispatch control characters in the Escape +// state. Typically, control characters are immediately executed in the +// Escape state without returning to ground. If this returns true, the +// state machine will instead call ActionExecuteFromEscape and then enter +// the Ground state when a control character is encountered in the escape +// state. +// Return Value: +// - True iff we should return to the Ground state when the state machine +// encounters a Control (C0) character in the Escape state. +bool OutputStateMachineEngine::DispatchControlCharsFromEscape() const +{ + return false; +} + +// Routine Description: +// - Converts a hex character to it's equivalent integer value. +// Arguments: +// - wch - Character to convert. +// - puiValue - recieves the int value of the char +// Return Value: +// - true iff the character is a hex character. +bool OutputStateMachineEngine::s_HexToUint(const wchar_t wch, + _Out_ unsigned int * const puiValue) +{ + *puiValue = 0; + bool fSuccess = false; + if (wch >= L'0' && wch <= L'9') + { + *puiValue = wch - L'0'; + fSuccess = true; + } + else if (wch >= L'A' && wch <= L'F') + { + *puiValue = (wch - L'A') + 10; + fSuccess = true; + } + else if (wch >= L'a' && wch <= L'f') + { + *puiValue = (wch - L'a') + 10; + fSuccess = true; + } + return fSuccess; +} + +// Routine Description: +// - Determines if a character is a valid number character, 0-9. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool OutputStateMachineEngine::s_IsNumber(const wchar_t wch) +{ + return wch >= L'0' && wch <= L'9'; // 0x30 - 0x39 +} + +// Routine Description: +// - Determines if a character is a valid hex character, 0-9a-fA-F. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool OutputStateMachineEngine::s_IsHexNumber(const wchar_t wch) +{ + return (wch >= L'0' && wch <= L'9') || // 0x30 - 0x39 + (wch >= L'A' && wch <= L'F') || + (wch >= L'a' && wch <= L'f'); +} + +// Routine Description: +// - Given a color spec string, attempts to parse the color that's encoded. +// The only supported spec currently is the following: +// spec: a color in the following format: +// "rgb://" +// where is one or two hex digits, upper or lower case. +// Arguments: +// - pwchBuffer - The string containing the color spec string to parse. +// - cchBuffer - a the length of the pwchBuffer +// - pRgb - recieves the color that we parsed +// Return Value: +// - True if a color was successfully parsed +bool OutputStateMachineEngine::s_ParseColorSpec(_In_reads_(cchBuffer) const wchar_t* const pwchBuffer, + const size_t cchBuffer, + _Out_ DWORD* const pRgb) +{ + const wchar_t* pwchCurr = pwchBuffer; + const wchar_t* const pwchEnd = pwchBuffer + cchBuffer; + bool foundRGB = false; + bool foundValidColorSpec = false; + unsigned int rguiColorValues[3] = {0}; + bool fSuccess = false; + // We can have anywhere between [11,15] characters + // 9 "rgb:h/h/h" + // 12 "rgb:hh/hh/hh" + // Any fewer cannot be valid, and any more will be too many. + // Return early in this case. + // We'll still have to bounds check when parsing the hh/hh/hh values + if (cchBuffer < 9 || cchBuffer > 12) + { + return false; + } + + // Now we look for "rgb:" + // Other colorspaces are theoretically possible, but we don't support them. + + if ((pwchCurr[0] == L'r') && + (pwchCurr[1] == L'g') && + (pwchCurr[2] == L'b') && + (pwchCurr[3] == L':') ) + { + foundRGB = true; + } + pwchCurr += 4; + + if (foundRGB) + { + // Colorspecs are up to hh/hh/hh, for 1-2 h's + for (size_t component = 0; component < 3; component++) + { + bool foundColor = false; + unsigned int* const pValue = &(rguiColorValues[component]); + for (size_t i = 0; i < 3; i++) + { + + const wchar_t wch = *pwchCurr; + pwchCurr++; + + if (s_IsHexNumber(wch)) + { + *pValue *= 16; + unsigned int intVal = 0; + if (s_HexToUint(wch, &intVal)) + { + *pValue += intVal; + } + else + { + // Encountered something weird oh no + foundColor = false; + break; + } + // If we're on the blue component, we're not going to see a /. + // Break out once we hit the end. + if (component == 2 && pwchCurr == pwchEnd) + { + foundValidColorSpec = true; + break; + } + } + else if (wch == L'/') + { + // Break this component, and start the next one. + foundColor = true; + break; + } + else + { + // Encountered something weird oh no + foundColor = false; + break; + } + } + if (!foundColor || pwchCurr == pwchEnd) + { + // Indicates there was a some error parsing color + // or we're at the end of the string. + break; + } + } + } + // Only if we find a valid colorspec can we pass it out successfully. + if (foundValidColorSpec) + { + DWORD color = RGB(LOBYTE(rguiColorValues[0]), + LOBYTE(rguiColorValues[1]), + LOBYTE(rguiColorValues[2])); + + *pRgb = color; + fSuccess = true; + } + return fSuccess; +} + +// Routine Description: +// - OSC 4 ; c ; spec ST +// c: the index of the ansi color table +// spec: a color in the following format: +// "rgb://" +// where is two hex digits +// Arguments: +// - ppwchTitle - a pointer to point to the Osc String to use as a title. +// - pcchTitleLength - a pointer place the length of ppwchTitle into. +// Return Value: +// - True if there was a title to output. (a title with length=0 is still valid) +bool OutputStateMachineEngine::_GetOscSetColorTable(_In_reads_(cchOscString) const wchar_t* const pwchOscStringBuffer, + const size_t cchOscString, + _Out_ size_t* const pTableIndex, + _Out_ DWORD* const pRgb) const +{ + *pTableIndex = 0; + *pRgb = 0; + const wchar_t* pwchCurr = pwchOscStringBuffer; + const wchar_t* const pwchEnd = pwchOscStringBuffer + cchOscString; + size_t _TableIndex = 0; + + bool foundTableIndex = false; + bool fSuccess = false; + // We can have anywhere between [11,16] characters + // 11 "#;rgb:h/h/h" + // 16 "###;rgb:hh/hh/hh" + // Any fewer cannot be valid, and any more will be too many. + // Return early in this case. + // We'll still have to bounds check when parsing the hh/hh/hh values + if (cchOscString < 11 || cchOscString > 16) + { + return false; + } + + // First try to get the table index, a number between [0,256] + for (size_t i = 0; i < 4; i++) + { + const wchar_t wch = *pwchCurr; + if (s_IsNumber(wch)) + { + _TableIndex *= 10; + _TableIndex += wch - L'0'; + + pwchCurr++; + } + else if (wch == L';' && i > 0) + { + // We need to explicitly pass in a number, we can't default to 0 if + // there's no param + pwchCurr++; + foundTableIndex = true; + break; + } + else + { + // Found an unexpected character, fail. + break; + } + } + // Now we look for "rgb:" + // Other colorspaces are theoretically possible, but we don't support them. + if (foundTableIndex) + { + DWORD color = 0; + fSuccess = s_ParseColorSpec(pwchCurr, pwchEnd - pwchCurr, &color); + + if (fSuccess) + { + *pTableIndex = _TableIndex; + *pRgb = color; + } + } + + + return fSuccess; +} + +// Routine Description: +// - OSC 12 ; spec ST +// spec: a color in the following format: +// "rgb://" +// where is two hex digits +// Arguments: +// - ppwchTitle - a pointer to point to the Osc String to use as a title. +// - pcchTitleLength - a pointer place the length of ppwchTitle into. +// Return Value: +// - True if there was a title to output. (a title with length=0 is still valid) +bool OutputStateMachineEngine::_GetOscSetCursorColor(_In_reads_(cchOscString) const wchar_t* const pwchOscStringBuffer, + const size_t cchOscString, + _Out_ DWORD* const pRgb) const +{ + *pRgb = 0; + const wchar_t* pwchCurr = pwchOscStringBuffer; + const wchar_t* const pwchEnd = pwchOscStringBuffer + cchOscString; + + bool fSuccess = false; + + DWORD color = 0; + fSuccess = s_ParseColorSpec(pwchCurr, pwchEnd - pwchCurr, &color); + + if (fSuccess) + { + *pRgb = color; + } + + return fSuccess; +} + +// Method Description: +// - Retrieves the type of window manipulation operation from the parameter pool +// stored during Param actions. +// This is kept seperate from the input version, as there may be +// codes that are supported in one direction but not the other. +// Arguments: +// - rgusParams - Array of parameters collected +// - cParams - Number of parameters we've collected +// - puiFunction - Memory location to receive the function type +// Return Value: +// - True iff we successfully pulled the function type from the parameters +bool OutputStateMachineEngine::_GetWindowManipulationType(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiFunction) const +{ + bool fSuccess = false; + *puiFunction = s_DefaultWindowManipulationType; + + if (cParams > 0) + { + switch(rgusParams[0]) + { + case DispatchTypes::WindowManipulationType::RefreshWindow: + *puiFunction = DispatchTypes::WindowManipulationType::RefreshWindow; + fSuccess = true; + break; + case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: + *puiFunction = DispatchTypes::WindowManipulationType::ResizeWindowInCharacters; + fSuccess = true; + break; + default: + fSuccess = false; + break; + } + } + + return fSuccess; +} + + +// Routine Description: +// - Retrieves a distance for a scroll operation from the parameter pool stored during Param actions. +// Arguments: +// - puiDistance - Memory location to receive the distance +// Return Value: +// - True if we successfully pulled the scroll distance from the parameters we've stored. False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetCursorStyle(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ DispatchTypes::CursorStyle* const pCursorStyle) const +{ + bool fSuccess = false; + *pCursorStyle = s_defaultCursorStyle; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *pCursorStyle = (DispatchTypes::CursorStyle)rgusParams[0]; + fSuccess = true; + } + + return fSuccess; +} + +// Method Description: +// - Sets us up to have another terminal acting as the tty instead of conhost. +// We'll set a couple members, and if they aren't null, when we get a +// sequence we don't understand, we'll pass it along to the terminal +// instead of eating it ourselves. +// Arguments: +// - pTtyConnection: This is a TerminalOutputConnection that we can write the +// sequence we didn't understand to. +// - pfnFlushToTerminal: This is a callback to the underlying state machine to +// trigger it to call ActionPassThroughString with whatever sequence it's +// currently processing. +// Return Value: +// - +void OutputStateMachineEngine::SetTerminalConnection(ITerminalOutputConnection* const pTtyConnection, + std::function pfnFlushToTerminal) +{ + this->_pTtyConnection = pTtyConnection; + this->_pfnFlushToTerminal = pfnFlushToTerminal; +} + + +// Routine Description: +// - Retrieves a number of times to repeat the last graphical character +// Arguments: +// - puiRepeatCount - Memory location to receive the repeat count +// Return Value: +// - True if we successfully pulled the repeat count from the parameters. +// False otherwise. +_Success_(return) +bool OutputStateMachineEngine::_GetRepeatCount(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiRepeatCount) const noexcept +{ + bool fSuccess = false; + *puiRepeatCount = s_uiDefaultRepeatCount; + + if (cParams == 0) + { + // Empty parameter sequences should use the default + fSuccess = true; + } + else if (cParams == 1) + { + // If there's one parameter, use it. + *puiRepeatCount = rgusParams[0]; + fSuccess = true; + } + + // Distances of 0 should be changed to 1. + if (*puiRepeatCount == 0) + { + *puiRepeatCount = s_uiDefaultRepeatCount; + } + + return fSuccess; +} + +// Method Description: +// - Clears our last stored character. The last stored character is the last +// graphical character we printed, which is reset if any other action is +// dispatched. +// Arguments: +// - +// Return Value: +// - +void OutputStateMachineEngine::_ClearLastChar() noexcept +{ + _lastPrintedChar = AsciiChars::NUL; +} diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp new file mode 100644 index 000000000..bafedc958 --- /dev/null +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- OutputStateMachineEngine.hpp + +Abstract: +- This is the implementation of the client VT output state machine engine. +*/ +#pragma once + +#include "../adapter/termDispatch.hpp" +#include "telemetry.hpp" +#include "IStateMachineEngine.hpp" +#include "../../inc/ITerminalOutputConnection.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class OutputStateMachineEngine : public IStateMachineEngine + { + public: + OutputStateMachineEngine(ITermDispatch* const pDispatch); + ~OutputStateMachineEngine(); + + bool ActionExecute(const wchar_t wch) override; + bool ActionExecuteFromEscape(const wchar_t wch) override; + + bool ActionPrint(const wchar_t wch) override; + + bool ActionPrintString(const wchar_t* const rgwch, const size_t cch) override; + + bool ActionPassThroughString(const wchar_t* const rgwch, + size_t const cch) override; + + bool ActionEscDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate) override; + + bool ActionCsiDispatch(const wchar_t wch, + const unsigned short cIntermediate, + const wchar_t wchIntermediate, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + + bool ActionClear() override; + + bool ActionIgnore() override; + + bool ActionOscDispatch(const wchar_t wch, + const unsigned short sOscParam, + _Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString) override; + + bool ActionSs3Dispatch(const wchar_t wch, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) override; + + bool FlushAtEndOfString() const override; + bool DispatchControlCharsFromEscape() const override; + + void SetTerminalConnection(Microsoft::Console::ITerminalOutputConnection* const pTtyConnection, + std::function pfnFlushToTerminal); + + const ITermDispatch& Dispatch() const noexcept; + ITermDispatch& Dispatch() noexcept; + + private: + std::unique_ptr _dispatch; + Microsoft::Console::ITerminalOutputConnection* _pTtyConnection; + std::function _pfnFlushToTerminal; + wchar_t _lastPrintedChar; + + bool _IntermediateQuestionMarkDispatch(const wchar_t wchAction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + bool _IntermediateExclamationDispatch(const wchar_t wch); + bool _IntermediateSpaceDispatch(const wchar_t wchAction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams); + + enum VTActionCodes : wchar_t + { + CUU_CursorUp = L'A', + CUD_CursorDown = L'B', + CUF_CursorForward = L'C', + CUB_CursorBackward = L'D', + CNL_CursorNextLine = L'E', + CPL_CursorPrevLine = L'F', + CHA_CursorHorizontalAbsolute = L'G', + CUP_CursorPosition = L'H', + ED_EraseDisplay = L'J', + EL_EraseLine = L'K', + SU_ScrollUp = L'S', + SD_ScrollDown = L'T', + ICH_InsertCharacter = L'@', + DCH_DeleteCharacter = L'P', + SGR_SetGraphicsRendition = L'm', + DECSC_CursorSave = L'7', + DECRC_CursorRestore = L'8', + DECSET_PrivateModeSet = L'h', + DECRST_PrivateModeReset = L'l', + ANSISYSSC_CursorSave = L's', // NOTE: Overlaps with DECLRMM/DECSLRM. Fix when/if implemented. + ANSISYSRC_CursorRestore = L'u', // NOTE: Overlaps with DECSMBV. Fix when/if implemented. + DECKPAM_KeypadApplicationMode = L'=', + DECKPNM_KeypadNumericMode = L'>', + DSR_DeviceStatusReport = L'n', + DA_DeviceAttributes = L'c', + DECSCPP_SetColumnsPerPage = L'|', + IL_InsertLine = L'L', + DL_DeleteLine = L'M', // Yes, this is the same as RI, however, RI is not preceeded by a CSI, and DL is. + VPA_VerticalLinePositionAbsolute = L'd', + DECSTBM_SetScrollingRegion = L'r', + RI_ReverseLineFeed = L'M', + HTS_HorizontalTabSet = L'H', // Not a CSI, so doesn't overlap with CUP + CHT_CursorForwardTab = L'I', + CBT_CursorBackTab = L'Z', + TBC_TabClear = L'g', + ECH_EraseCharacters = L'X', + HVP_HorizontalVerticalPosition = L'f', + DECSTR_SoftReset = L'p', + RIS_ResetToInitialState = L'c', // DA is prefaced by CSI, RIS by ESC + // 'q' is overloaded - no postfix is DECLL, ' ' postfix is DECSCUSR, and '"' is DECSCA + DECSCUSR_SetCursorStyle = L'q', // I believe we'll only ever implement DECSCUSR + DTTERM_WindowManipulation = L't', + REP_RepeatCharacter = L'b' + }; + + enum OscActionCodes : unsigned int + { + SetIconAndWindowTitle = 0, + SetWindowIcon = 1, + SetWindowTitle = 2, + SetColor = 4, + SetCursorColor = 12, + ResetCursorColor = 112, + }; + + enum class DesignateCharsetTypes + { + G0, + G1, + G2, + G3 + }; + + static const DispatchTypes::GraphicsOptions s_defaultGraphicsOption = DispatchTypes::GraphicsOptions::Off; + _Success_(return) + bool _GetGraphicsOptions(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_writes_(*pcOptions) DispatchTypes::GraphicsOptions* const rgGraphicsOptions, + _Inout_ size_t* const pcOptions) const; + + static const DispatchTypes::EraseType s_defaultEraseType = DispatchTypes::EraseType::ToEnd; + _Success_(return) + bool _GetEraseOperation(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ DispatchTypes::EraseType* const pEraseType) const; + + static const unsigned int s_uiDefaultCursorDistance = 1; + _Success_(return) + bool _GetCursorDistance(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiDistance) const; + + static const unsigned int s_uiDefaultScrollDistance = 1; + _Success_(return) + bool _GetScrollDistance(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiDistance) const; + + static const unsigned int s_uiDefaultConsoleWidth = 80; + _Success_(return) + bool _GetConsoleWidth(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiConsoleWidth) const; + + static const unsigned int s_uiDefaultLine = 1; + static const unsigned int s_uiDefaultColumn = 1; + _Success_(return) + bool _GetXYPosition(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiLine, + _Out_ unsigned int* const puiColumn) const; + + _Success_(return) + bool _GetDeviceStatusOperation(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ DispatchTypes::AnsiStatusType* const pStatusType) const; + + _Success_(return) + bool _VerifyHasNoParameters(const unsigned short cParams) const; + + _Success_(return) + bool _VerifyDeviceAttributesParams(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams) const; + + _Success_(return) + bool _GetPrivateModeParams(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_writes_(*pcParams) DispatchTypes::PrivateModeParams* const rgPrivateModeParams, + _Inout_ size_t* const pcParams) const; + + static const SHORT s_sDefaultTopMargin = 0; + static const SHORT s_sDefaultBottomMargin = 0; + _Success_(return) + bool _GetTopBottomMargins(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ SHORT* const psTopMargin, + _Out_ SHORT* const psBottomMargin) const; + + _Success_(return) + bool _GetOscTitle(_Inout_updates_(cchOscString) wchar_t* const pwchOscStringBuffer, + const unsigned short cchOscString, + _Outptr_result_buffer_(*pcchTitle) wchar_t** const ppwchTitle, + _Out_ unsigned short * pcchTitle) const; + + static const SHORT s_sDefaultTabDistance = 1; + _Success_(return) + bool _GetTabDistance(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ SHORT* const psDistance) const; + + static const SHORT s_sDefaultTabClearType = 0; + _Success_(return) + bool _GetTabClearType(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ SHORT* const psClearType) const; + + static const DesignateCharsetTypes s_DefaultDesignateCharsetType = DesignateCharsetTypes::G0; + _Success_(return) + bool _GetDesignateType(const wchar_t wchIntermediate, + _Out_ DesignateCharsetTypes* const pDesignateType) const; + + static const DispatchTypes::WindowManipulationType s_DefaultWindowManipulationType = DispatchTypes::WindowManipulationType::Invalid; + _Success_(return) + bool _GetWindowManipulationType(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiFunction) const; + + static bool s_HexToUint(const wchar_t wch, + _Out_ unsigned int * const puiValue); + static bool s_IsNumber(const wchar_t wch); + static bool s_IsHexNumber(const wchar_t wch); + bool _GetOscSetColorTable(_In_reads_(cchOscString) const wchar_t* const pwchOscStringBuffer, + const size_t cchOscString, + _Out_ size_t* const pTableIndex, + _Out_ DWORD* const pRgb) const; + + static bool s_ParseColorSpec(_In_reads_(cchBuffer) const wchar_t* const pwchBuffer, + const size_t cchBuffer, + _Out_ DWORD* const pRgb); + + bool _GetOscSetCursorColor(_In_reads_(cchOscString) const wchar_t* const pwchOscStringBuffer, + const size_t cchOscString, + _Out_ DWORD* const pRgb) const; + + static const DispatchTypes::CursorStyle s_defaultCursorStyle = DispatchTypes::CursorStyle::BlinkingBlockDefault; + _Success_(return) + bool _GetCursorStyle(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ DispatchTypes::CursorStyle* const pCursorStyle) const; + + static const unsigned int s_uiDefaultRepeatCount = 1; + _Success_(return) + bool _GetRepeatCount(_In_reads_(cParams) const unsigned short* const rgusParams, + const unsigned short cParams, + _Out_ unsigned int* const puiRepeatCount) const noexcept; + + void _ClearLastChar() noexcept; + + }; +} diff --git a/src/terminal/parser/ascii.hpp b/src/terminal/parser/ascii.hpp new file mode 100644 index 000000000..6cb537cc3 --- /dev/null +++ b/src/terminal/parser/ascii.hpp @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +namespace Microsoft::Console::VirtualTerminal +{ + enum AsciiChars : wchar_t + { + NUL = 0x0, // Null + SOH = 0x1, // Start of Heading + STX = 0x2, // Start of Text + ETX = 0x3, // End of Text + EOT = 0x4, // End of Transmission + ENQ = 0x5, // Enquiry + ACK = 0x6, // Acknowledge + BEL = 0x7, // Bell + BS = 0x8, // Backspace + TAB = 0x9, // Horizontal Tab + LF = 0xA, // Line Feed (new line) + VT = 0xB, // Vertical Tab + FF = 0xC, // Form Feed (new page) + CR = 0xD, // Carriage Return + SO = 0xE, // Shift Out + SI = 0xF, // Shift In + DLE = 0x10, // Data Link Escape + DC1 = 0x11, // Device Control 1 + DC2 = 0x12, // Device Control 2 + DC3 = 0x13, // Device Control 3 + DC4 = 0x14, // Device Control 4 + NAK = 0x15, // Negative Acknowledge + SYN = 0x16, // Synchronous Idle + ETB = 0x17, // End of Transmission Block + CAN = 0x18, // Cancel + EM = 0x19, // End of Medium + SUB = 0x1A, // Substitute + ESC = 0x1B, // Escape + FS = 0x1C, // File Seperator + GS = 0x1D, // Group Seperator + RS = 0x1E, // Record Seperator + US = 0x1F, // Unit Seperator + SPC = 0x20, // Space, first printable character + DEL = 0x7F, // Delete + }; +} diff --git a/src/terminal/parser/delfuzzpayload.bat b/src/terminal/parser/delfuzzpayload.bat new file mode 100644 index 000000000..4c2860d80 --- /dev/null +++ b/src/terminal/parser/delfuzzpayload.bat @@ -0,0 +1 @@ +rmdir /S /Q %_NTTREE%\unittests\ft_fuzzer \ No newline at end of file diff --git a/src/terminal/parser/dirs b/src/terminal/parser/dirs new file mode 100644 index 000000000..adc7406fd --- /dev/null +++ b/src/terminal/parser/dirs @@ -0,0 +1,4 @@ +DIRS=lib \ + ft_fuzzer \ + ft_fuzzwrapper \ + ut_parser \ diff --git a/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.cpp b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.cpp new file mode 100644 index 000000000..4a6bc944f --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.cpp @@ -0,0 +1,556 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "stdafx.h" + +#define __GENERATE_DIRECTED_FUZZING +#include "fuzzing_logic.h" +#include "fuzzing_directed.h" + +using namespace fuzz; + +// VT100 spec defines the ESC sequence as the char 0x1b +const CHAR ESC[2] { 0x1b, 0x0 }; + +// VT100 spec defines the CSI sequence as ESC followed by [ +const CHAR CSI[3] { 0x1b, 0x5b, 0x0 }; + +// There is an alternative, single-character CSI in the C1 control set: +const CHAR C1CSI[2] { static_cast(static_cast(0x9b)), 0x0 }; + +// VT100 spec defines the OSC sequence as ESC followed by ] +const CHAR OSC[3] { 0x1b, 0x5d, 0x0 }; + +static CStringA GenerateSGRToken(); +static CStringA GenerateCUXToken(); +static CStringA GenerateCUXToken2(); +static CStringA GenerateCUXToken3(); +static CStringA GeneratePrivateModeParamToken(); +static CStringA GenerateDeviceAttributesToken(); +static CStringA GenerateDeviceStatusReportToken(); +static CStringA GenerateEraseToken(); +static CStringA GenerateScrollToken(); +static CStringA GenerateWhiteSpaceToken(); +static CStringA GenerateInvalidToken(); +static CStringA GenerateTextToken(); +static CStringA GenerateOscTitleToken(); +static CStringA GenerateHardResetToken(); +static CStringA GenerateSoftResetToken(); +static CStringA GenerateOscColorTableToken(); + +const fuzz::_fuzz_type_entry g_repeatMap[] = +{ + { 4, [](BYTE) { return CFuzzChance::GetRandom(2, 0xF); } }, + { 1, [](BYTE) { return CFuzzChance::GetRandom(2, 0xFF); } }, + { 20, [](BYTE) { return (BYTE)0; } } +}; + +const std::function g_tokenGenerators[] = +{ + GenerateSGRToken, + GenerateCUXToken, + GenerateCUXToken2, + GenerateCUXToken3, + GeneratePrivateModeParamToken, + GenerateDeviceAttributesToken, + GenerateDeviceStatusReportToken, + GenerateScrollToken, + GenerateEraseToken, + GenerateOscTitleToken, + GenerateHardResetToken, + GenerateSoftResetToken, + GenerateOscColorTableToken +}; + +CStringA GenerateTokenLowProbability() +{ + const _fuzz_type_entry tokenGeneratorMap[] = + { + { 3, [&](CStringA) { return CFuzzChance::SelectOne(g_tokenGenerators, ARRAYSIZE(g_tokenGenerators))(); } }, + { 1, [](CStringA) { return GenerateInvalidToken(); } }, + { 1, [](CStringA) { return GenerateTextToken(); } }, + { 5, [](CStringA) { return GenerateWhiteSpaceToken(); } } + }; + CFuzzType ft(FUZZ_MAP(tokenGeneratorMap), CStringA("")); + + return (CStringA)ft; +} + +CStringA GenerateToken() +{ + const _fuzz_type_entry tokenGeneratorMap[] = + { + { 50, [](CStringA) { return GenerateTextToken(); } }, + { 40, [&](CStringA) { return CFuzzChance::SelectOne(g_tokenGenerators, ARRAYSIZE(g_tokenGenerators))(); } }, + { 1, [](CStringA) { return GenerateInvalidToken(); } }, + { 3, [](CStringA) { return GenerateWhiteSpaceToken(); } } + }; + CFuzzType ft(FUZZ_MAP(tokenGeneratorMap), CStringA("")); + + return (CStringA)ft; +} + +CStringA GenerateWhiteSpaceToken() +{ + const _fuzz_type_entry ftMap[] = + { + { 5, [](DWORD) { return CFuzzChance::GetRandom(0, 0xF); } }, + { 5, [](DWORD) { return CFuzzChance::GetRandom(0, 0xFF); } } + }; + CFuzzType ft(FUZZ_MAP(ftMap), 0); + + CStringA s; + for (DWORD i = 0; i < (DWORD)ft; i++) + { + s.Append(" "); + } + + return s; +} + +CStringA GenerateTextToken() +{ + const LPSTR tokens[] = + { + "The cow jumped over the moon.", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + "\r\n", + "\t", + "%s", + "%n", + ";", + "-", + "?", + "1024", + "0", + "0xFF" + }; + + return CStringA(CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens))); +} + +CStringA GenerateInvalidToken() +{ + const LPSTR tokens[] { ":", "'", "\"", "\\" }; + return CStringA(CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens))); +} + +CStringA GenerateFuzzedToken( + __in_ecount(cmap) const _fuzz_type_entry *map, + __in DWORD cmap, + __in_ecount(ctokens) const LPSTR *tokens, + __in DWORD ctokens) +{ + CStringA csis[] = { CSI, C1CSI }; + CStringA s = CFuzzChance::SelectOne(csis); + + BYTE manipulations = (BYTE)CFuzzType(FUZZ_MAP(g_repeatMap), 1); + for (BYTE i = 0; i < manipulations; i++) + { + CFuzzType ft(map, cmap, CStringA("")); + s.AppendFormat("%s%s%s%s%s", + GenerateTokenLowProbability().GetString(), + ((CStringA)ft).GetBuffer(), + GenerateTokenLowProbability().GetString(), + (i + 1 == manipulations) ? "" : ";", + GenerateTokenLowProbability().GetString()); + } + + s.Append(CFuzzChance::SelectOne(tokens, ctokens)); + return s; +} + +CStringA GenerateFuzzedOscToken( + __in_ecount(cmap) const _fuzz_type_entry *map, + __in DWORD cmap, + __in_ecount(ctokens) const LPSTR *tokens, + __in DWORD ctokens) +{ + CStringA s(OSC); + BYTE manipulations = (BYTE)CFuzzType(FUZZ_MAP(g_repeatMap), 1); + for (BYTE i = 0; i < manipulations; i++) + { + CFuzzType ft(map, cmap, CStringA("")); + s.AppendFormat("%s%s%s%s%s", + GenerateTokenLowProbability().GetString(), + ((CStringA)ft).GetBuffer(), + GenerateTokenLowProbability().GetString(), + (i + 1 == manipulations) ? "" : ";", + GenerateTokenLowProbability().GetString()); + } + + s.Append(CFuzzChance::SelectOne(tokens, ctokens)); + return s; +} + +// For SGR attributes, multiple can be specified in a row separated by ; and processed accordingly. +// For instance, 37;1;44m will do foreground white (low intensity, so effectively a gray) then set high intensity blue background. +CStringA GenerateSGRToken() +{ + const BYTE psValid[] = + { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, 24, 27, + 30, 31, 32, 33, 34, 35, 36, 37, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 49, + }; + + const LPSTR tokens[] = { "m" }; + const _fuzz_type_entry map[] = + { + { 40, [&](CStringA) { CStringA s; s.AppendFormat("%02d", CFuzzChance::SelectOne(psValid, ARRAYSIZE(psValid))); return s; } }, + { 10, [](CStringA) { CStringA s; s.AppendFormat("%d", CFuzzChance::GetRandom()); return s; } }, + { 25, [](CStringA) { return CStringA("35;5"); } }, + { 25, [](CStringA) { return CStringA("48;5"); } }, + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Cursor positioning, this function handles moving the cursor relative to the current location +// For example, moving the cursor to the next line, previous line, up, down, etc. +CStringA GenerateCUXToken() +{ + const LPSTR tokens[] = { "A", "B", "C", "D", "E", "F", "G" }; + const _fuzz_type_entry map[] = + { + { 25, [](CStringA) { CStringA s; s.AppendFormat("%d", CFuzzChance::GetRandom()); return s; } }, + { 25, [](CStringA) { CStringA s; s.AppendFormat("%d", CFuzzChance::GetRandom()); return s; } } + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Cursor positioning, this function handles saving and restoring the cursor position. +// Differs from other cursor functions since these are ESC sequences and not CSI sequences. +CStringA GenerateCUXToken2() +{ + const LPSTR tokens[] = { "7", "8" }; + CStringA cux(ESC); + cux.AppendFormat("%s%s%s", + GenerateTokenLowProbability().GetString(), + CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens)), + GenerateTokenLowProbability().GetString()); + return cux; +} + +// Cursor positioning with two arguments +CStringA GenerateCUXToken3() +{ + const LPSTR tokens[]{ "H" }; + const _fuzz_type_entry map[] = + { + {60, [](CStringA) { CStringA s; s.AppendFormat("%d;%d", CFuzzChance::GetRandom(), CFuzzChance::GetRandom()); return s; } }, // 60% give us two numbers in the valid range + {10, [](CStringA) { return CStringA(";"); } }, // 10% give us just a ; + {10, [](CStringA) { CStringA s; s.AppendFormat("%d;", CFuzzChance::GetRandom()); return s; } }, // 10% give us a column and no line + {10, [](CStringA) { CStringA s; s.AppendFormat(";%d", CFuzzChance::GetRandom()); return s; } }, // 10% give us a line and no column + // 10% give us nothing + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Hard Reset (has no args) +CStringA GenerateHardResetToken() +{ + const LPSTR tokens[] = { "c" }; + CStringA cux(ESC); + + cux.AppendFormat("%s%s%s", + GenerateTokenLowProbability().GetString(), + CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens)), + GenerateTokenLowProbability().GetString()); + return cux; +} + +// Soft Reset (has no args) +CStringA GenerateSoftResetToken() +{ + const LPSTR tokens[] = { "p" }; + CStringA cux(CSI); + + cux.AppendFormat("%s%s%s", + GenerateTokenLowProbability().GetString(), + CFuzzChance::SelectOne(tokens, ARRAYSIZE(tokens)), + GenerateTokenLowProbability().GetString()); + return cux; +} + +// Private Mode parameters. These cover a wide range of behaviors - hiding the cursor, +// enabling mouse mode, changing to the alt buffer, blinking the cursor, etc. +CStringA GeneratePrivateModeParamToken() +{ + const LPSTR tokens[] = { "h", "l" }; + const _fuzz_type_entry map[] = + { + { 12, [](CStringA) { CStringA s; s.AppendFormat("?%02d", CFuzzChance::GetRandom()); return s; } }, + { 8, [](CStringA) { return CStringA("?1"); } }, + { 8, [](CStringA) { return CStringA("?3"); } }, + { 8, [](CStringA) { return CStringA("?12"); } }, + { 8, [](CStringA) { return CStringA("?25"); } }, + { 8, [](CStringA) { return CStringA("?1000"); } }, + { 8, [](CStringA) { return CStringA("?1002"); } }, + { 8, [](CStringA) { return CStringA("?1003"); } }, + { 8, [](CStringA) { return CStringA("?1005"); } }, + { 8, [](CStringA) { return CStringA("?1006"); } }, + { 8, [](CStringA) { return CStringA("?1007"); } }, + { 8, [](CStringA) { return CStringA("?1049"); } } + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Erase sequences, valid numerical values are 0-2. If no numeric value is specified, 0 is assumed. +CStringA GenerateEraseToken() +{ + const LPSTR tokens[] = { "J", "K" }; + const _fuzz_type_entry map[] = + { + { 9, [](CStringA) { return CStringA(""); } }, + { 25, [](CStringA) { return CStringA("0"); } }, + { 25, [](CStringA) { return CStringA("1"); } }, + { 25, [](CStringA) { return CStringA("2"); } }, + { 25, [](CStringA) { return CStringA("3"); } }, + { 1, [](CStringA) { CStringA s; s.AppendFormat("%02d", CFuzzChance::GetRandom()); return s; } } + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Device Attributes +CStringA GenerateDeviceAttributesToken() +{ + const LPSTR tokens[] = { "c" }; + const _fuzz_type_entry map[] = + { + { 70, [](CStringA) { return CStringA(""); } }, // 70% leave it blank (valid) + { 29, [](CStringA) { return CStringA("0"); } }, // 29% put in a 0 (valid) + { 1, [](CStringA) { CStringA s; s.AppendFormat("%02d", CFuzzChance::GetRandom()); return s; } } // 1% make a mess (anything else) + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Device Attributes +CStringA GenerateDeviceStatusReportToken() +{ + const LPSTR tokens[] = { "n" }; + const _fuzz_type_entry map[] = + { + { 50, [](CStringA) { return CStringA("6"); } }, // 50% of the time, give us the one we were looking for (6, cursor report) + { 49, [](CStringA) { CStringA s; s.AppendFormat("%02d", CFuzzChance::GetRandom()); return s; } } // 49% of the time, put in a random value + // 1% leave it blank + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Scroll sequences, valid numeric values include 0-16384. +CStringA GenerateScrollToken() +{ + const LPSTR tokens[] = { "S", "T" }; + const _fuzz_type_entry map[] = + { + { 5, [](CStringA) { CStringA s; s.AppendFormat("%08d", CFuzzChance::GetRandom()); return s; } }, + { 5, [](CStringA) { CStringA s; s.AppendFormat("%08d", CFuzzChance::GetRandom()); return s; } }, + { 50, [](CStringA) { CStringA s; s.AppendFormat("%d", CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 20, [](CStringA) { CStringA s; s.AppendFormat("%02d", CFuzzChance::GetRandom()); return s; } } + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Resize sequences, valid numeric values include 0-16384. +CStringA GenerateResizeToken() +{ + const LPSTR tokens[] = { "t" }; + // 5% - generate a random window manipulation with 1 params + // 5% - generate a random window manipulation with 2 params + // 5% - generate a random window manipulation with no params + // 45% - generate a resize with two params + // 10% - generate a resize with only the first param + // 10% - generate a resize with only the second param + const _fuzz_type_entry map[] = + { + { 5, [](CStringA) { CStringA s; s.AppendFormat("%d;%d;%d", CFuzzChance::GetRandom(0, 0x4000), CFuzzChance::GetRandom(0, 0x4000), CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 5, [](CStringA) { CStringA s; s.AppendFormat("%d;%d", CFuzzChance::GetRandom(0, 0x4000), CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 5, [](CStringA) { CStringA s; s.AppendFormat("%d", CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 45, [](CStringA) { CStringA s; s.AppendFormat("8;%d;%d", CFuzzChance::GetRandom(0, 0x4000), CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 10, [](CStringA) { CStringA s; s.AppendFormat("8;%d;", CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + { 10, [](CStringA) { CStringA s; s.AppendFormat("8;;%d", CFuzzChance::GetRandom(0, 0x4000)); return s; } }, + }; + + return GenerateFuzzedToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Osc Window Title String. An Osc followed by a param on [0, SHORT_MAX], followed by a ";", followed by a string, +// and BEL terminated. +CStringA GenerateOscTitleToken() +{ + const LPSTR tokens[] = { "\x7" }; + const _fuzz_type_entry map[] = + { + { + 100, + [](CStringA) { + CStringA s; + SHORT limit = CFuzzChance::GetRandom(0, 10); + // append up to 10 numbers for the param + for(SHORT i = 0; i < limit; i++) + { + s.AppendFormat("%d", CFuzzChance::GetRandom(0, 9)); + } + s.Append(";"); + // append some characters for the string + limit = CFuzzChance::GetRandom(); + for(SHORT i = 0; i < limit; i++) + { + s.AppendFormat("%c", CFuzzChance::GetRandom()); + } + return s; + } + } + }; + + return GenerateFuzzedOscToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +// Osc Window Title String. An Osc followed by a param on [0, SHORT_MAX], followed by a ";", followed by a string, +// and BEL terminated. +CStringA GenerateOscColorTableToken() +{ + const LPSTR tokens[] = { "\x7", "\x1b\\" }; + const _fuzz_type_entry map[] = + { + { + 100, + [](CStringA) { + CStringA s; + SHORT limit = CFuzzChance::GetRandom(0, 10); + // append up to 10 numbers for the param + for(SHORT i = 0; i < limit; i++) + { + s.AppendFormat("%d", CFuzzChance::GetRandom(0, 9)); + } + s.Append(";"); + + // Append some random numbers for the index + limit = CFuzzChance::GetRandom(0, 10); + // append up to 10 numbers for the param + for(SHORT i = 0; i < limit; i++) + { + s.AppendFormat("%d", CFuzzChance::GetRandom(0, 9)); + } + // Maybe add more text + if (CFuzzChance::GetRandom()) + { + // usually add a RGB + limit = CFuzzChance::GetRandom(0, 10); + switch(limit) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + s.AppendFormat("rgb:"); + break; + case 7: + s.AppendFormat("rgbi:"); + break; + case 8: + s.AppendFormat("cmyk:"); + break; + default: + // append some characters for the string + limit = CFuzzChance::GetRandom(); + for(SHORT i = 0; i < limit; i++) + { + s.AppendFormat("%c", CFuzzChance::GetRandom()); + } + } + + + SHORT numColors = CFuzzChance::GetRandom(0, 5); + + // append up to 10 numbers for the param + for(SHORT i = 0; i < numColors; i++) + { + // Append some random numbers for the value + limit = CFuzzChance::GetRandom(0, 10); + // append up to 10 numbers for the param + for(SHORT j = 0; j < limit; j++) + { + s.AppendFormat("%d", CFuzzChance::GetRandom(0, 9)); + } + // Sometimes don't add a '/' + if (CFuzzChance::GetRandom(0, 10) != 0) + { + s.AppendFormat("/"); + } + } + + } + return s; + } + } + }; + + return GenerateFuzzedOscToken(FUZZ_MAP(map), tokens, ARRAYSIZE(tokens)); +} + +int __cdecl wmain(int argc, WCHAR* argv[]) +{ + if (argc != 3) + { + wprintf(L"Usage: "); + return -1; + } + + HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); + if (SUCCEEDED(hr)) + { + LPWSTR pwszOutputDir = argv[2]; + DWORD dwFileCount = _wtoi(argv[1]); + for (DWORD i = 0; i < dwFileCount; i++) + { + GUID guid = { 0 }; + hr = CoCreateGuid(&guid); + if (SUCCEEDED(hr)) + { + WCHAR wszName[MAX_PATH] = { 0 }; + StringFromGUID2(guid, wszName, ARRAYSIZE(wszName)); + + CStringW sGuid(wszName); + CStringW outputFile(pwszOutputDir); + outputFile.AppendFormat(L"\\%s.bin", sGuid.TrimLeft(L'{').TrimRight(L'}').GetBuffer()); + + CStringA text; + for (int j = 0; j < CFuzzChance::GetRandom(); j++) + { + text.Append(GenerateToken().GetBuffer()); + } + + CComPtr spStream; + hr = SHCreateStreamOnFileW(outputFile.GetBuffer(), STGM_CREATE | STGM_READWRITE, &spStream); + if (SUCCEEDED(hr)) + { + ULONG cbWritten = 0; + hr = spStream->Write(reinterpret_cast(text.GetBuffer()), text.GetLength(), &cbWritten); + if (SUCCEEDED(hr)) + { + wprintf(L"Wrote file (%d bytes): %s\n", cbWritten, outputFile.GetBuffer()); + } + } + } + } + + CoUninitialize(); + } + + return 0; +} diff --git a/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj new file mode 100644 index 000000000..fc199d6f0 --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj @@ -0,0 +1,35 @@ + + + + + + Create + + + + + + + + + + {96927B31-D6E8-4ABD-B03E-A5088A30BEBE} + Win32Proj + VTCommandFuzzer + TerminalParser.Fuzzer + VTCommandFuzzer + + + + stdafx.h + _CONSOLE;%(PreprocessorDefinitions) + + + Console + + + + + + + \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj.filters b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj.filters new file mode 100644 index 000000000..13ed87ffe --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/VTCommandFuzzer.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzer/fuzzing_directed.h b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h new file mode 100644 index 000000000..7ef4f1171 --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/fuzzing_directed.h @@ -0,0 +1,1243 @@ +// +// fuzzing_directed.h +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// The directed fuzzing definitions header. Requires C++0x11 support +// due to usage of variadic templates. +// +#ifndef __FUZZING_DIRECTED_H__ +#define __FUZZING_DIRECTED_H__ + +#pragma once + +#include + +#pragma warning(push) +#pragma warning(disable:4242) // Standard random library contains C4242 warning about conversion from unsigned int to unsigned short. +#include +#pragma warning(pop) + +#include +#include +#include +#include +#include + +#ifndef __FUZZING_ALLOCATOR +#define __FUZZING_ALLOCATOR CComAllocator +#endif + +namespace variadic +{ + template + struct index {}; + + template + struct gen_seq : gen_seq {}; + + template + struct gen_seq<0, Is...> : index{}; +} + +namespace fuzz +{ + // Fuzz traits change inherent behavior of the CFuzzType class + // (and associated derived classes). This is a bit-flag such that + // multiple traits can be applied as needed. + enum _FuzzTraits : unsigned int + { + // Default behavior is to not throw exceptions + TRAIT_DEFAULT = 0x0, + + // In the event that the fuzz map percentages add up to more than + // 100% during constructor initialization, an exception of type + // CFuzzRangeException is thrown. + TRAIT_THROW_ON_INIT_FAILURE = 0x1, + + // For classes that could realloc a buffer in order to grow or + // shrink the size of the fuzzed result, this results in having + // two different buffers that need to be freed. To make the calling + // code work correctly, we can use the flag below to transfer the + // allocation ownership such that the calling code frees the correct + // buffer and our fuzz classes frees the other buffer. + // Example: + // DWORD cbSize = 0; + // __untrusted_array_size(BYTE, DWORD) cbSizeUntrusted = + // __untrusted_array_size_init(BYTE, DWORD)(cbSize); + // __untrusted_array(BYTE, DWORD) arr = + // __untrusted_array_init(FUZZ_MAP(...), nullptr, cbSizeUntrusted); + // AllocateArray(&arr, &cbSizeUntrusted); // arr overloads & operator to + // // allow allocation and then wraps + // // this allocation with CFuzzArray + // ...use arr, potentially resulting in a reallocated fuzzed buffer... + // CoTaskMemFree(arr); // If reallocation occurs, the other buffer + // // will be freed when arr goes out of scope. + TRAIT_TRANSFER_ALLOCATION = 0x2, + + // Given the design of the CFuzzArray class, it could be unclear + // if the corresponding size of the array is the number of elements + // or the overall byte count. The class defaults to assuming + // the number of elements, but the trait below can be used to + // inform the class that the size is actually the number of total + // bytes. Note that when using byte arrays, there is no difference + // in behavior. + TRAIT_SIZE_IS_BCOUNT = 0x4 + }; + typedef UINT FuzzTraits; + + // Percentages used for fuzzing are converted into a range of acceptable + // values between 0 and 100. This allows a random value to be generated + // between 0 and 100, if it falls within the range then the fuzzing + // manipulation will be applied accordingly. + // For example: Generate a random value between 0-100 (inclusive) + // _Range_ | _Manipulation_ + // 91-100 | Fuzz permutation A + // 86-90 | Fuzz permutation B + // 80-85 | Fuzz permutation C + // 0-79 | Default value + struct _range + { + int iHigh; + int iLow; + }; + + // Fuzz type entries provide a fuzzing function, specified as pfnFuzz, + // together with a percentage that this fuzzing function should be + // invoked. The percentage, uiPercentage, should be between 1-100. + // There are some fuzzing classes (i.e. CFuzzString) that allow + // for fuzzed values to be reallocated, in these cases a function + // must be specified via pfnDealloc that can free the resulting + // memory appropriately. If no memory is being reallocated, pfnDealloc + // can be nullptr. + // + // An array of fuzz type entries (or fuzz array entries) is referred + // to as a fuzz map in subsequent documentation. + // + // The design of pfnFuzz is to allow for mutational based fuzzing + // scenarios, where "template" data is passed to the fuzzing routine + // via the function parameter. This data will be whatever the + // fuzzing class' value is, either from initialization of the class + // or by setting the value directly. + // For example: + // __untrusted_lpwstr pwsz = + // __untrusted_lpwstr_init(FUZZ_MAP(...), L"foo"); + // LPWSTR pwszFuzzed = pwsz; // Asking for the LPWSTR member will + // // cause the fuzzing map to be + // // evaluated, if a fuzz type entry is + // // selected, L"foo" will be passed + // // as the parameter to pfnFuzz + template + struct _fuzz_type_entry + { + unsigned int uiPercentage; + std::function<_Type(_Type, _Args...)> pfnFuzz; + std::function pfnDealloc; + }; + + // The range structure is an internal structure that maps the percentage + // specified within the fuzz type entry struct into its associated + // probability range. It is not expected that users of this codebase + // will need to use this struct type directly. + template + struct _range_fuzz_type_entry + { + _fuzz_type_entry<_Type, _Args...> fte; + _range range; + }; + + // Similar to fuzz type entries, there are different challenges when + // fuzzing an array, since the size of the array needs to be known + // and if a reallocation occurs, the size needs to be updated to stay + // in sync. To support this, fuzz array entries have two template + // parameters, the first one is the type of objects in the array, + // and the second parameter is the size, either in bytes or in element + // count. When designing a fuzz map for an array, you will need to + // determine whether _Type2 is the byte count or the element count. + // + // With respect to pfnFuzz, once again this follows a mutational + // based approach, where the parameters will be whatever CFuzzArray + // is initialized to. Notice that the second parameter is an + // automatic reference to the size, which allows this value to be + // updated in the event that a new fuzzed buffer is allocation and + // returned from pfnFuzz. If reallocation occurs, pfnDealloc must + // be provided as a means of appropriately freeing the memory. + // For example: + // DWORD cbSize = 0; + // __untrusted_array_size(BYTE, DWORD) cbSizeUntrusted = + // __untrusted_array_size_init(cbSize); + // __untrusted_array(BYTE, DWORD) arr = + // __untrusted_array_init(FUZZ_MAP(...), nullptr, cbSizeUntrusted); + // AllocateArray(&arr, &cbSizeUntrusted); // Operator overloading on & will + // // allow the array to be allocated + // // and then wrapped by CFuzzArray + // BYTE *rgFuzzed = arr; // Asking for the BYTE* value will cause + // // the fuzz map to be evaluated. If the + // // buffer is reallocated, pfnFuzz will + // // have a reference to cbSize which can be + // // updated appropriately. + template + struct _fuzz_array_entry + { + unsigned int uiPercentage; + std::function<_Type1* (_Type1*, _Type2&, _Args...)> pfnFuzz; + std::function pfnDealloc; + }; + + // Internal struct for mapping percentages to value ranges. Not + // expected to be used by users of this codebase but for internal + // use instead. + template + struct _range_fuzz_array_entry + { + _fuzz_array_entry<_Type1, _Type2, _Args...> fae; + _range range; + }; + + // During initialization of the fuzz classes, if the fuzz map + // percentages add up to more than 100% and the fuzz trait + // TRAIT_THROW_ON_INIT_FAILURE is applied, the CFuzzRangeException + // will be thrown. Since constructors cannot return errors, this allows + // for verifying that numeric errors have not been made when designing + // the fuzz map that would cause fuzzing manipulations to not ever + // be applied. Not that this exception cannot ever be thrown from + // CFuzzFlags, as the fuzz map is evaluated slightly differently when + // fuzzing bit-wise flags. + class CFuzzRangeException + { + public: + CFuzzRangeException() { }; + virtual ~CFuzzRangeException() { }; + }; + + // In an effort to avoid fuzzing code from scattering rand() throughout + // the codebase, the CFuzzChance class is designed as the go to place + // for generating random values (or random selection of values). This + // class is also used by the various fuzz classes internally for + // determining which fuzzing routine should be applied from the fuzz map. + class CFuzzChance + { + public: +// Will collide with min() and max() macros, so we need to jump through hoops +// to correctly use std::numeric_limits<_Type>::min() and std::numeric_limits<_Type>::max() +#ifdef min +#define __min_collision__ +#undef min +#endif + +#ifdef max +#define __max_collision__ +#undef max +#endif + template + static _Type GetRandom() throw () + { + return GetRandom<_Type>(std::numeric_limits<_Type>::min(), std::numeric_limits<_Type>::max()); + } + + template + static _Type GetRandom(__in _Type tCap) throw() + { + return GetRandom<_Type>(std::numeric_limits<_Type>::min(), --tCap); + } + + template + static _Type GetRandom(__in _Type tMin, __in _Type tMax) + { + std::mt19937 engine(m_rd()); // Mersenne twister MT19937 + std::uniform_int_distribution<_Type> distribution(tMin, tMax); + auto generator = std::bind(distribution, engine); + return generator(); + } + + // uniform_int_distribution only works with _Is_IntType types, which do not + // currently include char or unsigned char, so here is a specialization + // specifically for BYTE (unsigned char). + template<> + static BYTE GetRandom(__in BYTE tMin, __in BYTE tMax) + { + std::mt19937 engine(m_rd()); // Mersenne twister MT19937 + // BYTE is unsiged, so we want to also use an unsigned type to avoid sign + // extension of tMin and tMax. + std::uniform_int_distribution distribution(tMin, tMax); + auto generator = std::bind(distribution, engine); + return static_cast(generator()); + } +#ifdef __min_collision__ +#undef __min_collision__ +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#endif + +#ifdef __max_collision__ +#undef __max_collision__ +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + + // Given an array of elements, select a random element from the + // collection. Note that cElems is the number of items in the array. + template + static _Type SelectOne(__in_ecount(cElems) const _Type *rg, __in size_t cElems) throw() + { + return rg[GetRandom(cElems)]; + } + + // Given an array of elements, select a random element from the + // collection. Note that _cElems is the number of items in the array. + template + static _Type SelectOne(const _Type (&rg)[_cElems]) throw() + { + return rg[GetRandom(_cElems)]; + } + private: + CFuzzChance() { } + virtual ~CFuzzChance() { } + static std::random_device m_rd; + }; + + std::random_device CFuzzChance::m_rd; + + // Provides a common base class between CFuzzArray and CFuzzType, + // collecting the set of members that are used by both classes. This + // class cannot be instantiated directly and must be inherited from. + class CFuzzBase + { + protected: + CFuzzBase() : + m_fFuzzed(FALSE), + m_iPercentageTotal(100) + { }; + virtual ~CFuzzBase() { }; + + // Converts a percentage into a valid range. Note that riTotal + // is a reference value, which allows for a running total to be + // decremented as fuzz map percentages are mapped to valid ranges. + void ConvertPercentageToRange(__in unsigned int iPercentage, + __inout int &riTotal, + __deref_out _range *pr) const + { + pr->iHigh = riTotal; + pr->iLow = riTotal - iPercentage; + riTotal -= iPercentage; + } + protected: + BOOL m_fFuzzed; + int m_iPercentageTotal; + FuzzTraits m_traits{ TRAIT_DEFAULT }; + }; + + // 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 + // reallocation is possible (or an appropriate fuzzing strategy) is + // dependent on the scenario and must be determined by the person + // designing the fuzz map. If reallocation is going to be used, the + // _Alloc class must specify the appropriate allocator that corresponds + // to the code that is being fuzzed. For example, if all allocations + // are made using new/delete, the default CComAllocator is not + // appropriate and should be changed to CCRTAllocator. Adding new + // allocator classes is as easy as writing a new class that supports + // Allocate/Free/Reallocate, see documentation for CComAllocator. + template + class CFuzzArray : public CFuzzBase + { + public: + template friend class CFuzzArraySize; + + // Creates a CFuzzArray instance that wraps a buffer specfied by + // rg, together with its size (note that this is the number of elements + // not necessarily the byte count). cElems is a reference so it must + // point to a valid variable. In this constructor, it is valid for + // the fuzz map to be a null pointer, but it is expected that a fuzz + // map will be provided at a later time via SetFuzzArrayMap or + // AddFuzzArrayEntry. + // + // If CFuzzArray is initialized with rg == nullptr, this usage case + // is designed to leverage the & operator overload to have CFuzzArray + // initialized to a valid buffer. In this scenario, if reallocation + // is being used by the fuzz map and reallocation occurs, this class + // implements logic to transfer the fuzzed allocation to the calling code + // and then free the array that was set via the & operator. If this is + // not desirable, the situation is avoided by either not reallocating + // or by not initializing via &. + CFuzzArray( + __in_ecount(cfae) const _fuzz_array_entry<_Type1, _Type2, _Args...> *rgfae, + __in ULONG cfae, + __in_ecount_opt(cElems) _Type1 *rg, + __inout _Type2& cElems, + __in _Args&&... args) : + m_rgCaller(rg), + m_pcElems(&cElems), + m_pfas(nullptr), + m_tArgs(std::forward<_Args>(args)...) + { + Init(rgfae, cfae); + } + + // Constructor that allows association with a companion CFuzzArraySize object. + // See CFuzzArraySize comments for more details. + CFuzzArray( + __in_ecount(cfae) const _fuzz_array_entry<_Type1, _Type2, _Args...> *rgfae, + __in ULONG cfae, + __in_opt _Type1 *rg, + __in CFuzzArraySize<_Type1, _Type2, _Args...> &size, + __in _Args&&... args) : + m_rgCaller(rg), + m_pcElems(size.m_pcElems), + m_pfas(nullptr), + m_tArgs(std::forward<_Args>(args)...) + { + if (SUCCEEDED(Init(rgfae, cfae))) + { + size.Pair(*this); + m_pfas = size.Reference(); + } + } + + virtual ~CFuzzArray() + { + FreeRealloc(); + } + + // Requesting the array pointer will result in the fuzz map being + // evaluated, potentially returning a fuzzed value. Note that fuzzing + // is only evaluated once, so repeated access will return the same + // fuzzed choice. + __inline operator _Type1* () throw() + { + return GetValueFromMap(); + } + + // Allow calls to get the size of the fuzzed array ensuring evaluation + // of the fuzzmap. Designed primarily to be called from the CFuzzArraySize + // friend class. + __inline operator _Type2 () throw() + { + GetValueFromMap(); + return *m_pcElems; + } + + // The overloaded & operator allows this class to replace array pointers + // within the calling code, but the associated size need to be initialized + // via the constructor. If this operator is used and the fuzzing map + // applies reallocation, ownership of the respective buffers is transferred + // such that the fuzzed buffer becomes the caller's responsibility and the + // initial buffer is freed when this class is destroyed. Users of this + // codebase are responsible for ensuring the correct allocator is specified + // for _Alloc and that the calling code is still functionally correct. + __inline _Type1** operator &() throw() + { + assert(m_rgCaller == nullptr); + m_ftEffectiveTraits |= TRAIT_TRANSFER_ALLOCATION; + return (m_rgRealloc) ? &m_rgRealloc : &m_rgCaller; + } + + // Const version of this operator overload does not assume ownership transfer + // like the above case. + __inline const _Type1** operator &() const throw() + { + assert(m_rgCaller == nullptr); + return (m_rgRealloc) ? &m_rgRealloc : &m_rgCaller; + } + + // Setting the fuzz map will clear any previous applied fuzz map. The + // AddFuzzArrayEntry function can be used to add additional fuzz map + // entries without removing the existing map. Returns E_INVALIDARG in the + // event that the total percentages add up to more than 100%. + __inline HRESULT SetFuzzArrayMap( + __in_ecount(cfae) const _fuzz_array_entry<_Type1, _Type2, _Args...> *rgfae, + __in ULONG cfae) throw() + { + ClearFuzzArrayEntries(); + for (ULONG i = 0; i < cfae; i++) + { + AddFuzzArrayEntry(rgfae[i].uiPercentage, rgfae[i].pfnFuzz, rgfae[i].pfnDealloc); + } + + return (m_iPercentageTotal >= 0) ? S_OK : E_INVALIDARG; + } + + // Adds an additional fuzz map entry, without clearing the existing + // fuzz map. Returns E_INVALIDARG in the event that the total percentages + // add up to more than 100%. + HRESULT AddFuzzArrayEntry( + __in unsigned int uiPercentage, + __in std::function<_Type1* (_Type1*, _Type2&, _Args...)> pfnFuzz, + __in std::function pfnDealloc = nullptr) throw() + { + _range_fuzz_array_entry<_Type1, _Type2, _Args...> r = { 0 }; + r.fae.uiPercentage = uiPercentage; + r.fae.pfnFuzz = pfnFuzz; + r.fae.pfnDealloc = pfnDealloc; + ConvertPercentageToRange(uiPercentage, m_iPercentageTotal, &r.range); + m_map.push_back(r); + return (m_iPercentageTotal >= 0) ? S_OK : E_INVALIDARG; + } + + void ClearFuzzArrayEntries() throw() + { + m_map.clear(); + m_iPercentageTotal = 100; + } + + // Invokes the fuzz map in the event that fuzzing as not been applied. + // Since the fuzz map entries have their own potential allocation and + // deallocation routines, we actually make another copy of the fuzzed + // buffer in the event that reallocation has occurred (determined by + // comparing against the original initialized pointer value). This + // is important because we use the _Alloc value to ensure the correct + // memory allocator is used that is appropriate for the calling code. + __inline _Type1* GetValueFromMap() + { + if (!m_fFuzzed) + { + m_fFuzzed = TRUE; + WORD wRandom = CFuzzChance::GetRandom(100); + for (auto &r : m_map) + { + if (r.range.iLow <= wRandom && wRandom < r.range.iHigh) + { + _Type1 *rgTemp = CallFuzzMapFunction(r.fae.pfnFuzz, m_rgCaller, *m_pcElems, m_tArgs); + if (rgTemp && rgTemp != m_rgCaller) + { + size_t cbRealloc = (m_ftEffectiveTraits & TRAIT_SIZE_IS_BCOUNT) ? + *m_pcElems : + *m_pcElems * sizeof(_Type1); + m_rgRealloc = reinterpret_cast<_Type1*>(_Alloc::Allocate(cbRealloc)); + if (m_rgRealloc) + { + memcpy_s(m_rgRealloc, cbRealloc, rgTemp, cbRealloc); + } + + if (r.fae.pfnDealloc) + { + r.fae.pfnDealloc(rgTemp); + } + } + + break; + } + } + } + + return (m_rgRealloc) ? m_rgRealloc : m_rgCaller; + } + private: + _Type1* m_rgCaller; + _Type1* m_rgRealloc; + _Type2 *m_pcElems; + FuzzTraits m_ftEffectiveTraits; + CFuzzArraySize<_Type1, _Type2, _Args...>* m_pfas; + std::vector<_range_fuzz_array_entry<_Type1, _Type2, _Args...> > m_map; + std::tuple<_Args...> m_tArgs; + + CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...>* Reference() + { + return this; + } + + template + _Type1* CallFuzzMapFunction( + std::function<_Type1* (_Type1*, _Type2&, _Args...)> pfnFuzz, + _Type1 *t1, + _Type2 &t2, + std::tuple<_Args...>& tup, + variadic::index) + { + return pfnFuzz(t1, t2, std::get(tup)...); + } + + _Type1* CallFuzzMapFunction( + std::function<_Type1* (_Type1*, _Type2&, _Args...)> pfnFuzz, + _Type1 *t1, + _Type2 &t2, + std::tuple<_Args...>& tup) + { + return CallFuzzMapFunction(pfnFuzz, t1, t2, tup, variadic::gen_seq < sizeof...(_Args) > {}); + } + + HRESULT Init( + __in_ecount(cfae) const _fuzz_array_entry<_Type1, _Type2, _Args...> *rgfae, + __in ULONG cfae) + { + m_rgRealloc = nullptr; + m_ftEffectiveTraits = m_traits; + + // Since constructors cannot return error values, the + // TRAIT_THROW_ON_INIT_FAILURE trait allows for an exception + // to be thrown in the event that this class was not initialized + // correctly. The intended purpose is to catch users of this + // codebase who have incorrectly specified fuzz maps that add up + // to more than 100%. + HRESULT hr = hr = SetFuzzArrayMap(rgfae, cfae); + if (FAILED(hr) && (m_traits & TRAIT_THROW_ON_INIT_FAILURE)) + { + throw CFuzzRangeException(); + } + + if (m_rgCaller == nullptr) + { + m_ftEffectiveTraits |= TRAIT_TRANSFER_ALLOCATION; + } + + return hr; + } + + void FreeRealloc() + { + if (m_rgRealloc) + { + // See comments about explaining when we transfer + // allocation and deallocation responsibilities. + if (m_ftEffectiveTraits & TRAIT_TRANSFER_ALLOCATION) + { + _Alloc::Free(m_rgCaller); + m_rgCaller = nullptr; + } + else + { + _Alloc::Free(m_rgRealloc); + m_rgRealloc = nullptr; + } + } + } + }; + + // When working with arrays, care must be taken when passing a pointer to a + // fuzzed array together with the size to ensure the size lines up with the + // the evaluated fuzz map. Consider the following: + // + // BYTE rg[10] = {0}; + // size_t cb = ARRAYSIZE(rg); + // CFuzzArray rgUntrusted(FUZZ_MAP(...), rg, cb); + // hr = foo(rgUntrusted, cb); + // + // When evaluating the arguments for the function foo, the value of cb + // will be pushed as an argument before fa has a chance to evaluate the fuzzing map. + // This results in the cb parameter to foo being the original value, even if the + // fuzzing map alters cb. + // + // To account for this CFuzzArraySize pairs together with a CFuzzArray instance to ensure + // the fuzz map is evaluated prior to either the array pointer or the size being used. + // For example: + // + // BYTE rg[10] = {0}; + // size_t cb = ARRAYSIZE(rg); + // __untrusted_array_size(BYTE, size_t) cbUntrusted = __untrusted_array_size_init(cb); + // __untrusted_array(BYTE, size_t) rgUntrusted = __untrusted_array_init(FUZZ_MAP(...), rg, cbUntrusted); + // hr = foo(rgUntrusted, cbUntrusted); + template + class CFuzzArraySize + { + public: + template friend class CFuzzArray; + + CFuzzArraySize(__inout _Type2& cElems) : + m_pcElems(&cElems), + m_pfa(nullptr) + { + } + + virtual ~CFuzzArraySize() { } + + __inline operator _Type2 () throw() + { + if (m_pfa) + { + m_pfa->GetValueFromMap(); + } + + return *m_pcElems; + } + + __inline _Type2* operator &() throw() + { + return m_pcElems; + } + + __inline const _Type2* operator &() const throw() + { + return m_pcElems; + } + + __inline void Pair(__in CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...> &rfa) + { + m_pfa = rfa.Reference(); + } + private: + CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...> *m_pfa; + _Type2 *m_pcElems; + + CFuzzArraySize<_Type1, _Type2, _Args...>* Reference() + { + return this; + } + }; + + // The CFuzzType class is primarily designed for primitive types but + // is also used as the base class for CFuzzTypePtr and CFuzzLpwstr. + // The various operator overloads allow this class to wrap a type and + // usage of that type works transparently. Asking for the value + // of the type wrapped by this class will invoke the fuzzing map. + template + class CFuzzType : public CFuzzBase + { + public: + // Creates an instance of CFuzzType, initializing the value to + // the value specified in t. Note that providing a fuzz map + // is optional, but then expected to be provided via SetFuzzTypeMap + // or AddFuzzTypeEntry if fuzzing is to be applied. + CFuzzType( + __in_ecount(cfte) const _fuzz_type_entry<_Type, _Args...> *rgfte, + __in ULONG cfte, + __in _Type t, + __in _Args&&... args) : + m_t(t), + m_tInit(t), + m_tArgs(std::forward<_Args>(args)...) + { + m_pfnOnFuzzedValueFromMap = [](_Type t, std::function) { return t; }; + HRESULT hr = SetFuzzTypeMap(rgfte, cfte); + + // Since constructors cannot return error values, the + // TRAIT_THROW_ON_INIT_FAILURE trait allows for an exception + // to be thrown in the event that this class was not initialized + // correctly. The intended purpose is to catch users of this + // codebase who have incorrectly specified fuzz maps that add up + // to more than 100%. + if (FAILED(hr) && (m_traits & TRAIT_THROW_ON_INIT_FAILURE)) + { + throw CFuzzRangeException(); + } + } + + virtual ~CFuzzType() + { + } + + // Initializes with the value specified by t and then applies + // the fuzz map to produce a fuzzed return value. Usage of this + // operator will make the CFuzzType instance look like a function + // call, which might be desirable in some instances. + // For example: + // __untrusted_t(int) iVal = __untrusted_init(FUZZ_MAP(...), 10); + // int iFuzzedVal = iVal(15); // re-initializes the default value to 15, + // // then applies the fuzz map to return + // // a potentially fuzzed value + __inline _Type operator () (__in _Type t) throw() + { + m_t = m_tInit = t; + return GetValueFromMap(); + } + + // Will reinitialize the value for the internally wrapped type. + // If the fuzzing map has already been evaluated, initializing + // via the = operator will not cause the fuzzing map to be + // evaluated when the value is subsequently retrieved. + __inline void operator = (__in _Type t) throw() + { + m_t = m_tInit = t; + } + + // Getting the value of this instance will cause the fuzz map + // to be evaluated. The fuzz map will only be evaluated once, + // subsequent access will always return the selected value. + __inline operator _Type () throw() + { + return GetValueFromMap(); + } + + // Allows initialization of the internal type by providing a + // reference. If the fuzz map has been evaluated, the reference + // is provided to the fuzzed value. + __inline virtual _Type* operator &() throw() + { + return (m_fFuzzed) ? &m_t : &m_tInit; + } + + // Similar to above, if the fuzz map has been evaluated, the + // reference is provided to the fuzzed value. + __inline const _Type* operator &() const throw() + { + return (m_fFuzzed) ? &m_t : &m_tInit; + } + + // Setting the fuzz map will clear any previous applied fuzz map. The + // AddFuzzArrayEntry function can be used to add additional fuzz map + // entries without removing the existing map. Returns E_INVALIDARG in the + // event that the total percentages add up to more than 100%. + __inline HRESULT SetFuzzTypeMap( + __in_ecount(cfte) const _fuzz_type_entry<_Type, _Args...> *rgfte, + __in ULONG cfte) throw() + { + ClearFuzzTypeEntries(); + for (ULONG i = 0; i < cfte; i++) + { + AddFuzzTypeEntry(rgfte[i].uiPercentage, rgfte[i].pfnFuzz, rgfte[i].pfnDealloc); + } + + return (m_iPercentageTotal >= 0) ? S_OK : E_INVALIDARG; + } + + // Adds an additional fuzz map entry, without clearing the existing + // fuzz map. Returns E_INVALIDARG in the event that the total percentages + // add up to more than 100%. + HRESULT AddFuzzTypeEntry( + __in unsigned int uiPercentage, + __in std::function<_Type(_Type, _Args...)> pfnFuzz, + __in std::function pfnDealloc = nullptr) throw() + { + _range_fuzz_type_entry<_Type, _Args...> r = { 0 }; + r.fte.uiPercentage = uiPercentage; + r.fte.pfnFuzz = pfnFuzz; + r.fte.pfnDealloc = pfnDealloc; + ConvertPercentageToRange(uiPercentage, m_iPercentageTotal, &r.range); + m_map.push_back(r); + return (m_iPercentageTotal >= 0) ? S_OK : E_INVALIDARG; + } + + void ClearFuzzTypeEntries() throw() + { + m_map.clear(); + m_iPercentageTotal = 100; + } + protected: + _Type m_t; + _Type m_tInit; + std::vector<_range_fuzz_type_entry<_Type, _Args...> > m_map; + std::function<_Type(_Type, std::function)> m_pfnOnFuzzedValueFromMap; + std::tuple<_Args...> m_tArgs; + + _Type CallFuzzMapFunction( + std::function<_Type(_Type, _Args...)> pfnFuzz, + _Type t, + std::tuple<_Args...>& tup) + { + return CallFuzzMapFunction(pfnFuzz, t, tup, variadic::gen_seq < sizeof...(_Args) > {}); + } + + // To support sub classes ability to realloc fuzzed values, + // the m_pfnOnFuzzedValueFromMap will be invoked whenever a fuzz map + // entry is selected. When not overridden, the default behavior is to + // set this classes value to the fuzzed value returned from the + // associated fuzzing routine. For an example that alters this behavior + // take a look at the CFuzzLpwstr class. + __inline virtual _Type GetValueFromMap() + { + if (!m_fFuzzed) + { + m_fFuzzed = TRUE; + m_t = m_tInit; + WORD wRandom = CFuzzChance::GetRandom(100); + for (auto &r : m_map) + { + if (r.range.iLow <= wRandom && wRandom < r.range.iHigh) + { + m_t = m_pfnOnFuzzedValueFromMap(CallFuzzMapFunction(r.fte.pfnFuzz, m_tInit, m_tArgs), r.fte.pfnDealloc); + break; + } + } + } + + return m_t; + } + private: + template + _Type CallFuzzMapFunction( + std::function<_Type(_Type, _Args...)> pfnFuzz, + _Type t, + std::tuple<_Args...>& tup, + variadic::index) + { + UNREFERENCED_PARAMETER(tup); // Compiler gets confused by the expansion of tup below. + return pfnFuzz(t, std::get(tup)...); + } + }; + + // The CFuzzTypePtr class provides all the same functionality as the + // base CFuzzType class, but also provides an additional operator + // override for ->. This allows pointers to be wrapped by this class + // to be seamlessly used in the calling codebase. Note that correct + // fuzzing behavior is entirely dependent on the design of the fuzzing + // map, this class is not intended to fuzz actual pointer values. + template + class CFuzzTypePtr : public CFuzzType < _Type, _Args... > + { + public: + // Note that _Type is expected to be a pointer type (thus making + // the operator override of -> make sense). It is the callers + // responsibility to set the type appropriately. + CFuzzTypePtr( + __in_ecount(cfte) const _fuzz_type_entry<_Type> *rgfte, + __in ULONG cfte, + __in _Type pt, + __in _Args&&... args) : + CFuzzType(rgfte, cfte, pt, std::forward<_Args>(args)...) + { + } + + virtual ~CFuzzTypePtr() { } + + _Type operator->() const throw() + { + return (m_fFuzzed) ? m_t : m_tInit; + } + + // This operator makes it possible to invoke the fuzzing map + // by calling this class like a parameterless function. This is + // used to support the __make_untrusted call below. Note that once + // the fuzzing map is invoked, all future access of the pointer + // members will be pointed to potentially fuzzed data (as per + // the logic of the fuzz map). + __inline void operator() () throw() + { + GetValueFromMap(); + } + }; + + // The CFuzzLpwstr class extends the CFuzzType class in order to + // provide the ability to realloc fuzzed strings. This is important + // because it might be desirable to grow or shrink a fuzzed string + // based upon the scenario. Because reallocation can occur, this + // class needs to maintain similar logic to the CFuzzArray class for + // transferring ownership of buffers. Similarly, if reallocation + // is allowed, _Alloc needs to match the appropriate allocation + // method used by the calling code. Please see CFuzzArray documentation + // for more details. + template + class CFuzzString : public CFuzzType <_Type*, _Args...> + { + public: + CFuzzString( + __in_ecount(cfte) const _fuzz_type_entry<_Type*, _Args...> *rgfte, + __in ULONG cfte, + __in _Type *psz, + __in _Args... args) : + CFuzzType(rgfte, cfte, psz, std::forward<_Args>(args)...) + { + OnFuzzedValueFromMap(); + } + + virtual ~CFuzzString() + { + FreeFuzzedString(); + } + + // Provide operator overloading to allow this class to be + // initialized across function calls that would allocate + // a string. It is assumed that if initialization occurs + // via this method and then reallocation occurs, it will + // be necessary to transfer ownership of who frees the + // appropriate buffer. + // For example: + // __untrusted_lpwstr pwsz = + // __untrusted_lpwstr_init(FUZZ_MAP(...), nullptr); + // AllocateString(&pwsz); + // ...use pwsz to invoke fuzzing map... + // CoTaskMemFree(pwsz); // If reallocation occurs, + // // using pwsz will provide the + // // fuzzed string. This means + // // pwsz is responsible for freeing + // // the original allocation. + __inline virtual _Type** operator &() throw() + { + m_ftEffectiveTraits |= TRAIT_TRANSFER_ALLOCATION; + return (m_fFuzzed) ? &m_t : &m_tInit; + } + private: + _Type *m_pszFuzzed; + FuzzTraits m_ftEffectiveTraits; + + // Since reallocation could occur that is dependent on matching the + // allocation routines of the calling code, we want to ensure we + // allocate using the correct allocator specified by _Alloc. However, + // the allocation routines in the fuzzing map could differ, therefore + // we copy the fuzzed string as appropriate and use the specified + // dealloc function in the fuzz map entry to delete the fuzzed string + // created by the fuzz map entry. Slightly inefficient, but safer + // and supports a wider variety of scenarios. + void OnFuzzedValueFromMap() + { + m_pszFuzzed = nullptr; + m_ftEffectiveTraits = m_traits; + m_pfnOnFuzzedValueFromMap = [&](_Type *psz, std::function dealloc) + { + FreeFuzzedString(); + _Type *pszFuzzed = psz; + if (psz && psz != m_tInit) + { + size_t cb = (sizeof(_Type) == sizeof(char)) ? + (strlen(reinterpret_cast(psz)) + 1) * sizeof(char) : + (wcslen(reinterpret_cast(psz)) + 1) * sizeof(WCHAR); + m_pszFuzzed = reinterpret_cast<_Type*>(_Alloc::Allocate(cb)); + if (m_pszFuzzed) + { + (sizeof(_Type) == sizeof(char)) ? + StringCbCopyA(reinterpret_cast(m_pszFuzzed), cb, reinterpret_cast(psz)) : + StringCbCopyW(reinterpret_cast(m_pszFuzzed), cb, reinterpret_cast(psz)); + pszFuzzed = m_pszFuzzed; + } + + if (dealloc) + { + dealloc(psz); + } + } + + return pszFuzzed; + }; + } + + void FreeFuzzedString() + { + if (m_pszFuzzed) + { + // See comments about explaining when we transfer + // allocation and deallocation responsibilities. + if (m_ftEffectiveTraits & TRAIT_TRANSFER_ALLOCATION) + { + _Alloc::Free(m_tInit); + m_tInit = nullptr; + } + else + { + _Alloc::Free(m_pszFuzzed); + m_pszFuzzed = nullptr; + } + } + } + }; + + // The CFuzzFlags class extends the CFuzzType base class, but + // operates slightly differently than other fuzz class implementations. + // The intended usage of this class is when dealing with bit flags, + // where covering the range of possible bit flag combinations would be + // very tedious for the fuzz map designer. To aid in this scenario, + // the fuzz map entries are evaluated on a per flag basis, meaning the + // chance of inserting that flag is dependent on the percentage value. + // For example: + // In the below fuzz map, CFuzzFlags will interpret this to mean + // apply STARTF_FORCEONFEEDBACK 10% of the time, together with + // STARTF_FORCEOFFFEEDBACK 2% of the time, together with + // STARTF_PREVENTPINNING %1 of the time, etc. + // + // _fuzz_type_entry _fuzz_dwFlags_map[] = + // { + // { 10, [] (DWORD) { return STARTF_FORCEONFEEDBACK; } }, + // { 2, [] (DWORD) { return STARTF_FORCEOFFFEEDBACK; } }, + // { 1, [] (DWORD) { return STARTF_PREVENTPINNING; } }, + // { 50, [] (DWORD) { return STARTF_RUNFULLSCREEN; } }, + // }; + // + // Also note that this class should specifically not have the + // TRAIT_THROW_ON_INIT_FAILURE trait, as it is expected that the + // percentages will total more than 100% when added together. + template + class CFuzzFlags : public CFuzzType < _Type, _Args... > + { + public: + CFuzzFlags( + __in_ecount(cfte) const _fuzz_type_entry<_Type> *rgfte, + __in ULONG cfte, + __in _Type flags, + __in _Args&&... args) : + CFuzzType(rgfte, cfte, flags, std::forward<_Args>(args)...) + { + } + + virtual ~CFuzzFlags() + { + } + protected: + __inline virtual _Type GetValueFromMap() + { + if (!m_fFuzzed) + { + m_t = 0; + m_fFuzzed = TRUE; + for (auto &r : m_map) + { + // Generate a new random value during each map entry + // and use it to evaluate if each individual fuzz map + // entry should be applied. + WORD wRandom = CFuzzChance::GetRandom(100); + + // Translate percentages to allow for each flag to be considered + // for inclusion independently. + int iHigh = 100; + int iLow = iHigh - (r.range.iHigh - r.range.iLow); + if (iLow <= wRandom && wRandom < iHigh) + { + m_t |= CallFuzzMapFunction(r.fte.pfnFuzz, m_tInit, m_tArgs); + } + } + } + + return m_t; + } + }; +} + +// To make using these classes a no-opt if fuzzing is not intended to +// be applied, the following set of functions and defines operate +// differently if __GENERATE_DIRECTED_FUZZING is defined. If defined, +// these wrappers use the correct fuzz classes and initialize these +// classes with the appropriate map specified by the caller. If not +// defined, essentially the use of these classes disappear and the +// remaining functions are optimized away by the compiler. When +// applied appropriately, not turning on fuzzing leaves no performance +// impact, no size footprint, and no leakage of information into the +// symbols. +#ifdef __GENERATE_DIRECTED_FUZZING +#define FUZZ_ARRAY_START_STATIC(name, type, size, ...) static const fuzz::_fuzz_array_entry name[] = { +#define FUZZ_ARRAY_START(name, type, size, ...) const fuzz::_fuzz_array_entry name[] = { +#define FUZZ_ARRAY_END() }; + +#define FUZZ_TYPE_START_STATIC(name, type, ...) static const fuzz::_fuzz_type_entry name[] = { +#define FUZZ_TYPE_START(name, type, ...) const fuzz::_fuzz_type_entry name[] = { +#define FUZZ_TYPE_END() }; + +#define FUZZ_MAP_ENTRY_ALLOC(i, pfnAlloc, pfnDelete) { i, pfnAlloc, pfnDelete }, +#define FUZZ_MAP_ENTRY(i, pfnAlloc) { i, pfnAlloc }, + +#define FUZZ_MAP(x) x, ARRAYSIZE(x) + +#define __untrusted_t(type, ...) fuzz::CFuzzType +#define __untrusted_lpwstr_t(...) fuzz::CFuzzString<__FUZZING_ALLOCATOR, WCHAR, __VA_ARGS__> +#define __untrusted_lpwstr fuzz::CFuzzString<__FUZZING_ALLOCATOR, WCHAR> +#define __untrusted_lpstr_t(...) fuzz::CFuzzString<__FUZZING_ALLOCATOR, CHAR, __VA_ARGS__> +#define __untrusted_lpstr fuzz::CFuzzString<__FUZZING_ALLOCATOR, CHAR> +#define __untrusted_ptr(ptr, ...) fuzz::CFuzzTypePtr +#define __untrusted_array(ptr, size, ...) fuzz::CFuzzArray<__FUZZING_ALLOCATOR, ptr, size, __VA_ARGS__> +#define __untrusted_array_size(ptr, size, ...) fuzz::CFuzzArraySize + +template +static fuzz::CFuzzType<_Type, _Args...> __untrusted_init( + __in_ecount(cfte) const fuzz::_fuzz_type_entry<_Type, _Args...> *rgfte, + __in ULONG cfte, + __in _Type t, + __in _Args&&... args) +{ + return fuzz::CFuzzType<_Type, _Args...>(rgfte, cfte, t, std::forward<_Args>(args)...); +} + +template +static fuzz::CFuzzString<__FUZZING_ALLOCATOR, WCHAR, _Args...> __untrusted_lpwstr_init( + __in_ecount(cfte) const fuzz::_fuzz_type_entry *rgfte, + __in ULONG cfte, + __in LPWSTR pwsz, + __in _Args&&... args) +{ + return fuzz::CFuzzString<__FUZZING_ALLOCATOR, WCHAR, _Args...>(rgfte, cfte, pwsz, std::forward<_Args>(args)...); +} + +template +static fuzz::CFuzzString<__FUZZING_ALLOCATOR, CHAR, _Args...> __untrusted_lpstr_init( + __in_ecount(cfte) const fuzz::_fuzz_type_entry *rgfte, + __in ULONG cfte, + __in LPSTR psz, + __in _Args&&... args) +{ + return fuzz::CFuzzString<__FUZZING_ALLOCATOR, CHAR, _Args...>(rgfte, cfte, psz, std::forward<_Args>(args)...); +} + +template +static fuzz::CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...> __untrusted_array_init( + __in_ecount(cfae) const fuzz::_fuzz_array_entry<_Type1, _Type2, _Args...> *rgfae, + __in ULONG cfae, + __in _Type1 *pt, + __in fuzz::CFuzzArraySize<_Type1, _Type2, _Args...> &rfas, + __in _Args&&... args) +{ + return fuzz::CFuzzArray<__FUZZING_ALLOCATOR, _Type1, _Type2, _Args...>(rgfae, cfae, pt, rfas, std::forward<_Args>(args)...); +} + +template +static fuzz::CFuzzArraySize<_Type1, _Type2, _Args...> __untrusted_array_size_init(__inout _Type2 &t2) +{ + return fuzz::CFuzzArraySize<_Type1, _Type2, _Args...>(t2); +} + +template +static void __make_untrusted_ptr(__in fuzz::CFuzzTypePtr<_Type, _Args...> &rftp) +{ + rftp(); +} +#else +#define FUZZ_ARRAY_START_STATIC(name, type, size, ...) +#define FUZZ_ARRAY_START(name, type, size, ...) +#define FUZZ_ARRAY_END() + +#define FUZZ_TYPE_START_STATIC(name, type, ...) +#define FUZZ_TYPE_START(name, type, ...) +#define FUZZ_TYPE_END() + +#define FUZZ_MAP_ENTRY_ALLOC(i, pfnAlloc, pfnDelete) +#define FUZZ_MAP_ENTRY(i, pfnAlloc) + +#define FUZZ_MAP(x) nullptr, 0 + +#define __untrusted_t(type, ...) type +#define __untrusted_lpwstr_t(...) LPWSTR +#define __untrusted_lpwstr LPWSTR +#define __untrusted_lpstr_t(...) LPSTR +#define __untrusted_lpstr LPSTR +#define __untrusted_ptr(type, ...) type +#define __untrusted_array(ptr, size, ...) ptr* +#define __untrusted_array_size(ptr, size, ...) size + +template +static __forceinline _Type __untrusted_init( + __in_opt void*, + __in ULONG, + __in _Type t, + __in _Args&&...) +{ + return t; +} + +template +static __forceinline LPWSTR __untrusted_lpwstr_init( + __in_opt void*, + __in ULONG, + __in LPWSTR pwsz, + __in _Args&&...) +{ + return pwsz; +} + +template +static __forceinline LPSTR __untrusted_lpstr_init( + __in_opt void*, + __in ULONG, + __in LPSTR psz, + __in _Args&&...) +{ + return psz; +} + +template +static __forceinline _Type1* __untrusted_array_init( + __in_opt void*, + __in ULONG, + __in _Type1 *pt, + __in _Type2, + __in _Args&&...) +{ + return pt; +} + +template +static _Type2 __untrusted_array_size_init(_Type2 t2) +{ + return t2; +} + +template +static __forceinline void __make_untrusted_ptr(__in _Type&) +{ + return; +} +#endif + +#endif diff --git a/src/terminal/parser/ft_fuzzer/fuzzing_logic.h b/src/terminal/parser/ft_fuzzer/fuzzing_logic.h new file mode 100644 index 000000000..9e529fc5a --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/fuzzing_logic.h @@ -0,0 +1,492 @@ +// +// fuzzing_logic.h +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// The directed fuzzing logic header. Must be paired together with fuzzing_directed.h. +// +#ifndef __FUZZING_LOGIC_H__ +#define __FUZZING_LOGIC_H__ + +#pragma once + +#include "fuzzing_directed.h" +#ifdef __GENERATE_DIRECTED_FUZZING +#include +#include + +namespace fuzz +{ + // + // Fuzzing manipulations follow the following name conventions: + // _[fz/const]_[type]_[description] + // [fz/const] : separates randomized actions versus those that produce a constant effect + // [type] : the type of data that is being fuzzed, abbreviated by its hungarian notation + // [description] : describe the manipulation that is being applied + // + + // + // The string fuzzing functions are designed to allow the caller to determine if NULL + // termination is necessary. For an example of the appropriate way to call any of the + // _fz_wsz_* functions, see FuzzStringW and FuzzStringW_NoRealloc. + // + + // Inserts a format character to a random location within the string. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _fz_wsz_addFormatChar(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + if (rcch > 1) + { + const LPWSTR rgFormatStringChars[] = { L"%n", L"%s", L"%d" }; + size_t cbDestSize = 2 * sizeof(WCHAR); + memcpy_s( + &(pwsz[CFuzzChance::GetRandom(rcch - 1)]), // -1 because we are writing 2 chars + cbDestSize, + CFuzzChance::SelectOne(rgFormatStringChars, ARRAYSIZE(rgFormatStringChars)), + cbDestSize); + } + + return pwsz; + } + + // Inserts a format character to a random location within the string. + // Note that rcch does not include the NULL terminator + static LPSTR _fz_sz_addFormatChar(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + if (rcch > 1) + { + const LPSTR rgFormatStringChars[] = { "%n", "%s", "%d" }; + size_t cbDestSize = 2 * sizeof(CHAR); + memcpy_s( + &(psz[CFuzzChance::GetRandom(rcch - 1)]), + cbDestSize, + CFuzzChance::SelectOne(rgFormatStringChars, ARRAYSIZE(rgFormatStringChars)), + cbDestSize); + } + + return psz; + } + + // Adds a character related to paths to a random location within the string. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _fz_wsz_addPathChar(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + if (rcch > 0) + { + const WCHAR rgPathChars[] = { L'.', L'\\', L'/', L':', L',', L';' }; + pwsz[CFuzzChance::GetRandom(rcch)] = + CFuzzChance::SelectOne(rgPathChars, ARRAYSIZE(rgPathChars)); + } + + return pwsz; + } + + // Adds a character related to paths to a random location within the string. + // Note that rcch does not include the NULL terminator + static LPSTR _fz_sz_addPathChar(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + if (rcch > 0) + { + const CHAR rgPathChars[] = { '.', '\\', '/', ':', ',', ';' }; + psz[CFuzzChance::GetRandom(rcch)] = + CFuzzChance::SelectOne(rgPathChars, ARRAYSIZE(rgPathChars)); + } + + return psz; + } + + // Adds an invalid path character to a random location within the string. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _fz_wsz_addInvalidPathChar(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + if (rcch > 0) + { + const WCHAR rgInvalidPathChars[] = { L'?', L'<', L'>', L'"', L'|', L'*' }; + pwsz[CFuzzChance::GetRandom(rcch)] = + CFuzzChance::SelectOne(rgInvalidPathChars, ARRAYSIZE(rgInvalidPathChars)); + } + + return pwsz; + } + + // Adds an invalid path character to a random location within the string. + // Note that rcch does not include the NULL terminator + static LPSTR _fz_sz_addInvalidPathChar(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + if (rcch > 0) + { + const CHAR rgInvalidPathChars[] = { '?', '<', '>', '"', '|', '*' }; + psz[CFuzzChance::GetRandom(rcch)] = + CFuzzChance::SelectOne(rgInvalidPathChars, ARRAYSIZE(rgInvalidPathChars)); + } + + return psz; + } + + // Implementation depends on CFuzzLogic class and is therefore + // declared after CFuzzLogic is defined below. + template static _Type* _fz_flipBYTE(__inout_ecount(rcelms) _Type *p, __inout size_t &rcelms); + template static _Type* _fz_flipWCHAR(__inout_ecount(rcelms) _Type *p, __inout size_t &rcelms); + static char* _fz_sz_tokenizeSpaces(__in char *psz); + + // Mirrors the first half of the string across the second half of the string. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _const_wsz_mirror(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + if (rcch > 0) + { + size_t cchStart = 0; + size_t cchEnd = rcch - 1; + while (cchStart < cchEnd) + { + pwsz[cchEnd--] = pwsz[cchStart++]; + } + } + + return pwsz; + } + + // Mirrors the first half of the string across the second half of the string. + // Note that rcch does not include the NULL terminator + static LPSTR _const_sz_mirror(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + if (rcch > 0) + { + size_t cchStart = 0; + size_t cchEnd = rcch - 1; + while (cchStart < cchEnd) + { + psz[cchEnd--] = psz[cchStart++]; + } + } + + return psz; + } + + // Replicates the string repeatedly until the end of the buffer is reached. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _const_wsz_replicate(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + if (rcch > 0) + { + size_t cch = wcslen(pwsz); + size_t cchStart = 0; + while (cch < rcch) + { + pwsz[cch++] = pwsz[cchStart++]; + } + } + + return pwsz; + } + + // Replicates the string repeatedly until the end of the buffer is reached. + // Note that rcch does not include the NULL terminator + static LPSTR _const_sz_replicate(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + if (rcch > 0) + { + size_t cch = strlen(psz); + size_t cchStart = 0; + while (cch < rcch) + { + psz[cch++] = psz[cchStart++]; + } + } + + return psz; + } + + // Replaces the string with a valid system path to shell32.dll in the system32 dir. + // Note that rcch is the count of characters (minus the NULL terminator), not the count of bytes. + static LPWSTR _const_wsz_validPath(__inout_ecount(rcch) WCHAR *pwsz, __inout size_t &rcch) + { + WCHAR wszSystemDirectory[MAX_PATH] = { 0 }; + if (GetSystemDirectoryW(wszSystemDirectory, ARRAYSIZE(wszSystemDirectory))) + { + StringCchPrintfW(pwsz, rcch, L"%s\\shell32.dll", wszSystemDirectory); + } + + return pwsz; + } + + // Replaces the string with a valid system path to shell32.dll in the system32 dir. + // Note that rcch does not include the NULL terminator + static LPSTR _const_sz_validPath(__inout_ecount(rcch) CHAR *psz, __inout size_t &rcch) + { + CHAR szSystemDirectory[MAX_PATH] = { 0 }; + if (GetSystemDirectoryA(szSystemDirectory, ARRAYSIZE(szSystemDirectory))) + { + StringCchPrintfA(psz, rcch, "%s\\shell32.dll", szSystemDirectory); + } + + return psz; + } + + // Reverses the string in place. + static LPWSTR _const_wsz_reverse(__inout WCHAR *pwsz, __inout size_t &) + { + return _wcsrev(pwsz); + } + + // Reverses the string in place. + static LPSTR _const_sz_reverse(__inout CHAR *psz, __inout size_t &) + { + return _strrev(psz); + } + + // Contains fuzzing logic based upon a variety of default scenarios. The + // idea is to capture and make available a comprehensive fuzzing library + // that does not require external modules or complex setup. This should + // make fuzzing easier to implement and test, as well as more explicit + // with regard to what fuzzing manipulations are possible. + template + class CFuzzLogic + { + public: + // Permutes a random element of the array with a valid value that can be + // contained within the size of a single element. See _fz_wsz_flipBYTE + // and _fz_wsz_flipWCHAR for an example of how the element size determines + // the amount of data manipulated. + template + static _Type* FuzzArrayElement(__in_ecount(cElems) _Type *rg, __in size_t cElems) throw() + { + if (rg && cElems) + { + rg[CFuzzChance::GetRandom(cElems)] = + static_cast<_Type>(CFuzzChance::GetRandom( + static_cast(pow(static_cast(2), static_cast(sizeof(_Type) * 8))))); + } + + return rg; + } + + // Fuzzes a string by allocating a new fuzzed string. Note that the string + // length can shrink or grow in relation to the template data passed in + // via pwsz. The maximum size the string can grow is 2 times the current + // length. + static LPWSTR FuzzStringW(__in LPCWSTR pwsz) throw() + { + const _fuzz_type_entry rgfte[] = + { + { 10, [](size_t cch) { return CFuzzChance::GetRandom(cch + 1); } }, + { 50, [](size_t cch) { return cch + CFuzzChance::GetRandom(cch + 1); } } + }; + CFuzzType fuzz_cb(FUZZ_MAP(rgfte), wcslen(pwsz)); + + size_t cch = fuzz_cb + 1; // add 1 for ensuring NULL termination + LPWSTR pwszRealloc = reinterpret_cast(_Alloc::Allocate(cch * sizeof(WCHAR))); + if (pwszRealloc) + { + pwszRealloc[--cch] = L'\0'; + StringCchCopyW(pwszRealloc, cch, pwsz); + FuzzStringW_NoRealloc(pwszRealloc, cch); + } + + return pwszRealloc; + } + + // Fuzzes a string by allocating a new fuzzed string. Note that the string + // length can shrink or grow in relation to the template data passed in + // via pwsz. The maximum size the string can grow is 2 times the current + // length. + static LPSTR FuzzStringA(__in LPCSTR psz) throw() + { + LPSTR pszRealloc = nullptr; + + const _fuzz_type_entry rgfte[] = + { + { 10, [](size_t cch) { return CFuzzChance::GetRandom(cch + 1); } }, + { 50, [](size_t cch) { return cch + CFuzzChance::GetRandom(cch + 1); } } + }; + CFuzzType fuzz_cch(FUZZ_MAP(rgfte), strlen(psz)); + + size_t cchTemp = fuzz_cch + 1; // add 1 for ensuring NULL termination + LPSTR pszReallocTemp = reinterpret_cast(_Alloc::Allocate(cchTemp * sizeof(CHAR))); + if (pszReallocTemp) + { + pszReallocTemp[--cchTemp] = '\0'; + StringCchCopyA(pszReallocTemp, cchTemp, psz); + + const _fuzz_type_entry fuzzMap[] = + { + { 5, _fz_sz_tokenizeSpaces, FreeFuzzedBuffer }, + { 95, [=](LPSTR p) + { + size_t cchInner = cchTemp; + return FuzzStringA_NoRealloc(p, cchInner); + } + } + }; + CFuzzString<__FUZZING_ALLOCATOR, CHAR> eval(FUZZ_MAP(fuzzMap), pszReallocTemp); + + // Performance optimization: 95% of the time the buffer returned from eval will not be reallocated + // and thus will still be pointing at pszReallocTemp. In this case, it is not necessary to duplicate + // the string a second time, just transfer ownership to the calling function. + if ((LPSTR)eval == pszReallocTemp) + { + pszRealloc = pszReallocTemp; + } + else + { + pszRealloc = DuplicateStringA(eval); + FreeFuzzedBuffer(pszReallocTemp); + } + } + + return pszRealloc; + } + + // Fuzzes a string in place, no new memory is allocated to perform this + // fuzzing. This means that the return value is the same as the pwsz + // parameter. + static LPWSTR FuzzStringW_NoRealloc(__inout LPWSTR pwsz) throw() + { + size_t cch = wcslen(pwsz); + return FuzzStringW_NoRealloc(pwsz, cch); + } + + // Fuzzes a string in place, no new memory is allocated to perform this + // fuzzing. This means that the return value is the same as the psz + // parameter. + static LPSTR FuzzStringA_NoRealloc(__inout LPSTR psz) throw() + { + size_t cch = strlen(psz); + return FuzzStringA_NoRealloc(psz, cch); + } + + static LPSTR DuplicateStringA(__in LPCSTR psz) throw() + { + size_t cch = strlen(psz) + 1; + LPSTR pszDuplicate = reinterpret_cast(_Alloc::Allocate(cch * sizeof(CHAR))); + if (pszDuplicate) + { + StringCchCopyA(pszDuplicate, cch, psz); + } + + return pszDuplicate; + } + + // This function will free any allocations that are made as part of this + // class. Specifically, fuzzed strings created with FuzzStringW should + // always be freed with FreeFuzzedBuffer. The prototype of this function + // should always support being used within a fuzz array entry or a fuzz + // type entry as the pfnDealloc function. + static void FreeFuzzedBuffer(void *pv) throw() + { + _Alloc::Free(pv); + } + private: + CFuzzLogic() { }; + virtual ~CFuzzLogic() { }; + + static LPWSTR FuzzStringW_NoRealloc(__inout LPWSTR pwsz, __inout size_t &rcch) + { + if (rcch > 0) + { + const _fuzz_array_entry rgfae[] = + { + // small randomized manipulations + { 21, _fz_wsz_addFormatChar }, + { 21, _fz_wsz_addPathChar }, + { 21, _fz_wsz_addInvalidPathChar }, + { 11, [](WCHAR *pwsz, size_t &rcch) { return _fz_flipByte(pwsz, rcch); } }, + { 10, [](WCHAR *pwsz, size_t &rcch) { return _fz_flipEntry(pwsz, rcch); } }, + + // non-random manipulations + { 4, _const_wsz_replicate }, + { 4, _const_wsz_mirror }, + { 4, _const_wsz_validPath }, + { 4, _const_wsz_reverse } + }; + CFuzzArray<__FUZZING_ALLOCATOR, WCHAR, size_t> fa(FUZZ_MAP(rgfae), pwsz, rcch); + fa.GetValueFromMap(); + } + + return pwsz; + } + + static LPSTR FuzzStringA_NoRealloc(__inout LPSTR psz, __inout size_t &rcch) + { + if (rcch > 0) + { + const _fuzz_array_entry rgfae[] = + { + // small randomized manipulations + { 21, _fz_sz_addFormatChar }, + { 21, _fz_sz_addPathChar }, + { 21, _fz_sz_addInvalidPathChar }, + { 21, [](CHAR *psz, size_t &rcch) { return _fz_flipByte(psz, rcch); } }, + + // non-random manipulations + { 4, _const_sz_replicate }, + { 4, _const_sz_mirror }, + { 4, _const_sz_validPath }, + { 4, _const_sz_reverse } + }; + CFuzzArray<__FUZZING_ALLOCATOR, CHAR, size_t> fa(FUZZ_MAP(rgfae), psz, rcch); + fa.GetValueFromMap(); + } + + return psz; + } + }; + + // Flips a random byte value within the buffer. + template + static _Type* _fz_flipByte(__inout_ecount(rcelms) _Type *p, __inout size_t &rcelms) + { + if (rcelms > 0) + { + return reinterpret_cast<_Type*>(CFuzzLogic<>::FuzzArrayElement( + reinterpret_cast(p), (rcelms)* sizeof(_Type))); + } + + return p; + } + + // Flips a random entry value within the buffer + template + static _Type* _fz_flipEntry(__inout_ecount(rcelms) _Type *p, __inout size_t &rcelms) + { + if (rcelms > 0) + { + return CFuzzLogic<>::FuzzArrayElement(p, rcelms); + } + + return p; + } + + static char* _fz_sz_tokenizeSpaces(__in char *psz) + { + const _fuzz_type_entry repeatMap[] = + { + { 10, [](DWORD) { return 0; } }, + { 10, [](DWORD) { return 2; } }, + { 1, [](DWORD) { return CFuzzChance::GetRandom(0xF); } } + }; + + CStringA sFuzzed; + char *next_token = nullptr; + char *token = strtok_s(psz, " ", &next_token); + while (token) + { + CFuzzType repeat(FUZZ_MAP(repeatMap), 1); + for (DWORD i = 0; i < (DWORD)repeat; i++) + { + sFuzzed.AppendFormat("%s ", token); + } + + token = strtok_s(nullptr, " ", &next_token); + } + + // If psz has a final trailing space, avoid trimming it away. Otherwise, remove + // the extra added final space appended via the loop above. + size_t cch = strlen(psz); + return CFuzzLogic<>::DuplicateStringA(psz[cch] == ' ' ? sFuzzed.TrimRight() : sFuzzed); + } +} +#endif + +#endif diff --git a/src/terminal/parser/ft_fuzzer/run.bat b/src/terminal/parser/ft_fuzzer/run.bat new file mode 100644 index 000000000..0d47824f0 --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/run.bat @@ -0,0 +1 @@ +%_NTTREE%\unittests\VTCommandFuzzer.exe %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzer/sources b/src/terminal/parser/ft_fuzzer/sources new file mode 100644 index 000000000..8707044ea --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/sources @@ -0,0 +1,71 @@ +# ------------------------------------- +# Windows Console +# - Console Virtual Terminal Parser Fuzzer +# ------------------------------------- + +# This program will generate fuzz input for the parsing engine +# and is to be used in conjunction with the fuzz wrapper tool. + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = VTCommandFuzzer +TARGETTYPE = PROGRAM +UMTYPE = console +UMENTRY = wmain +TARGET_DESTINATION = UnitTests +DLLDEF = + +TEST_CODE = 1 + +# ------------------------------------- +# CRT Configuration +# ------------------------------------- + +USE_UNICRT = 1 +USE_MSVCRT = 1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + +BUILD_FOR_CORESYSTEM = 1 +ATL_VER = 70 +USE_STATIC_ATL = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +UNICODE = 1 +C_DEFINES = $(C_DEFINES) -DUNICODE -D_UNICODE + +# ------------------------------------- +# Compiler Settings +# ------------------------------------- + +MSC_WARNING_LEVEL = /W4 /WX +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = stdafx.h + +SOURCES = \ + VTCommandFuzzer.cpp \ + + + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ diff --git a/src/terminal/parser/ft_fuzzer/stdafx.cpp b/src/terminal/parser/ft_fuzzer/stdafx.cpp new file mode 100644 index 000000000..5a0412f98 --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/stdafx.cpp @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// stdafx.cpp : source file that includes just the standard includes +// VTCommandFuzzer.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/src/terminal/parser/ft_fuzzer/stdafx.h b/src/terminal/parser/ft_fuzzer/stdafx.h new file mode 100644 index 000000000..2c0653506 --- /dev/null +++ b/src/terminal/parser/ft_fuzzer/stdafx.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include diff --git a/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj b/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj new file mode 100644 index 000000000..dd4c122b1 --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj @@ -0,0 +1,39 @@ + + + + + + Create + + + + + + + + + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + + {F210A4AE-E02A-4BFC-80BB-F50A672FE763} + Win32Proj + FuzzWrapper + TerminalParser.FuzzWrapper + ConTerm.Parser.FuzzWrapper + + + + _CONSOLE;%(PreprocessorDefinitions) + + + Console + + + + + + + \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj.filters b/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj.filters new file mode 100644 index 000000000..b1d364aa2 --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/FuzzWrapper.vcxproj.filters @@ -0,0 +1,36 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzwrapper/apple.ans b/src/terminal/parser/ft_fuzzwrapper/apple.ans new file mode 100644 index 000000000..203eb17dd --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/apple.ans @@ -0,0 +1,49 @@ +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛßßßßßßÛÛÛÛÛÛÛÛÛÛÛÛß ÜÛÛ +Û ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛß ÜÜÜÜ ÛÜÜ ßÛÛ +ÛÛÛÛÛß ÜÛßß ÜÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛßß ÛÛ +ÛÛÛÛÛÜÜÜÜÜÜÜÜÜßßßÜÜÜÜÜÜÜßß Ü +ÜÜÛÛ ßÛ Û Ü ßßßßß  +ÜÛßßßßßÜÜÜÜÜÜÜÜÜÜÜÜÜÜ +ÜÜÜÜÜÜÜßßßßßßßßßÛÜÜÜ ßß  +ÜÜ ÛÜ ßß Ü Ü ßßßßßßÜÞÛ +Ûßßßßßßßßßßßßßßßßßßßß +ÛÛÛÛÛÛÛÛÛÜ ßßÛ ßß ß Ü + Û Û Ûßß ÜÜÜÜ ß Û +ÝÝÞÜßß ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÜ + ßßßÛ Û Û Û ß +ßßß ÜÛÛÛÛÛÛÛ ÞÛÛÞÛÛÛÜÜÜÜ +ÛÛÝ ÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜ +ÛÛÛÛÛÛÛÛÛÛÛÝ ÛÝÝÞÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ ÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜßß ÜÜÜÜÜ  +ÞÛÛ ÜÜÜÜÜÜ ßßßßßßßÜÜ +ßßßßßÜÜÜÜÜÜÜÜÜÜßßßßßßßßßßßßßßßßßßßßßßßßß +ßßßßß ÜÛÛÛÛÛÛÛÛÜÛ ÜÛÛÛÛÛÛÛÛÛÛÜ +ßßßßßßßßßÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +Ûß ÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛßÜ +ßÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ ÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÝ Û +ÞÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÝÞÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛ ÞÝÛÛÛÛÛÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ +ÜÜÜÜÜÜÜÜÜÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÝ ÛÞÜÜÜÜßßßßßßßßßßßßßßßßßß +ßßßßßßßÝÞÛ ÛÛÛÛÛÛÛÛÛÛÛÛÜ ßÛÛÛÛÛ +Ý ÞÝßßßßÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÛÛÝÞÝ ÛÛÛ +ÛÛÛÛÛÛÛÛÛÛ ÛÛÛÝ ÛÝÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ Û ÞÛÛÛÛÛÛÛÛÛÛÛÛÝ ÞÛÝ  +ÛÞÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÝÞÝ ßßßÛÛÛÛÛÛÛÛß  + ß ÛÝÛÛÛÛÛÜÜÜÜÜÜÜÜÜÜÜÜ +ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÛ ßßßßß Û +ßÜÜÜÜÜÜßßßßßßßßßßßßßßßßßßßßßßßßßßßÜ ß +Ü ÛÝÜßßßßßßß +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÜ ßÜ ÜÜÛß +ÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÜ ß +ßÜÜÜÜÜ ÜÜÛÛßßÜÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÛÛÜÜÜ ßßßßßßßßßßß ÜÜÜÛÜÜÜÜÜÜÜÜÜÜÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛÛ +ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ +ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzwrapper/colortest.bat b/src/terminal/parser/ft_fuzzwrapper/colortest.bat new file mode 100644 index 000000000..797900b70 --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/colortest.bat @@ -0,0 +1 @@ +echo FooBarBazBazzNormal. \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzwrapper/echoDispatch.cpp b/src/terminal/parser/ft_fuzzwrapper/echoDispatch.cpp new file mode 100644 index 000000000..34a8d872b --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/echoDispatch.cpp @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "echoDispatch.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +void EchoDispatch::Print(const wchar_t wchPrintable) +{ + wprintf(L"Print: %c (0x%x)\r\n", wchPrintable, wchPrintable); +} + +void EchoDispatch::PrintString(const wchar_t* const rgwch, const size_t cch) +{ + wprintf(L"PrintString: \"%s\" (%zd chars)\r\n", rgwch, cch); +} + +void EchoDispatch::Execute(const wchar_t wchControl) +{ + wprintf(L"Execute: 0x%x\r\n", wchControl); +} diff --git a/src/terminal/parser/ft_fuzzwrapper/echoDispatch.hpp b/src/terminal/parser/ft_fuzzwrapper/echoDispatch.hpp new file mode 100644 index 000000000..48b925535 --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/echoDispatch.hpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "../../adapter/termDispatch.hpp" + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class EchoDispatch : public TermDispatch + { + public: + void Print(const wchar_t wchPrintable) override; + void PrintString(const wchar_t* const rgwch, const size_t cch) override; + void Execute(const wchar_t wchControl) override; + }; + } + } +} diff --git a/src/terminal/parser/ft_fuzzwrapper/main.cpp b/src/terminal/parser/ft_fuzzwrapper/main.cpp new file mode 100644 index 000000000..fb452b7b4 --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/main.cpp @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "echoDispatch.hpp" +#include "..\stateMachine.hpp" +#include "..\OutputStateMachineEngine.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +void PrintUsage() +{ + wprintf(L"Usage: conterm.parser.fuzzwrapper.exe \r\n"); + wprintf(L"Use codepage 1200 for Unicode. 437 for US English. 0 for reading straight as ASCII.\r\n"); +} + +UINT const UNICODE_CP = 1200; +UINT const ASCII_CP = 0; +UINT uiCodePage = UNICODE_CP; +FILE* hFile = nullptr; + +bool GetChar(wchar_t* pwch) +{ + if (uiCodePage == UNICODE_CP) + { + *pwch = fgetwc(hFile); + return *pwch != WEOF; + } + else if (uiCodePage == ASCII_CP) + { + int ch = fgetc(hFile); + *pwch = 0; + *pwch = (wchar_t)ch; + return ch != EOF; + } + else + { + int ch = fgetc(hFile); + MultiByteToWideChar(uiCodePage, 0, (char*)&ch, 1, pwch, 1); + return ch != EOF; + } +} + +int __cdecl wmain(int argc, wchar_t* argv[]) +{ + int ret = 0; + + if (argc != 3) + { + PrintUsage(); + ret = E_INVALIDARG; + } + else + { + uiCodePage = (UINT)_wtoi(argv[2]); + wprintf(L"Using codepage '%d'", uiCodePage); + + wprintf(L"Opening file '%s'...\r\n", argv[1]); + hFile = _wfopen(argv[1], L"r"); + wchar_t wch; + bool fGotChar = GetChar(&wch); + + StateMachine machine(new OutputStateMachineEngine(new EchoDispatch)); + + wprintf(L"Sending characters to state machine...\r\n"); + while (fGotChar) + { + machine.ProcessCharacter(wch); + fGotChar = GetChar(&wch); + } + + fclose(hFile); + wprintf(L"Done.\r\n"); + } + + return ret; +} diff --git a/src/terminal/parser/ft_fuzzwrapper/precomp.cpp b/src/terminal/parser/ft_fuzzwrapper/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/terminal/parser/ft_fuzzwrapper/precomp.h b/src/terminal/parser/ft_fuzzwrapper/precomp.h new file mode 100644 index 000000000..7cd07d0ea --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/precomp.h @@ -0,0 +1,23 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#include + +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" diff --git a/src/terminal/parser/ft_fuzzwrapper/run.bat b/src/terminal/parser/ft_fuzzwrapper/run.bat new file mode 100644 index 000000000..ce3dd15db --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/run.bat @@ -0,0 +1 @@ +%_NTTREE%\unittests\conterm.parser.fuzzwrapper.exe %1 %2 %3 %4 %5 %6 \ No newline at end of file diff --git a/src/terminal/parser/ft_fuzzwrapper/sources b/src/terminal/parser/ft_fuzzwrapper/sources new file mode 100644 index 000000000..c9cc02aaa --- /dev/null +++ b/src/terminal/parser/ft_fuzzwrapper/sources @@ -0,0 +1,57 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Virtual Terminal Parser Fuzz Wrapper +# ------------------------------------- + +# This program wraps the Virtual Terminal Parser lib in a simple +# console application that can be used for fuzz testing purposes. +# It will simply read in text in a variety of formats and inject the text +# into the parsing engine. +# It is expected that this binary is monitored during its runtime for +# various security concerns (overruns, heap issues, etc.) + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTerm.Parser.FuzzWrapper +TARGETTYPE = PROGRAM +UMTYPE = console +UMENTRY = wmain +TARGET_DESTINATION = UnitTests +DLLDEF = + +TEST_CODE = 1 + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = precomp.h + +SOURCES = \ + main.cpp \ + echoDispatch.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib \ + $(OBJ_PATH)\..\lib\$(O)\ConTermParser.lib \ diff --git a/src/terminal/parser/lib/parser-common.vcxproj b/src/terminal/parser/lib/parser-common.vcxproj new file mode 100644 index 000000000..35dc08e33 --- /dev/null +++ b/src/terminal/parser/lib/parser-common.vcxproj @@ -0,0 +1,24 @@ + + + + + + + + + + Create + + + + + + + + + + + + diff --git a/src/terminal/parser/lib/parser.vcxproj b/src/terminal/parser/lib/parser.vcxproj new file mode 100644 index 000000000..9cb410779 --- /dev/null +++ b/src/terminal/parser/lib/parser.vcxproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + {3AE13314-1939-4DFA-9C14-38CA0834050C} + Win32Proj + parser + TerminalParser + ConTermParser + + + + + diff --git a/src/terminal/parser/lib/parser.vcxproj.filters b/src/terminal/parser/lib/parser.vcxproj.filters new file mode 100644 index 000000000..7e66305d3 --- /dev/null +++ b/src/terminal/parser/lib/parser.vcxproj.filters @@ -0,0 +1,51 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/parser/lib/sources b/src/terminal/parser/lib/sources new file mode 100644 index 000000000..5dab4f9bc --- /dev/null +++ b/src/terminal/parser/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTermParser +TARGETTYPE = LIBRARY diff --git a/src/terminal/parser/precomp.cpp b/src/terminal/parser/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/terminal/parser/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/terminal/parser/precomp.h b/src/terminal/parser/precomp.h new file mode 100644 index 000000000..38a4422bc --- /dev/null +++ b/src/terminal/parser/precomp.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +*/ + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include + +#include + +#include + +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS +#include + +#include "telemetry.hpp" +#include "tracing.hpp" diff --git a/src/terminal/parser/runfuzz.bat b/src/terminal/parser/runfuzz.bat new file mode 100644 index 000000000..653de2c54 --- /dev/null +++ b/src/terminal/parser/runfuzz.bat @@ -0,0 +1,7 @@ +mkdir %_NTTREE%\unittests\ft_fuzzer +CALL .\ft_fuzzer\run.bat 10 %_NTTREE%\unittests\ft_fuzzer +for /f %%f in ('dir /B %_NTTREE%\unittests\ft_fuzzer\*.bin') DO ( + CALL .\ft_fuzzwrapper\run.bat %%f 0 + CALL .\ft_fuzzwrapper\run.bat %%f 1200 + CALL .\ft_fuzzwrapper\run.bat %%f 437 +) diff --git a/src/terminal/parser/runtest.bat b/src/terminal/parser/runtest.bat new file mode 100644 index 000000000..33b7b7070 --- /dev/null +++ b/src/terminal/parser/runtest.bat @@ -0,0 +1 @@ +.\ut_parser\run.bat %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/terminal/parser/sources.inc b/src/terminal/parser/sources.inc new file mode 100644 index 000000000..d49cff67e --- /dev/null +++ b/src/terminal/parser/sources.inc @@ -0,0 +1,51 @@ +!include ..\..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Virtual Terminal Parser +# ------------------------------------- + +# This module provides the ability to parse an incoming stream +# of text for Virtual Terminal Sequences. +# These sequences are in-band signaling embedded within the stream +# that signals the console to do something special (beyond just displaying as text). + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES = \ + ..\stateMachine.cpp \ + ..\InputStateMachineEngine.cpp \ + ..\OutputStateMachineEngine.cpp \ + ..\telemetry.cpp \ + ..\tracing.cpp \ + +INCLUDES = \ + $(INCLUDES); \ + ..; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-keyboard-l1.lib \ + +DLOAD_ERROR_HANDLER = kernelbase + +DELAYLOAD = \ + ext-ms-win-ntuser-keyboard-l1.dll; \ diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp new file mode 100644 index 000000000..432d287ce --- /dev/null +++ b/src/terminal/parser/stateMachine.cpp @@ -0,0 +1,1445 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "stateMachine.hpp" + +#include "ascii.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +//Takes ownership of the pEngine. +StateMachine::StateMachine(IStateMachineEngine* const pEngine) : + _pEngine(THROW_IF_NULL_ALLOC(pEngine)), + _state(VTStates::Ground), + _trace(Microsoft::Console::VirtualTerminal::ParserTracing()), + _cParams(0), + _pusActiveParam(nullptr), + _cIntermediate(0), + _wchIntermediate(UNICODE_NULL), + _pwchCurr(nullptr), + _iParamAccumulatePos(0), + // pwchOscStringBuffer Initialized below + _pwchSequenceStart(nullptr), + // rgusParams Initialized below + _sOscNextChar(0), + _sOscParam(0), + _currRunLength(0) +{ + ZeroMemory(_pwchOscStringBuffer, sizeof(_pwchOscStringBuffer)); + ZeroMemory(_rgusParams, sizeof(_rgusParams)); + _ActionClear(); +} + +const IStateMachineEngine& StateMachine::Engine() const noexcept +{ + return *_pEngine; +} + +IStateMachineEngine& StateMachine::Engine() noexcept +{ + return *_pEngine; +} + +// 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. +bool StateMachine::s_IsActionableFromGround(const wchar_t wch) +{ + return (wch <= AsciiChars::US) || s_IsC1Csi(wch) || s_IsDelete(wch); +} + +// Routine Description: +// - Determines if a character belongs to the C0 escape range. +// This is character sequences less than a space character (null, backspace, new line, etc.) +// See also https://en.wikipedia.org/wiki/C0_and_C1_control_codes +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsC0Code(const wchar_t wch) +{ + return (wch >= AsciiChars::NUL && wch <= AsciiChars::ETB) || + wch == AsciiChars::EM || + (wch >= AsciiChars::FS && wch <= AsciiChars::US); +} + +// Routine Description: +// - Determines if a character is a C1 CSI (Control Sequence Introducer) +// This is a single-character way to start a control sequence, as opposed to "ESC[". +// +// Not all single-byte codepages support C1 control codes--in some, the range that would +// be used for C1 codes are instead used for additional graphic characters. +// +// However, we do not need to worry about confusion whether a single byte \x9b in a +// single-byte stream represents a C1 CSI or some other glyph, because by the time we +// get here, everything is Unicode. Knowing whether a single-byte \x9b represents a +// single-character C1 CSI or some other glyph is handled by MultiByteToWideChar before +// we get here (if the stream was not already UTF-16). For instance, in CP_ACP, if a +// \x9b shows up, it will get converted to \x203a. So, if we get here, and have a +// \x009b, we know that it unambiguously represents a C1 CSI. +// +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsC1Csi(const wchar_t wch) +{ + return wch == L'\x9b'; +} + +// Routine Description: +// - Determines if a character is a valid intermediate in an VT escape sequence. +// Intermediates are punctuation type characters that are generally vendor specific and +// modify the operational mode of a command. +// See also http://vt100.net/emu/dec_ansi_parser +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsIntermediate(const wchar_t wch) +{ + return wch >= L' ' && wch <= L'/'; // 0x20 - 0x2F +} + +// Routine Description: +// - Determines if a character is the delete character. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsDelete(const wchar_t wch) +{ + return wch == AsciiChars::DEL; +} + +// Routine Description: +// - Determines if a character is the escape character. +// Used to start escape sequences. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsEscape(const wchar_t wch) +{ + return wch == AsciiChars::ESC; +} + +// Routine Description: +// - Determines if a character is "control sequence" beginning indicator. +// This immediately follows an escape and signifies a varying length control sequence. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsCsiIndicator(const wchar_t wch) +{ + return wch == L'['; // 0x5B +} + +// Routine Description: +// - Determines if a character is a delimiter between two parameters in a "control sequence" +// This occurs in the middle of a control sequence after escape and CsiIndicator have been recognized +// between a series of parameters. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsCsiDelimiter(const wchar_t wch) +{ + return wch == L';'; // 0x3B +} + +// Routine Description: +// - Determines if a character is a valid parameter value +// Parameters must be numerical digits. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsCsiParamValue(const wchar_t wch) +{ + return wch >= L'0' && wch <= L'9'; // 0x30 - 0x39 +} + +// Routine Description: +// - Determines if a character is a private range marker for a control sequence. +// Private range markers indicate vendor-specific behavior. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsCsiPrivateMarker(const wchar_t wch) +{ + return wch == L'<' || wch == L'=' || wch == L'>' || wch == L'?'; // 0x3C - 0x3F +} + +// Routine Description: +// - Determines if a character is invalid in a control sequence +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsCsiInvalid(const wchar_t wch) +{ + return wch == L':'; // 0x3A +} + +// Routine Description: +// - Determines if a character is "operating system control string" beginning +// indicator. +// This immediately follows an escape and signifies a signifies a varying +// length control sequence, quite similar to CSI. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsSs3Indicator(const wchar_t wch) +{ + return wch == L'O'; // 0x4F +} + +// Routine Description: +// - Determines if a character is a "Single Shift Select" indicator. +// This immediately follows an escape and signifies a varying length control string. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscIndicator(const wchar_t wch) +{ + return wch == L']'; // 0x5D +} + +// Routine Description: +// - Determines if a character is a delimiter between two parameters in a "operating system control sequence" +// This occurs in the middle of a control sequence after escape and OscIndicator have been recognized, +// after the paramater indicating which OSC action to take. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscDelimiter(const wchar_t wch) +{ + return wch == L';'; // 0x3B +} + +// Routine Description: +// - Determines if a character is a valid parameter value for an OSC String, +// that is, the indicator of which OSC action to take. +// Parameters must be numerical digits. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscParamValue(const wchar_t wch) +{ + return s_IsNumber(wch); // 0x30 - 0x39 +} + +// Routine Description: +// - Determines if a character should be initiate the end of an OSC sequence. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscTerminationInitiator(const wchar_t wch) +{ + return wch == AsciiChars::ESC; +} + +// Routine Description: +// - Determines if a character should be ignored in a operating system control sequence +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscInvalid(const wchar_t wch) +{ + return wch <= L'\x17' || + wch == L'\x19' || + (wch >= L'\x1c' && wch <= L'\x1f') ; +} + +// Routine Description: +// - Determines if a character is "operating system control string" termination indicator. +// This signals the end of an OSC string collection. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsOscTerminator(const wchar_t wch) +{ + return wch == L'\x7' || wch == L'\x9C'; // Bell character or C1 terminator +} + +// Routine Description: +// - Determines if a character is a valid number character, 0-9. +// Arguments: +// - wch - Character to check. +// Return Value: +// - True if it is. False if it isn't. +bool StateMachine::s_IsNumber(const wchar_t wch) +{ + return wch >= L'0' && wch <= L'9'; // 0x30 - 0x39 +} + +// Routine Description: +// - Triggers the Execute action to indicate that the listener should immediately respond to a C0 control character. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionExecute(const wchar_t wch) +{ + _trace.TraceOnExecute(wch); + _pEngine->ActionExecute(wch); + +} + +// Routine Description: +// - Triggers the Execute action to indicate that the listener should +// immediately respond to a C0 control character, with the added +// information that we're executing it from the Escsape state. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionExecuteFromEscape(const wchar_t wch) +{ + _trace.TraceOnExecuteFromEscape(wch); + _pEngine->ActionExecuteFromEscape(wch); + +} + +// Routine Description: +// - Triggers the Print action to indicate that the listener should render the character given. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionPrint(const wchar_t wch) +{ + _trace.TraceOnAction(L"Print"); + _pEngine->ActionPrint(wch); +} + + +// Routine Description: +// - Triggers the EscDispatch action to indicate that the listener should handle a simple escape sequence. +// These sequences traditionally start with ESC and a simple letter. No complicated parameters. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionEscDispatch(const wchar_t wch) +{ + _trace.TraceOnAction(L"EscDispatch"); + + bool fSuccess = _pEngine->ActionEscDispatch(wch, _cIntermediate, _wchIntermediate); + + // Trace the result. + _trace.DispatchSequenceTrace(fSuccess); + + if (!fSuccess) + { + // Suppress it and log telemetry on failed cases + TermTelemetry::Instance().LogFailed(wch); + } +} + +// Routine Description: +// - Triggers the CsiDispatch action to indicate that the listener should handle a control sequence. +// These sequences perform various API-type commands that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionCsiDispatch(const wchar_t wch) +{ + _trace.TraceOnAction(L"CsiDispatch"); + + bool fSuccess = _pEngine->ActionCsiDispatch(wch, _cIntermediate, _wchIntermediate, _rgusParams, _cParams); + + // Trace the result. + _trace.DispatchSequenceTrace(fSuccess); + + if (!fSuccess) + { + // Suppress it and log telemetry on failed cases + TermTelemetry::Instance().LogFailed(wch); + } +} + +// Routine Description: +// - Triggers the Collect action to indicate that the state machine should store this character as part of an escape/control sequence. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionCollect(const wchar_t wch) +{ + _trace.TraceOnAction(L"Collect"); + + // store collect data + if (_cIntermediate < s_cIntermediateMax) + { + _wchIntermediate = wch; + } + + _cIntermediate++; +} + +// Routine Description: +// - Triggers the Param action to indicate that the state machine should store this character as a part of a parameter +// to a control sequence. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionParam(const wchar_t wch) +{ + _trace.TraceOnAction(L"Param"); + + // Verify both the count and that the pointer didn't run off the end of the + // array. If we're past the end, this param is just ignored. + if (_cParams <= s_cParamsMax && _pusActiveParam < &_rgusParams[s_cParamsMax]) + { + // If we're adding a character to the first parameter, + // then we now have one parameter. + if (_iParamAccumulatePos == 0 && _cParams == 0) + { + _cParams++; + } + + // On a delimiter, increase the number of params we've seen. + // "Empty" params should still count as a param - + // eg "\x1b[0;;m" should be three "0" params + if (wch == L';') + { + // Move to next param. + // If we're on the last param (_cParams == s_cParamsMax), + // then _pusActiveParam will now be past the end of _rgusParams, + // and any future params will be ignored. + _pusActiveParam++; + + // clear out the accumulator count to prepare for the next one + _iParamAccumulatePos = 0; + + // Don't increment the _cParams to be greater than s_cParamsMax. + // We're using _pusActiveParam to make sure we don't fill too + // many params. + if (_cParams < s_cParamsMax) + { + _cParams++; + } + } + else + { + // don't bother accumulating if we're storing more than 4 digits (since we're putting it into a short) + if (_iParamAccumulatePos < 5) + { + // convert character into digit. + unsigned short const usDigit = wch - L'0'; // convert character into value + + // multiply existing values by 10 to make space in the 1s digit + *_pusActiveParam *= 10; + + // mark that we've now stored another digit. + _iParamAccumulatePos++; + + // store the digit in the 1s place. + *_pusActiveParam += usDigit; + + if (*_pusActiveParam > SHORT_MAX) + { + *_pusActiveParam = SHORT_MAX; + } + } + else + { + *_pusActiveParam = SHORT_MAX; + } + } + } +} + +// Routine Description: +// - Triggers the Clear action to indicate that the state machine should erase all internal state. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionClear() +{ + _trace.TraceOnAction(L"Clear"); + + // clear all internal stored state. + _wchIntermediate = 0; + _cIntermediate = 0; + + for (unsigned short i = 0; i < s_cParamsMax; i++) + { + _rgusParams[i] = 0; + } + + _cParams = 0; + _iParamAccumulatePos = 0; + _pusActiveParam = _rgusParams; // set pointer back to beginning of array + + _sOscParam = 0; + _sOscNextChar = 0; + + _pEngine->ActionClear(); + +} + +// Routine Description: +// - Triggers the Ignore action to indicate that the state machine should eat this character and say nothing. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionIgnore() +{ + // do nothing. + _trace.TraceOnAction(L"Ignore"); +} + +// Routine Description: +// - Stores this character as part of the param indicating which OSC action to take. +// Arguments: +// - wch - Character to collect. +// Return Value: +// - +void StateMachine::_ActionOscParam(const wchar_t wch) +{ + _trace.TraceOnAction(L"OscParamCollect"); + + // don't bother accumulating if we're storing more than 4 digits (since we're putting it into a short) + if (_iParamAccumulatePos < 5) + { + // convert character into digit. + unsigned short const usDigit = wch - L'0'; // convert character into value + + // multiply existing values by 10 to make space in the 1s digit + _sOscParam *= 10; + + // mark that we've now stored another digit. + _iParamAccumulatePos++; + + // store the digit in the 1s place. + _sOscParam += usDigit; + + if (_sOscParam > SHORT_MAX) + { + _sOscParam = SHORT_MAX; + } + } + else + { + _sOscParam = SHORT_MAX; + } +} + +// Routine Description: +// - Stores this character as part of the OSC string +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionOscPut(const wchar_t wch) +{ + _trace.TraceOnAction(L"OscPut"); + + // if we're past the end, this param is just ignored. + // need to leave one char for \0 at end + if (_sOscNextChar < s_cOscStringMaxLength - 1) + { + _pwchOscStringBuffer[_sOscNextChar] = wch; + _sOscNextChar++; + //we'll place the null at the end of the string when we send the actual action. + } +} + +// Routine Description: +// - Triggers the CsiDispatch action to indicate that the listener should handle a control sequence. +// These sequences perform various API-type commands that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionOscDispatch(const wchar_t wch) +{ + _trace.TraceOnAction(L"OscDispatch"); + + bool fSuccess = _pEngine->ActionOscDispatch(wch, _sOscParam, _pwchOscStringBuffer, _sOscNextChar); + + // Trace the result. + _trace.DispatchSequenceTrace(fSuccess); + + if (!fSuccess) + { + // Suppress it and log telemetry on failed cases + TermTelemetry::Instance().LogFailed(wch); + } +} + +// Routine Description: +// - Triggers the Ss3Dispatch action to indicate that the listener should handle a control sequence. +// These sequences perform various API-type commands that can include many parameters. +// Arguments: +// - wch - Character to dispatch. +// Return Value: +// - +void StateMachine::_ActionSs3Dispatch(const wchar_t wch) +{ + _trace.TraceOnAction(L"Ss3Dispatch"); + + bool fSuccess = _pEngine->ActionSs3Dispatch(wch, _rgusParams, _cParams); + + // Trace the result. + _trace.DispatchSequenceTrace(fSuccess); + + if (!fSuccess) + { + // Suppress it and log telemetry on failed cases + TermTelemetry::Instance().LogFailed(wch); + } +} + +// Routine Description: +// - Moves the state machine into the Ground state. +// This state is entered: +// 1. By default at the beginning of operation +// 2. After any execute/dispatch action. +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterGround() +{ + _state = VTStates::Ground; + _trace.TraceStateChange(L"Ground"); +} + +// Routine Description: +// - Moves the state machine into the Escape state. +// This state is entered: +// 1. When the Escape character is seen at any time. +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterEscape() +{ + _state = VTStates::Escape; + _trace.TraceStateChange(L"Escape"); + _ActionClear(); + _trace.ClearSequenceTrace(); +} + +// Routine Description: +// - Moves the state machine into the EscapeIntermediate state. +// This state is entered: +// 1. When EscIntermediate characters are seen after an Escape entry (only from the Escape state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterEscapeIntermediate() +{ + _state = VTStates::EscapeIntermediate; + _trace.TraceStateChange(L"EscapeIntermediate"); +} + +// Routine Description: +// - Moves the state machine into the CsiEntry state. +// This state is entered: +// 1. When the CsiEntry character is seen after an Escape entry (only from the Escape state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterCsiEntry() +{ + _state = VTStates::CsiEntry; + _trace.TraceStateChange(L"CsiEntry"); + _ActionClear(); +} + +// Routine Description: +// - Moves the state machine into the CsiParam state. +// This state is entered: +// 1. When valid parameter characters are detected on entering a CSI (from CsiEntry state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterCsiParam() +{ + _state = VTStates::CsiParam; + _trace.TraceStateChange(L"CsiParam"); +} + +// Routine Description: +// - Moves the state machine into the CsiIgnore state. +// This state is entered: +// 1. When an invalid character is detected during a CSI sequence indicating we should ignore the whole sequence. +// (From CsiEntry, CsiParam, or CsiIntermediate states.) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterCsiIgnore() +{ + _state = VTStates::CsiIgnore; + _trace.TraceStateChange(L"CsiIgnore"); +} + +// Routine Description: +// - Moves the state machine into the CsiIntermediate state. +// This state is entered: +// 1. When an intermediate character is seen immediately after entering a control sequence (from CsiEntry) +// 2. When an intermediate character is seen while collecting parameter data (from CsiParam) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterCsiIntermediate() +{ + _state = VTStates::CsiIntermediate; + _trace.TraceStateChange(L"CsiIntermediate"); +} + +// Routine Description: +// - Moves the state machine into the OscParam state. +// This state is entered: +// 1. When an OscEntry character (']') is seen after an Escape entry (only from the Escape state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterOscParam() +{ + _state = VTStates::OscParam; + _trace.TraceStateChange(L"OscParam"); +} + +// Routine Description: +// - Moves the state machine into the OscString state. +// This state is entered: +// 1. When a delimiter character (';') is seen in the OSC Param state. +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterOscString() +{ + _state = VTStates::OscString; + _trace.TraceStateChange(L"OscString"); +} + +// Routine Description: +// - Moves the state machine into the OscTermination state. +// This state is entered: +// 1. When an ESC is seen in an OSC string. This escape will be followed by a +// '\', as to encode a 0x9C as a 7-bit ASCII char stream. +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterOscTermination() +{ + _state = VTStates::OscTermination; + _trace.TraceStateChange(L"OscTermination"); +} + +// Routine Description: +// - Moves the state machine into the Ss3Entry state. +// This state is entered: +// 1. When the Ss3Entry character is seen after an Escape entry (only from the Escape state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterSs3Entry() +{ + _state = VTStates::Ss3Entry; + _trace.TraceStateChange(L"Ss3Entry"); + _ActionClear(); +} + +// Routine Description: +// - Moves the state machine into the Ss3Param state. +// This state is entered: +// 1. When valid parameter characters are detected on entering a SS3 (from Ss3Entry state) +// Arguments: +// - +// Return Value: +// - +void StateMachine::_EnterSs3Param() +{ + _state = VTStates::Ss3Param; + _trace.TraceStateChange(L"Ss3Param"); +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the Ground state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Handle a C1 Control Sequence Introducer +// 3. Print all other characters +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventGround(const wchar_t wch) +{ + _trace.TraceOnEvent(L"Ground"); + if (s_IsC0Code(wch) || s_IsDelete(wch)) + { + _ActionExecute(wch); + } + else if (s_IsC1Csi(wch)) + { + _EnterCsiEntry(); + } + else + { + _ActionPrint(wch); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the Escape state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Enter Control Sequence state +// 5. Dispatch an Escape action. +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventEscape(const wchar_t wch) +{ + _trace.TraceOnEvent(L"Escape"); + if (s_IsC0Code(wch)) + { + if (_pEngine->DispatchControlCharsFromEscape()) + { + _ActionExecuteFromEscape(wch); + _EnterGround(); + } + else + { + _ActionExecute(wch); + } + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsIntermediate(wch)) + { + _ActionCollect(wch); + _EnterEscapeIntermediate(); + } + else if (s_IsCsiIndicator(wch)) + { + _EnterCsiEntry(); + } + else if (s_IsOscIndicator(wch)) + { + _EnterOscParam(); + } + else if (s_IsSs3Indicator(wch)) + { + _EnterSs3Entry(); + } + else + { + _ActionEscDispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the EscapeIntermediate state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Dispatch an Escape action. +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventEscapeIntermediate(const wchar_t wch) +{ + _trace.TraceOnEvent(L"EscapeIntermediate"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsIntermediate(wch)) + { + _ActionCollect(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else + { + _ActionEscDispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiEntry state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 5. Store parameter data +// 6. Collect Control Sequence Private markers +// 7. Dispatch a control sequence with parameters for action +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventCsiEntry(const wchar_t wch) +{ + _trace.TraceOnEvent(L"CsiEntry"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsIntermediate(wch)) + { + _ActionCollect(wch); + _EnterCsiIntermediate(); + } + else if (s_IsCsiInvalid(wch)) + { + _EnterCsiIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiDelimiter(wch)) + { + _ActionParam(wch); + _EnterCsiParam(); + } + else if (s_IsCsiPrivateMarker(wch)) + { + _ActionCollect(wch); + _EnterCsiParam(); + } + else + { + _ActionCsiDispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiIntermediate state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 5. Dispatch a control sequence with parameters for action +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventCsiIntermediate(const wchar_t wch) +{ + _trace.TraceOnEvent(L"CsiIntermediate"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsIntermediate(wch)) + { + _ActionCollect(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiInvalid(wch) || s_IsCsiDelimiter(wch) || s_IsCsiPrivateMarker(wch)) + { + _EnterCsiIgnore(); + } + else + { + _ActionCsiDispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiIgnore state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 5. Return to Ground +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventCsiIgnore(const wchar_t wch) +{ + _trace.TraceOnEvent(L"CsiIgnore"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsIntermediate(wch)) + { + _ActionIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiInvalid(wch) || s_IsCsiDelimiter(wch) || s_IsCsiPrivateMarker(wch)) + { + _ActionIgnore(); + } + else + { + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiParam state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Collect Intermediate characters +// 4. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 5. Store parameter data +// 6. Dispatch a control sequence with parameters for action +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventCsiParam(const wchar_t wch) +{ + _trace.TraceOnEvent(L"CsiParam"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiDelimiter(wch)) + { + _ActionParam(wch); + } + else if (s_IsIntermediate(wch)) + { + _ActionCollect(wch); + _EnterCsiIntermediate(); + } + else if (s_IsCsiInvalid(wch) || s_IsCsiPrivateMarker(wch)) + { + _EnterCsiIgnore(); + } + else + { + _ActionCsiDispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the OscParam state. +// Events in this state will: +// 1. Collect numeric values into an Osc Param +// 2. Move to the OscString state on a delimiter +// 3. Ignore everything else. +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventOscParam(const wchar_t wch) +{ + _trace.TraceOnEvent(L"OscParam"); + if (s_IsOscTerminator(wch)) + { + _EnterGround(); + } + else if (s_IsOscParamValue(wch)) + { + _ActionOscParam(wch); + } + else if (s_IsOscDelimiter(wch)) + { + _EnterOscString(); + } + else + { + _ActionIgnore(); + } +} + +// Routine Description: +// - Processes a character event into a Action that occurs while in the OscParam state. +// Events in this state will: +// 1. Trigger the OSC action associated with the param on an OscTerminator +// 2. If we see a ESC, enter the OscTermination state. We'll wait for one +// more character before we dispatch the string. +// 3. Ignore OscInvalid characters. +// 4. Collect everything else into the OscString +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventOscString(const wchar_t wch) +{ + _trace.TraceOnEvent(L"OscString"); + if (s_IsOscTerminator(wch)) + { + _ActionOscDispatch(wch); + _EnterGround(); + } + else if (s_IsOscTerminationInitiator(wch)) + { + _EnterOscTermination(); + } + else if (s_IsOscInvalid(wch)) + { + _ActionIgnore(); + } + else + { + // add this character to our OSC string + _ActionOscPut(wch); + } +} + +// Routine Description: +// - Handle the two-character termination of a OSC sequence. +// Events in this state will: +// 1. Trigger the OSC action associated with the param on an OscTerminator +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventOscTermination(const wchar_t wch) +{ + _trace.TraceOnEvent(L"OscTermination"); + + _ActionOscDispatch(wch); + _EnterGround(); +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the Ss3Entry state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 4. Store parameter data +// 5. Dispatch a control sequence with parameters for action +// SS3 sequences are structurally the same as CSI sequences, just with a +// different initiation. It's safe to reuse CSI's functions for +// determining if a character is a parameter, delimiter, or invalid. +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventSs3Entry(const wchar_t wch) +{ + _trace.TraceOnEvent(L"Ss3Entry"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsCsiInvalid(wch)) + { + // It's safe for us to go into the CSI ignore here, because both SS3 and + // CSI sequences ignore characters the same way. + _EnterCsiIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiDelimiter(wch)) + { + _ActionParam(wch); + _EnterSs3Param(); + } + else + { + _ActionSs3Dispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Processes a character event into an Action that occurs while in the CsiParam state. +// Events in this state will: +// 1. Execute C0 control characters +// 2. Ignore Delete characters +// 3. Begin to ignore all remaining parameters when an invalid character is detected (CsiIgnore) +// 4. Store parameter data +// 5. Dispatch a control sequence with parameters for action +// Arguments: +// - wch - Character that triggered the event +// Return Value: +// - +void StateMachine::_EventSs3Param(const wchar_t wch) +{ + _trace.TraceOnEvent(L"Ss3Param"); + if (s_IsC0Code(wch)) + { + _ActionExecute(wch); + } + else if (s_IsDelete(wch)) + { + _ActionIgnore(); + } + else if (s_IsCsiParamValue(wch) || s_IsCsiDelimiter(wch)) + { + _ActionParam(wch); + } + else if (s_IsCsiInvalid(wch) || s_IsCsiPrivateMarker(wch)) + { + _EnterCsiIgnore(); + } + else + { + _ActionSs3Dispatch(wch); + _EnterGround(); + } +} + +// Routine Description: +// - Entry to the state machine. Takes characters one by one and processes them according to the state machine rules. +// Arguments: +// - wch - New character to operate upon +// Return Value: +// - +void StateMachine::ProcessCharacter(const wchar_t wch) +{ + _trace.TraceCharInput(wch); + + // Process "from anywhere" events first. + if (wch == AsciiChars::CAN || + wch == AsciiChars::SUB) + { + _ActionExecute(wch); + _EnterGround(); + } + else if (s_IsEscape(wch) && _state != VTStates::OscString) + { + // Don't go to escape from the OSC string state - ESC can be used to + // terminate OSC strings. + _EnterEscape(); + } + else + { + // Then pass to the current state as an event + switch (_state) + { + case VTStates::Ground: + return _EventGround(wch); + case VTStates::Escape: + return _EventEscape(wch); + case VTStates::EscapeIntermediate: + return _EventEscapeIntermediate(wch); + case VTStates::CsiEntry: + return _EventCsiEntry(wch); + case VTStates::CsiIntermediate: + return _EventCsiIntermediate(wch); + case VTStates::CsiIgnore: + return _EventCsiIgnore(wch); + case VTStates::CsiParam: + return _EventCsiParam(wch); + case VTStates::OscParam: + return _EventOscParam(wch); + case VTStates::OscString: + return _EventOscString(wch); + case VTStates::OscTermination: + return _EventOscTermination(wch); + case VTStates::Ss3Entry: + return _EventSs3Entry(wch); + case VTStates::Ss3Param: + return _EventSs3Param(wch); + default: + return; + } + } +} +// Method Description: +// - Pass the current string we're processing through to the engine. It may eat +// the string, it may write it straight to the input unmodified, it might +// write the string to the tty application. A pointer to this function will +// get handed to the OutputStateMachineEngine, so that it can write strings +// it doesn't understand to the tty. +// This does not modify the state of the state machine. Callers should be in +// the Action*Dispatch state, and upon completion, the state's handler (eg +// _EventCsiParam) should move us into the ground state. +// Arguments: +// - +// Return Value: +// - true if the engine successfully handled the string. +bool StateMachine::FlushToTerminal() +{ + // _pwchCurr is incremented after a call to ProcessCharacter to indicate + // that pwchCurr was processed. + // However, if we're here, then the processing of pwchChar triggered the + // engine to request the entire sequence get passed through, including pwchCurr. + return _pEngine->ActionPassThroughString(_pwchSequenceStart, + _pwchCurr-_pwchSequenceStart+1); +} + +// 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 +// a escape sequence, then feed characters into the state machine one at a +// time until we return to the ground state. +// Arguments: +// - rgwch - Array of new characters to operate upon +// - cch - Count of characters in array +// Return Value: +// - +void StateMachine::ProcessString(const wchar_t* const rgwch, const size_t cch) +{ + _pwchCurr = rgwch; + _pwchSequenceStart = rgwch; + _currRunLength = 0; + + // This should be static, because if one string starts a sequence, and the next finishes it, + // we want the partial sequence state to persist. + static bool s_fProcessIndividually = false; + + for(size_t cchCharsRemaining = cch; cchCharsRemaining > 0; cchCharsRemaining--) + { + if (s_fProcessIndividually) + { + // If we're processing characters individually, send it to the state machine. + ProcessCharacter(*_pwchCurr); + _pwchCurr++; + 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. + s_fProcessIndividually = false; + _pwchSequenceStart = _pwchCurr; + _currRunLength = 0; + } + } + else + { + if (s_IsActionableFromGround(*_pwchCurr)) // If the current char is the start of an escape sequence, or should be executed in ground state... + { + FAIL_FAST_IF(!(_pwchSequenceStart + _currRunLength <= rgwch + cch)); + _pEngine->ActionPrintString(_pwchSequenceStart, _currRunLength); // ... print all the chars leading up to it as part of the run... + _trace.DispatchPrintRunTrace(_pwchSequenceStart, _currRunLength); + s_fProcessIndividually = true; // begin processing future characters individually... + _currRunLength = 0; + _pwchSequenceStart = _pwchCurr; + ProcessCharacter(*_pwchCurr); // ... Then process the character individually. + if (_state == VTStates::Ground) // If the character took us right back to ground, start another run after it. + { + s_fProcessIndividually = false; + _pwchSequenceStart = _pwchCurr + 1; + _currRunLength = 0; + } + } + else + { + _currRunLength++; // Otherwise, add this char to the current run to be printed. + } + _pwchCurr++; + } + } + + // If we're at the end of the string and have remaining un-printed characters, + if (!s_fProcessIndividually && _currRunLength > 0) + { + // print the rest of the characters in the string + _pEngine->ActionPrintString(_pwchSequenceStart, _currRunLength); + _trace.DispatchPrintRunTrace(_pwchSequenceStart, _currRunLength); + + } + else if (s_fProcessIndividually) + { + if (_pEngine->FlushAtEndOfString()) + { + // Reset our state, and put all but the last char in again. + ResetState(); + // Chars to flush are [pwchSequenceStart, pwchCurr) + const wchar_t* pwch = _pwchSequenceStart; + for (; pwch < _pwchCurr-1; pwch++) + { + ProcessCharacter(*pwch); + } + // Manually execute the last char [pwchCurr] + switch (_state) + { + case VTStates::Ground: + return _ActionExecute(*pwch); + case VTStates::Escape: + case VTStates::EscapeIntermediate: + return _ActionEscDispatch(*pwch); + case VTStates::CsiEntry: + case VTStates::CsiIntermediate: + case VTStates::CsiIgnore: + case VTStates::CsiParam: + return _ActionCsiDispatch(*pwch); + case VTStates::OscParam: + case VTStates::OscString: + case VTStates::OscTermination: + return _ActionOscDispatch(*pwch); + case VTStates::Ss3Entry: + case VTStates::Ss3Param: + return _ActionSs3Dispatch(*pwch); + default: + return; + } + + } + } +} + +void StateMachine::ProcessString(const std::wstring& wstr) +{ + return ProcessString(wstr.c_str(), wstr.length()); +} + +// Routine Description: +// - Wherever the state machine is, whatever it's going, go back to ground. +// This is used by conhost to "jiggle the handle" - when VT support is +// turned off, we don't want any bad state left over for the next input it's turned on for +// Arguments: +// - +// Return Value: +// - +void StateMachine::ResetState() +{ + _EnterGround(); +} diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp new file mode 100644 index 000000000..5133a30b1 --- /dev/null +++ b/src/terminal/parser/stateMachine.hpp @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- stateMachine.hpp + +Abstract: +- This declares the entire state machine for handling Virtual Terminal Sequences +- The design is based from the specifications at http://vt100.net +- The actual implementation of actions decoded by the StateMachine should be + implemented in an IStateMachineEngine. +*/ + +#pragma once + +#include "IStateMachineEngine.hpp" +#include "telemetry.hpp" +#include "tracing.hpp" +#include + +namespace Microsoft::Console::VirtualTerminal +{ + class StateMachine final + { +#ifdef UNIT_TESTING + friend class OutputEngineTest; + friend class InputEngineTest; +#endif + + public: + StateMachine(IStateMachineEngine* const pEngine); + + void ProcessCharacter(const wchar_t wch); + void ProcessString(const wchar_t* const rgwch, const size_t cch); + void ProcessString(const std::wstring& wstr); + + void ResetState(); + + bool FlushToTerminal(); + + const IStateMachineEngine& Engine() const noexcept; + IStateMachineEngine& Engine() noexcept; + + static const short s_cIntermediateMax = 1; + static const short s_cParamsMax = 16; + static const short s_cOscStringMaxLength = 256; + + private: + static bool s_IsActionableFromGround(const wchar_t wch); + static bool s_IsC0Code(const wchar_t wch); + static bool s_IsC1Csi(const wchar_t wch); + static bool s_IsIntermediate(const wchar_t wch); + static bool s_IsDelete(const wchar_t wch); + static bool s_IsEscape(const wchar_t wch); + static bool s_IsCsiIndicator(const wchar_t wch); + static bool s_IsCsiDelimiter(const wchar_t wch); + static bool s_IsCsiParamValue(const wchar_t wch); + static bool s_IsCsiPrivateMarker(const wchar_t wch); + static bool s_IsCsiInvalid(const wchar_t wch); + static bool s_IsOscIndicator(const wchar_t wch); + static bool s_IsOscDelimiter(const wchar_t wch); + static bool s_IsOscParamValue(const wchar_t wch); + static bool s_IsOscInvalid(const wchar_t wch); + static bool s_IsOscTerminator(const wchar_t wch); + static bool s_IsOscTerminationInitiator(const wchar_t wch); + static bool s_IsDesignateCharsetIndicator(const wchar_t wch); + static bool s_IsCharsetCode(const wchar_t wch); + static bool s_IsNumber(const wchar_t wch); + static bool s_IsSs3Indicator(const wchar_t wch); + + void _ActionExecute(const wchar_t wch); + void _ActionExecuteFromEscape(const wchar_t wch); + void _ActionPrint(const wchar_t wch); + void _ActionEscDispatch(const wchar_t wch); + void _ActionCollect(const wchar_t wch); + void _ActionParam(const wchar_t wch); + void _ActionCsiDispatch(const wchar_t wch); + void _ActionOscParam(const wchar_t wch); + void _ActionOscPut(const wchar_t wch); + void _ActionOscDispatch(const wchar_t wch); + void _ActionSs3Dispatch(const wchar_t wch); + + void _ActionClear(); + void _ActionIgnore(); + + void _EnterGround(); + void _EnterEscape(); + void _EnterEscapeIntermediate(); + void _EnterCsiEntry(); + void _EnterCsiParam(); + void _EnterCsiIgnore(); + void _EnterCsiIntermediate(); + void _EnterOscParam(); + void _EnterOscString(); + void _EnterOscTermination(); + void _EnterSs3Entry(); + void _EnterSs3Param(); + + void _EventGround(const wchar_t wch); + void _EventEscape(const wchar_t wch); + void _EventEscapeIntermediate(const wchar_t wch); + void _EventCsiEntry(const wchar_t wch); + void _EventCsiIntermediate(const wchar_t wch); + void _EventCsiIgnore(const wchar_t wch); + void _EventCsiParam(const wchar_t wch); + void _EventOscParam(const wchar_t wch); + void _EventOscString(const wchar_t wch); + void _EventOscTermination(const wchar_t wch); + void _EventSs3Entry(const wchar_t wch); + void _EventSs3Param(const wchar_t wch); + + enum class VTStates + { + Ground, + Escape, + EscapeIntermediate, + CsiEntry, + CsiIntermediate, + CsiIgnore, + CsiParam, + OscParam, + OscString, + OscTermination, + Ss3Entry, + Ss3Param + }; + + Microsoft::Console::VirtualTerminal::ParserTracing _trace; + + std::unique_ptr _pEngine; + + VTStates _state; + + wchar_t _wchIntermediate; + unsigned short _cIntermediate; + + unsigned short _rgusParams[s_cParamsMax]; + unsigned short _cParams; + unsigned short* _pusActiveParam; + unsigned short _iParamAccumulatePos; + + unsigned short _sOscParam; + unsigned short _sOscNextChar; + wchar_t _pwchOscStringBuffer[s_cOscStringMaxLength]; + + // These members track out state in the parsing of a single string. + // FlushToTerminal uses these, so that an engine can force a string + // we're parsing to go straight through to the engine's ActionPassThroughString + const wchar_t* _pwchCurr; + const wchar_t* _pwchSequenceStart; + size_t _currRunLength; + + }; +} diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp new file mode 100644 index 000000000..2d227514e --- /dev/null +++ b/src/terminal/parser/telemetry.cpp @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include "telemetry.hpp" + +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() + : _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() +{ + WriteFinalTraceLog(); + TraceLoggingUnregister(g_hConsoleVirtTermParserEventTraceProvider); +} + +// Routine Description: +// - Logs the usage of a particular VT100 code. +// +// Arguments: +// - code - VT100 code. +// Return Value: +// - +void TermTelemetry::Log(const Codes code) +{ + // 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) +{ + 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() +{ + unsigned int uiTemp = _uiTimesUsedCurrent; + _uiTimesUsedCurrent = 0; + return uiTemp; +} + +// Routine Description: +// - Gets and resets the total count of codes failed. +// +// Arguments: +// - +// Return Value: +// - total number. +unsigned int TermTelemetry::GetAndResetTimesFailedCurrent() +{ + unsigned int uiTemp = _uiTimesFailedCurrent; + _uiTimesFailedCurrent = 0; + return uiTemp; +} + +// Routine Description: +// - Gets and resets the total count of codes failed outside the valid range. +// +// Arguments: +// - +// Return Value: +// - total number. +unsigned int TermTelemetry::GetAndResetTimesFailedOutsideRangeCurrent() +{ + unsigned int uiTemp = _uiTimesFailedOutsideRangeCurrent; + _uiTimesFailedOutsideRangeCurrent = 0; + return uiTemp; +} + +// 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) +{ + _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) +{ + _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. + bool fLoggedSequence = (_uiTimesFailedOutsideRange > 0); + + if (!fLoggedSequence) + { + for (int n = 0; n < ARRAYSIZE(_uiTimesUsed); n++) + { + if (_uiTimesUsed[n] > 0) + { + fLoggedSequence = true; + break; + } + } + } + + if (!fLoggedSequence) + { + for (int 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[EL], "EL"), + TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"), + TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"), + TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"), + TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"), + TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"), + TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"), + TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"), + TraceLoggingUInt32(_uiTimesUsed[DSR], "DSR"), + TraceLoggingUInt32(_uiTimesUsed[DA], "DA"), + TraceLoggingUInt32(_uiTimesUsed[VPA], "VPA"), + 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[ANSISYSSC], "ANSISYSSC"), + TraceLoggingUInt32(_uiTimesUsed[ANSISYSRC], "ANSISYSRC"), + TraceLoggingUInt32(_uiTimesUsed[DECSTBM], "DECSTBM"), + 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[HVP], "HVP"), + TraceLoggingUInt32(_uiTimesUsed[DECSTR], "DECSTR"), + TraceLoggingUInt32(_uiTimesUsed[RIS], "RIS"), + TraceLoggingUInt32(_uiTimesUsed[DECSCUSR], "DECSCUSR"), + TraceLoggingUInt32(_uiTimesUsed[DTTERM_WM], "DTTERM_WM"), + TraceLoggingUInt32(_uiTimesUsed[OSCCT], "OscColorTable"), + TraceLoggingUInt32(_uiTimesUsed[OSCSCC], "OscSetCursorColor"), + TraceLoggingUInt32(_uiTimesUsed[OSCRCC], "OscResetCursorColor"), + TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), + TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"), + TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange")); + } + } +} diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp new file mode 100644 index 000000000..e73d75de1 --- /dev/null +++ b/src/terminal/parser/telemetry.hpp @@ -0,0 +1,116 @@ +// 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 "limits.h" + +TRACELOGGING_DECLARE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider); + +namespace Microsoft::Console::VirtualTerminal +{ + class TermTelemetry sealed + { + + public: + // Implement this as a singleton class. + static TermTelemetry& Instance() + { + 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, + EL, + SGR, + DECSC, + DECRC, + DECSET, + DECRST, + DECKPAM, + DECKPNM, + DSR, + DA, + VPA, + ICH, + DCH, + SU, + SD, + ANSISYSSC, + ANSISYSRC, + IL, + DL, + DECSTBM, + RI, + OSCWT, + HTS, + CHT, + CBT, + TBC, + ECH, + DesignateG0, + DesignateG1, + DesignateG2, + DesignateG3, + HVP, + DECSTR, + RIS, + DECSCUSR, + DTTERM_WM, + OSCCT, + OSCSCC, + OSCRCC, + REP, + // Only use this last enum as a count of the number of codes. + NUMBER_OF_CODES + }; + void Log(const Codes code); + void LogFailed(const wchar_t wch); + void SetShouldWriteFinalLog(const bool writeLog); + void SetActivityId(const GUID *activityId); + unsigned int GetAndResetTimesUsedCurrent(); + unsigned int GetAndResetTimesFailedCurrent(); + unsigned int GetAndResetTimesFailedOutsideRangeCurrent(); + + private: + // Used to prevent multiple instances + TermTelemetry(); + ~TermTelemetry(); + TermTelemetry(TermTelemetry const&); + void operator=(TermTelemetry const&); + + 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 new file mode 100644 index 000000000..878ad7bad --- /dev/null +++ b/src/terminal/parser/tracing.cpp @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "tracing.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +ParserTracing::ParserTracing() +{ + ClearSequenceTrace(); +} + +ParserTracing::~ParserTracing() +{ + +} + +void ParserTracing::TraceStateChange(_In_ PCWSTR const pwszName) const +{ + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_EnterState", + TraceLoggingWideString(pwszName), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::TraceOnAction(_In_ PCWSTR const pwszName) const +{ + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_Action", + TraceLoggingWideString(pwszName), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::TraceOnExecute(const wchar_t wch) const +{ + INT16 sch = (INT16)wch; + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_Execute", + TraceLoggingWChar(wch), + TraceLoggingHexInt16(sch), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::TraceOnExecuteFromEscape(const wchar_t wch) const +{ + INT16 sch = (INT16)wch; + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_ExecuteFromEscape", + TraceLoggingWChar(wch), + TraceLoggingHexInt16(sch), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::TraceOnEvent(_In_ PCWSTR const pwszName) const +{ + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_Event", + TraceLoggingWideString(pwszName), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::TraceCharInput(const wchar_t wch) +{ + AddSequenceTrace(wch); + INT16 sch = (INT16)wch; + + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_NewChar", + TraceLoggingWChar(wch), + TraceLoggingHexInt16(sch), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); +} + +void ParserTracing::AddSequenceTrace(const wchar_t wch) +{ + // -1 to always leave the last character as null/0. + if (_cchSequenceTrace < s_cMaxSequenceTrace - 1) + { + _rgwchSequenceTrace[_cchSequenceTrace] = wch; + _cchSequenceTrace++; + } +} + +void ParserTracing::DispatchSequenceTrace(const bool fSuccess) +{ + if (fSuccess) + { + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_Sequence_OK", + TraceLoggingWideString(_rgwchSequenceTrace), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); + } + else + { + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_Sequence_FAIL", + TraceLoggingWideString(_rgwchSequenceTrace), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); + } + + ClearSequenceTrace(); +} + +void ParserTracing::ClearSequenceTrace() +{ + ZeroMemory(_rgwchSequenceTrace, sizeof(_rgwchSequenceTrace)); + _cchSequenceTrace = 0; +} + +// NOTE: I'm expecting this to not be null terminated +void ParserTracing::DispatchPrintRunTrace(const wchar_t* const pwsString, const size_t cchString) const +{ + size_t charsRemaining = cchString; + wchar_t str[BYTE_MAX + 4 + sizeof(wchar_t) + sizeof('\0')]; + + if (cchString == 1) + { + wchar_t wch = *pwsString; + INT16 sch = (INT16)wch; + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_PrintRun", + TraceLoggingWChar(wch), + TraceLoggingHexInt16(sch), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); + } + else + { + while (charsRemaining > 0) + { + size_t strLen = 0; + if (charsRemaining > ARRAYSIZE(str) - 1) + { + strLen = ARRAYSIZE(str) - 1; + } + else + { + strLen = charsRemaining; + } + charsRemaining -= strLen; + + memcpy(str, pwsString, sizeof(wchar_t) * strLen); + str[strLen] = '\0'; + + TraceLoggingWrite(g_hConsoleVirtTermParserEventTraceProvider, "StateMachine_PrintRun", + TraceLoggingWideString(str), + TraceLoggingValue(strLen), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE) + ); + + } + } +} diff --git a/src/terminal/parser/tracing.hpp b/src/terminal/parser/tracing.hpp new file mode 100644 index 000000000..b871d41dc --- /dev/null +++ b/src/terminal/parser/tracing.hpp @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/* +Module Name: +- tracing.hpp + +Abstract: +- This module is used for recording tracing/debugging information to the telemetry ETW channel +- The data is not automatically broadcast to telemetry backends. +- NOTE: Many functions in this file appear to be copy/pastes. This is because the TraceLog documentation warns + to not be "cute" in trying to reduce its macro usages with variables as it can cause unexpected behavior. +*/ + +#pragma once + +#include "telemetry.hpp" + +namespace Microsoft::Console::VirtualTerminal +{ + class ParserTracing sealed + { + public: + + ParserTracing(); + ~ParserTracing(); + + void TraceStateChange(_In_ PCWSTR const pwszName) const; + void TraceOnAction(_In_ PCWSTR const pwszName) const; + void TraceOnExecute(const wchar_t wch) const; + void TraceOnExecuteFromEscape(const wchar_t wch) const; + void TraceOnEvent(_In_ PCWSTR const pwszName) const; + void TraceCharInput(const wchar_t wch); + + void AddSequenceTrace(const wchar_t wch); + void DispatchSequenceTrace(const bool fSuccess); + void ClearSequenceTrace(); + void DispatchPrintRunTrace(const wchar_t* const pwsString, const size_t cchString) const; + + private: + static const size_t s_cMaxSequenceTrace = 32; + + wchar_t _rgwchSequenceTrace[s_cMaxSequenceTrace]; + size_t _cchSequenceTrace; + + + }; +} diff --git a/src/terminal/parser/ut_parser/InputEngineTest.cpp b/src/terminal/parser/ut_parser/InputEngineTest.cpp new file mode 100644 index 000000000..9e7de77a0 --- /dev/null +++ b/src/terminal/parser/ut_parser/InputEngineTest.cpp @@ -0,0 +1,790 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "WexTestClass.h" +#include "../../inc/consoletaeftemplates.hpp" + +#include "stateMachine.hpp" +#include "InputStateMachineEngine.hpp" +#include "../input/terminalInput.hpp" +#include "../../inc/unicode.hpp" +#include "../../types/inc/convert.hpp" + +#include +#include +#include +#include +#include + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "../../../interactivity/inc/VtApiRedirection.hpp" +#endif + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class InputEngineTest; + class TestInteractDispatch; + }; + }; +}; +using namespace Microsoft::Console::VirtualTerminal; + +bool IsShiftPressed(const DWORD modifierState) +{ + return WI_IsFlagSet(modifierState, SHIFT_PRESSED); +} + +bool IsAltPressed(const DWORD modifierState) +{ + return WI_IsAnyFlagSet(modifierState, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); +} + +bool IsCtrlPressed(const DWORD modifierState) +{ + return WI_IsAnyFlagSet(modifierState, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); +} + +bool ModifiersEquivalent(DWORD a, DWORD b) +{ + bool fShift = IsShiftPressed(a) == IsShiftPressed(b); + bool fAlt = IsAltPressed(a) == IsAltPressed(b); + bool fCtrl = IsCtrlPressed(a) == IsCtrlPressed(b); + return fShift && fCtrl && fAlt; +} + +class TestState +{ +public: + TestState() : + vExpectedInput{}, + _stateMachine{ nullptr }, + _expectedToCallWindowManipulation{ false }, + _expectSendCtrlC{ false }, + _expectCursorPosition{ false }, + _expectedCursor{ -1, -1 }, + _expectedWindowManipulation{ DispatchTypes::WindowManipulationType::Invalid }, + _expectedCParams{ 0 } + { + std::fill_n(_expectedParams, ARRAYSIZE(_expectedParams), gsl::narrow(0)); + } + + void RoundtripTerminalInputCallback(_In_ std::deque>& inEvents) + { + // Take all the characters out of the input records here, and put them into + // the input state machine. + auto inputRecords = IInputEvent::ToInputRecords(inEvents); + std::wstring vtseq = L""; + for (auto& inRec : inputRecords) + { + VERIFY_ARE_EQUAL(KEY_EVENT, inRec.EventType); + if (inRec.Event.KeyEvent.bKeyDown) + { + vtseq += &inRec.Event.KeyEvent.uChar.UnicodeChar; + } + } + Log::Comment( + NoThrowString().Format(L"\tvtseq: \"%s\"(%zu)", vtseq.c_str(), vtseq.length()) + ); + + _stateMachine->ProcessString(&vtseq[0], vtseq.length()); + Log::Comment(L"String processed"); + } + + void TestInputCallback(std::deque>& inEvents) + { + auto records = IInputEvent::ToInputRecords(inEvents); + VERIFY_ARE_EQUAL((size_t)1, vExpectedInput.size()); + + bool foundEqual = false; + INPUT_RECORD irExpected = vExpectedInput.back(); + + Log::Comment( + NoThrowString().Format(L"\texpected:\t") + + VerifyOutputTraits::ToString(irExpected) + ); + + // Look for an equivalent input record. + // Differences between left and right modifiers are ignored, as long as one is pressed. + // There may be other keypresses, eg. modifier keypresses, those are ignored. + for (auto& inRec : records) + { + Log::Comment( + NoThrowString().Format(L"\tActual :\t") + + VerifyOutputTraits::ToString(inRec) + ); + + bool areEqual = + (irExpected.EventType == inRec.EventType) && + (irExpected.Event.KeyEvent.bKeyDown == inRec.Event.KeyEvent.bKeyDown) && + (irExpected.Event.KeyEvent.wRepeatCount == inRec.Event.KeyEvent.wRepeatCount) && + (irExpected.Event.KeyEvent.uChar.UnicodeChar == inRec.Event.KeyEvent.uChar.UnicodeChar) && + ModifiersEquivalent(irExpected.Event.KeyEvent.dwControlKeyState, inRec.Event.KeyEvent.dwControlKeyState); + + foundEqual |= areEqual; + if (areEqual) + { + Log::Comment(L"\t\tFound Match"); + } + } + + VERIFY_IS_TRUE(foundEqual); + vExpectedInput.clear(); + } + + void TestInputStringCallback(std::deque>& inEvents) + { + auto records = IInputEvent::ToInputRecords(inEvents); + + for (auto expected : vExpectedInput) + { + Log::Comment( + NoThrowString().Format(L"\texpected:\t") + + VerifyOutputTraits::ToString(expected) + ); + + } + + INPUT_RECORD irExpected = vExpectedInput.front(); + Log::Comment( + NoThrowString().Format(L"\tLooking for:\t") + + VerifyOutputTraits::ToString(irExpected) + ); + + + // Look for an equivalent input record. + // Differences between left and right modifiers are ignored, as long as one is pressed. + // There may be other keypresses, eg. modifier keypresses, those are ignored. + for (auto& inRec : records) + { + Log::Comment( + NoThrowString().Format(L"\tActual :\t") + + VerifyOutputTraits::ToString(inRec) + ); + + bool areEqual = + (irExpected.EventType == inRec.EventType) && + (irExpected.Event.KeyEvent.bKeyDown == inRec.Event.KeyEvent.bKeyDown) && + (irExpected.Event.KeyEvent.wRepeatCount == inRec.Event.KeyEvent.wRepeatCount) && + (irExpected.Event.KeyEvent.uChar.UnicodeChar == inRec.Event.KeyEvent.uChar.UnicodeChar) && + ModifiersEquivalent(irExpected.Event.KeyEvent.dwControlKeyState, inRec.Event.KeyEvent.dwControlKeyState); + + if (areEqual) + { + Log::Comment(L"\t\tFound Match"); + vExpectedInput.pop_front(); + if (vExpectedInput.size() > 0) + { + irExpected = vExpectedInput.front(); + Log::Comment( + NoThrowString().Format(L"\tLooking for:\t") + + VerifyOutputTraits::ToString(irExpected) + ); + } + } + } + VERIFY_ARE_EQUAL(static_cast(0), vExpectedInput.size(), L"Verify we found all the inputs we were expecting"); + vExpectedInput.clear(); + } + + std::deque vExpectedInput; + StateMachine* _stateMachine; + bool _expectedToCallWindowManipulation; + bool _expectSendCtrlC; + bool _expectCursorPosition; + COORD _expectedCursor; + DispatchTypes::WindowManipulationType _expectedWindowManipulation; + unsigned short _expectedParams[16]; + size_t _expectedCParams; +}; + +class Microsoft::Console::VirtualTerminal::InputEngineTest +{ + TEST_CLASS(InputEngineTest); + + void RoundtripTerminalInputCallback(std::deque>& inEvents); + void TestInputCallback(std::deque>& inEvents); + void TestInputStringCallback(std::deque>& inEvents); + + + TEST_CLASS_SETUP(ClassSetup) + { + return true; + } + + TEST_CLASS_CLEANUP(ClassCleanup) + { + return true; + } + + TEST_METHOD_SETUP(MethodSetup) + { + return true; + } + + TEST_METHOD(C0Test); + TEST_METHOD(AlphanumericTest); + TEST_METHOD(RoundTripTest); + TEST_METHOD(WindowManipulationTest); + TEST_METHOD(NonAsciiTest); + TEST_METHOD(CursorPositioningTest); + TEST_METHOD(CSICursorBackTabTest); + TEST_METHOD(AltBackspaceTest); + TEST_METHOD(AltCtrlDTest); + + friend class TestInteractDispatch; +}; + + +class Microsoft::Console::VirtualTerminal::TestInteractDispatch final : public IInteractDispatch +{ +public: + TestInteractDispatch(_In_ std::function>&)> pfn, + _In_ TestState* testState); + virtual bool WriteInput(_In_ std::deque>& inputEvents) override; + virtual bool WriteCtrlC() override; + virtual bool WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) override; // DTTERM_WindowManipulation + virtual bool WriteString(_In_reads_(cch) const wchar_t* const pws, + const size_t cch) override; + + virtual bool MoveCursor(const unsigned int row, + const unsigned int col) override; + +private: + std::function>&)> _pfnWriteInputCallback; + TestState* _testState; // non-ownership pointer +}; + +TestInteractDispatch::TestInteractDispatch(_In_ std::function>&)> pfn, + _In_ TestState* testState) : + _pfnWriteInputCallback(pfn), + _testState(testState) +{ + +} + +bool TestInteractDispatch::WriteInput(_In_ std::deque>& inputEvents) +{ + _pfnWriteInputCallback(inputEvents); + return true; +} + +bool TestInteractDispatch::WriteCtrlC() +{ + VERIFY_IS_TRUE(_testState->_expectSendCtrlC); + KeyEvent key = KeyEvent(true, 1, 'C', 0, UNICODE_ETX, LEFT_CTRL_PRESSED); + std::deque> inputEvents; + inputEvents.push_back(std::make_unique(key)); + return WriteInput(inputEvents); +} + +bool TestInteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulationType uiFunction, + _In_reads_(cParams) const unsigned short* const rgusParams, + const size_t cParams) +{ + + VERIFY_ARE_EQUAL(true, _testState->_expectedToCallWindowManipulation); + VERIFY_ARE_EQUAL(_testState->_expectedWindowManipulation, uiFunction); + for(size_t i = 0; i < cParams; i++) + { + VERIFY_ARE_EQUAL(_testState->_expectedParams[i], rgusParams[i]); + } + return true; +} + +bool TestInteractDispatch::WriteString(_In_reads_(cch) const wchar_t* const pws, + const size_t cch) +{ + std::deque> keyEvents; + + for (size_t i = 0; i < cch; ++i) + { + const wchar_t wch = pws[i]; + // We're forcing the translation to CP_USA, so that it'll be constant + // regardless of the CP the test is running in + std::deque> convertedEvents = CharToKeyEvents(wch, CP_USA); + std::move(convertedEvents.begin(), + convertedEvents.end(), + std::back_inserter(keyEvents)); + } + + return WriteInput(keyEvents); +} + +bool TestInteractDispatch::MoveCursor(const unsigned int row, + const unsigned int col) +{ + VERIFY_IS_TRUE(_testState->_expectCursorPosition); + COORD received = { static_cast(col), static_cast(row) }; + VERIFY_ARE_EQUAL(_testState->_expectedCursor, received); + return true; +} + +void InputEngineTest::C0Test() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + Log::Comment(L"Sending 0x0-0x19 to parser to make sure they're translated correctly back to C-key"); + DisableVerifyExceptions disable; + for (wchar_t wch = '\x0'; wch < '\x20'; wch++) + { + std::wstring inputSeq = std::wstring(&wch, 1); + // In general, he actual key that we're going to generate for a C0 char + // is char+0x40 and with ctrl pressed. + wchar_t expectedWch = wch + 0x40; + bool writeCtrl = true; + // These two are weird exceptional cases. + switch(wch) + { + case L'\r': // Enter + expectedWch = wch; + writeCtrl = false; + break; + case L'\x1b': // Escape + expectedWch = wch; + writeCtrl = false; + break; + case L'\t': // Tab + writeCtrl = false; + break; + } + + short keyscan = VkKeyScanW(expectedWch); + short vkey = keyscan & 0xff; + short keyscanModifiers = (keyscan >> 8) & 0xff; + WORD scanCode = (WORD)MapVirtualKeyW(vkey, MAPVK_VK_TO_VSC); + + DWORD dwModifierState = 0; + if (writeCtrl) + { + dwModifierState = WI_SetFlag(dwModifierState, LEFT_CTRL_PRESSED); + } + // If we need to press shift for this key, but not on alphabetical chars + // Eg simulating C-z, not C-S-z. + if (WI_IsFlagSet(keyscanModifiers, 1) && (expectedWch < L'A' || expectedWch > L'Z' )) + { + dwModifierState = WI_SetFlag(dwModifierState, SHIFT_PRESSED); + } + + // Just make sure we write the same thing telnetd did: + if (wch == UNICODE_ETX) + { + Log::Comment(NoThrowString().Format( + L"We used to expect 0x%x, 0x%x, 0x%x, 0x%x here", + vkey, scanCode, wch, dwModifierState + )); + vkey = 'C'; + scanCode = 0; + wch = UNICODE_ETX; + dwModifierState = LEFT_CTRL_PRESSED; + Log::Comment(NoThrowString().Format( + L"Now we expect 0x%x, 0x%x, 0x%x, 0x%x here", + vkey, scanCode, wch, dwModifierState + )); + testState._expectSendCtrlC = true; + } + else + { + testState._expectSendCtrlC = false; + } + + Log::Comment(NoThrowString().Format(L"Testing char 0x%x", wch)); + Log::Comment(NoThrowString().Format(L"Input Sequence=\"%s\"", inputSeq.c_str())); + + INPUT_RECORD inputRec; + + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = dwModifierState; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = vkey; + inputRec.Event.KeyEvent.wVirtualScanCode = scanCode; + inputRec.Event.KeyEvent.uChar.UnicodeChar = wch; + + testState.vExpectedInput.push_back(inputRec); + + _stateMachine->ProcessString(&inputSeq[0], inputSeq.length()); + + } +} + +void InputEngineTest::AlphanumericTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + Log::Comment(L"Sending every printable ASCII character"); + DisableVerifyExceptions disable; + for (wchar_t wch = '\x20'; wch < '\x7f'; wch++) + { + std::wstring inputSeq = std::wstring(&wch, 1); + + short keyscan = VkKeyScanW(wch); + short vkey = keyscan & 0xff; + WORD scanCode = (wchar_t)MapVirtualKeyW(vkey, MAPVK_VK_TO_VSC); + + short keyscanModifiers = (keyscan >> 8) & 0xff; + // Because of course, these are not the same flags. + DWORD dwModifierState = 0 | + (WI_IsFlagSet(keyscanModifiers, 1) ? SHIFT_PRESSED : 0) | + (WI_IsFlagSet(keyscanModifiers, 2) ? LEFT_CTRL_PRESSED : 0) | + (WI_IsFlagSet(keyscanModifiers, 4) ? LEFT_ALT_PRESSED : 0) ; + + Log::Comment(NoThrowString().Format(L"Testing char 0x%x", wch)); + Log::Comment(NoThrowString().Format(L"Input Sequence=\"%s\"", inputSeq.c_str())); + + INPUT_RECORD inputRec; + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = dwModifierState; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = vkey; + inputRec.Event.KeyEvent.wVirtualScanCode = scanCode; + inputRec.Event.KeyEvent.uChar.UnicodeChar = wch; + + testState.vExpectedInput.push_back(inputRec); + + _stateMachine->ProcessString(&inputSeq[0], inputSeq.length()); + } + +} + +void InputEngineTest::RoundTripTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + // Send Every VKEY through the TerminalInput module, then take the char's + // from the generated INPUT_RECORDs and put them through the InputEngine. + // The VKEY sequence it writes out should be the same as the original. + + auto pfn2 = std::bind(&TestState::RoundtripTerminalInputCallback, &testState, std::placeholders::_1); + TerminalInput terminalInput{ pfn2 }; + + for (BYTE vkey = 0; vkey < BYTE_MAX; vkey++) + { + wchar_t wch = (wchar_t)MapVirtualKeyW(vkey, MAPVK_VK_TO_CHAR); + WORD scanCode = (wchar_t)MapVirtualKeyW(vkey, MAPVK_VK_TO_VSC); + + unsigned int uiActualKeystate = 0; + + // Couple of exceptional cases here: + if (vkey >= 'A' && vkey <= 'Z') + { + // A-Z need shift pressed in addition to the 'a'-'z' chars. + uiActualKeystate = WI_SetFlag(uiActualKeystate, SHIFT_PRESSED); + } + else if (vkey == VK_CANCEL || vkey == VK_PAUSE) + { + uiActualKeystate = WI_SetFlag(uiActualKeystate, LEFT_CTRL_PRESSED); + } + + if (vkey == UNICODE_ETX) + { + testState._expectSendCtrlC = true; + } + + INPUT_RECORD irTest = { 0 }; + irTest.EventType = KEY_EVENT; + irTest.Event.KeyEvent.dwControlKeyState = uiActualKeystate; + irTest.Event.KeyEvent.wRepeatCount = 1; + irTest.Event.KeyEvent.wVirtualKeyCode = vkey; + irTest.Event.KeyEvent.bKeyDown = TRUE; + irTest.Event.KeyEvent.uChar.UnicodeChar = wch; + irTest.Event.KeyEvent.wVirtualScanCode = scanCode; + + Log::Comment( + NoThrowString().Format(L"Expecting:: ") + + VerifyOutputTraits::ToString(irTest) + ); + + testState.vExpectedInput.clear(); + testState.vExpectedInput.push_back(irTest); + + auto inputKey = IInputEvent::Create(irTest); + terminalInput.HandleKey(inputKey.get()); + } + +} + +void InputEngineTest::WindowManipulationTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine.get()); + testState._stateMachine = _stateMachine.get(); + + Log::Comment(NoThrowString().Format( + L"Try sending a bunch of Window Manipulation sequences. " + L"Only the valid ones should call the " + L"TestInteractDispatch::WindowManipulation callback." + )); + + bool fValidType = false; + + const unsigned short param1 = 123; + const unsigned short param2 = 456; + const wchar_t* const wszParam1 = L"123"; + const wchar_t* const wszParam2 = L"456"; + + for(unsigned int i = 0; i < static_cast(BYTE_MAX); i++) + { + if (i == DispatchTypes::WindowManipulationType::ResizeWindowInCharacters) + { + fValidType = true; + } + + std::wstringstream seqBuilder; + seqBuilder << L"\x1b[" << i; + + + if (i == DispatchTypes::WindowManipulationType::ResizeWindowInCharacters) + { + // We need to build the string with the params as strings for some reason - + // x86 would implicitly convert them to chars (eg 123 -> '{') + // before appending them to the string + seqBuilder << L";" << wszParam1 << L";" << wszParam2; + + testState._expectedToCallWindowManipulation = true; + testState._expectedCParams = 2; + testState._expectedParams[0] = param1; + testState._expectedParams[1] = param2; + testState._expectedWindowManipulation = static_cast(i); + } + else if (i == DispatchTypes::WindowManipulationType::RefreshWindow) + { + // refresh window doesn't expect any params. + + testState._expectedToCallWindowManipulation = true; + testState._expectedCParams = 0; + testState._expectedWindowManipulation = static_cast(i); + } + else + { + testState._expectedToCallWindowManipulation = false; + testState._expectedCParams = 0; + testState._expectedWindowManipulation = DispatchTypes::WindowManipulationType::Invalid; + } + seqBuilder << L"t"; + std::wstring seq = seqBuilder.str(); + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", seq.c_str() + )); + _stateMachine->ProcessString(&seq[0], seq.length()); + } +} + +void InputEngineTest::NonAsciiTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputStringCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine.get()); + testState._stateMachine = _stateMachine.get(); + Log::Comment(L"Sending various non-ascii strings, and seeing what we get out"); + + INPUT_RECORD proto = {0}; + proto.EventType = KEY_EVENT; + proto.Event.KeyEvent.dwControlKeyState = 0; + proto.Event.KeyEvent.wRepeatCount = 1; + proto.Event.KeyEvent.wVirtualKeyCode = 0; + proto.Event.KeyEvent.wVirtualScanCode = 0; + // Fill these in for each char + proto.Event.KeyEvent.bKeyDown = TRUE; + proto.Event.KeyEvent.uChar.UnicodeChar = UNICODE_NULL; + + Log::Comment(NoThrowString().Format( + L"We're sending utf-16 characters here, because the VtInputThread has " + L"already converted the ut8 input to utf16 by the time it calls the state machine." + )); + + // "Л", UTF-16: 0x041B, utf8: "\xd09b" + std::wstring utf8Input = L"\x041B"; + INPUT_RECORD test = proto; + test.Event.KeyEvent.uChar.UnicodeChar = utf8Input[0]; + + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", utf8Input.c_str() + )); + + testState.vExpectedInput.clear(); + testState.vExpectedInput.push_back(test); + test.Event.KeyEvent.bKeyDown = FALSE; + testState.vExpectedInput.push_back(test); + _stateMachine->ProcessString(&utf8Input[0], utf8Input.length()); + + // "旅", UTF-16: 0x65C5, utf8: "0xE6 0x97 0x85" + utf8Input = L"\u65C5"; + test = proto; + test.Event.KeyEvent.uChar.UnicodeChar = utf8Input[0]; + + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", utf8Input.c_str() + )); + + testState.vExpectedInput.clear(); + testState.vExpectedInput.push_back(test); + test.Event.KeyEvent.bKeyDown = FALSE; + testState.vExpectedInput.push_back(test); + _stateMachine->ProcessString(&utf8Input[0], utf8Input.length()); +} + +void InputEngineTest::CursorPositioningTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto dispatch = std::make_unique(pfn, &testState); + VERIFY_IS_NOT_NULL(dispatch.get()); + auto inputEngine = std::make_unique(dispatch.release(), true); + VERIFY_IS_NOT_NULL(inputEngine.get()); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + Log::Comment(NoThrowString().Format( + L"Try sending a cursor position response, then send it again. " + L"The first time, it should be interpreted as a cursor position. " + L"The state machine engine should reset itself to normal operation " + L"after that, and treat the second as an F3." + )); + + std::wstring seq = L"\x1b[1;4R"; + testState._expectCursorPosition = true; + testState._expectedCursor = { 4, 1 }; + + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", seq.c_str() + )); + _stateMachine->ProcessString(&seq[0], seq.length()); + + testState._expectCursorPosition = false; + + INPUT_RECORD inputRec; + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = LEFT_ALT_PRESSED | SHIFT_PRESSED; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = VK_F3; + inputRec.Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKey(VK_F3, MAPVK_VK_TO_VSC)); + inputRec.Event.KeyEvent.uChar.UnicodeChar = L'\0'; + + testState.vExpectedInput.push_back(inputRec); + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", seq.c_str() + )); + _stateMachine->ProcessString(&seq[0], seq.length()); +} + +void InputEngineTest::CSICursorBackTabTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + INPUT_RECORD inputRec; + + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = SHIFT_PRESSED; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = VK_TAB; + inputRec.Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(VK_TAB, MAPVK_VK_TO_VSC)); + inputRec.Event.KeyEvent.uChar.UnicodeChar = L'\t'; + + testState.vExpectedInput.push_back(inputRec); + + const std::wstring seq = L"\x1b[Z"; + Log::Comment(NoThrowString().Format( + L"Processing \"%s\"", seq.c_str() + )); + _stateMachine->ProcessString(&seq[0], seq.length()); +} + +void InputEngineTest::AltBackspaceTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + INPUT_RECORD inputRec; + + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = LEFT_ALT_PRESSED; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = VK_BACK; + inputRec.Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(VK_BACK, MAPVK_VK_TO_VSC)); + inputRec.Event.KeyEvent.uChar.UnicodeChar = L'\x08'; + + testState.vExpectedInput.push_back(inputRec); + + const std::wstring seq = L"\x1b\x7f"; + Log::Comment(NoThrowString().Format(L"Processing \"\\x1b\\x7f\"")); + _stateMachine->ProcessString(seq); +} + +void InputEngineTest::AltCtrlDTest() +{ + TestState testState; + auto pfn = std::bind(&TestState::TestInputCallback, &testState, std::placeholders::_1); + + auto inputEngine = std::make_unique(new TestInteractDispatch(pfn, &testState)); + auto _stateMachine = std::make_unique(inputEngine.release()); + VERIFY_IS_NOT_NULL(_stateMachine); + testState._stateMachine = _stateMachine.get(); + + INPUT_RECORD inputRec; + + inputRec.EventType = KEY_EVENT; + inputRec.Event.KeyEvent.bKeyDown = TRUE; + inputRec.Event.KeyEvent.dwControlKeyState = LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED; + inputRec.Event.KeyEvent.wRepeatCount = 1; + inputRec.Event.KeyEvent.wVirtualKeyCode = 0x44; // D key + inputRec.Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(0x44, MAPVK_VK_TO_VSC)); + inputRec.Event.KeyEvent.uChar.UnicodeChar = L'\x04'; + + testState.vExpectedInput.push_back(inputRec); + + const std::wstring seq = L"\x1b\x04"; + Log::Comment(NoThrowString().Format(L"Processing \"\\x1b\\x04\"")); + _stateMachine->ProcessString(seq); +} diff --git a/src/terminal/parser/ut_parser/OutputEngineTest.cpp b/src/terminal/parser/ut_parser/OutputEngineTest.cpp new file mode 100644 index 000000000..b3edf84a7 --- /dev/null +++ b/src/terminal/parser/ut_parser/OutputEngineTest.cpp @@ -0,0 +1,1650 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include +#include "../../inc/consoletaeftemplates.hpp" + +#include "stateMachine.hpp" +#include "OutputStateMachineEngine.hpp" + +#include "ascii.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +namespace Microsoft +{ + namespace Console + { + namespace VirtualTerminal + { + class OutputEngineTest; + } + } +} + +// From VT100.net... +// 9999-10000 is the classic boundary for most parsers parameter values. +// 16383-16384 is the boundary for DECSR commands according to EK-VT520-RM section 4.3.3.2 +// 32767-32768 is our boundary SHORT_MAX for the Windows console +#define PARAM_VALUES L"{0, 1, 2, 1000, 9999, 10000, 16383, 16384, 32767, 32768, 50000, 999999999}" + +class DummyDispatch final : public TermDispatch +{ +public: + virtual void Execute(const wchar_t /*wchControl*/) override + { + } + + virtual void Print(const wchar_t /*wchPrintable*/) override + { + } + + virtual void PrintString(const wchar_t* const /*rgwch*/, const size_t /*cch*/) override + { + } +}; + +class Microsoft::Console::VirtualTerminal::OutputEngineTest final +{ + TEST_CLASS(OutputEngineTest); + + TEST_METHOD(TestEscapePath) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiTest", L"{0,1,2,3,4,5,6,7,8,9,10,11}") // one value for each type of state test below. + END_TEST_METHOD_PROPERTIES() + + unsigned int uiTest; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiTest", uiTest)); + + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + // The OscString state shouldn't escape out after an ESC. + bool shouldEscapeOut = true; + + switch (uiTest) + { + case 0: + { + Log::Comment(L"Escape from Ground."); + mach._state = StateMachine::VTStates::Ground; + break; + } + case 1: + { + Log::Comment(L"Escape from Escape."); + mach._state = StateMachine::VTStates::Escape; + break; + } + case 2: + { + Log::Comment(L"Escape from Escape Intermediate"); + mach._state = StateMachine::VTStates::EscapeIntermediate; + break; + } + case 3: + { + Log::Comment(L"Escape from CsiEntry"); + mach._state = StateMachine::VTStates::CsiEntry; + break; + } + case 4: + { + Log::Comment(L"Escape from CsiIgnore"); + mach._state = StateMachine::VTStates::CsiIgnore; + break; + } + case 5: + { + Log::Comment(L"Escape from CsiParam"); + mach._state = StateMachine::VTStates::CsiParam; + break; + } + case 6: + { + Log::Comment(L"Escape from CsiIntermediate"); + mach._state = StateMachine::VTStates::CsiIntermediate; + break; + } + case 7: + { + Log::Comment(L"Escape from OscParam"); + mach._state = StateMachine::VTStates::OscParam; + break; + } + case 8: + { + Log::Comment(L"Escape from OscString"); + shouldEscapeOut = false; + mach._state = StateMachine::VTStates::OscString; + break; + } + case 9: + { + Log::Comment(L"Escape from OscTermination"); + mach._state = StateMachine::VTStates::OscTermination; + break; + } + case 10: + { + Log::Comment(L"Escape from Ss3Entry"); + mach._state = StateMachine::VTStates::Ss3Entry; + break; + } + case 11: + { + Log::Comment(L"Escape from Ss3Param"); + mach._state = StateMachine::VTStates::Ss3Param; + break; + } + } + + mach.ProcessCharacter(AsciiChars::ESC); + if(shouldEscapeOut) + { + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + } + + } + + TEST_METHOD(TestEscapeImmediatePath) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'#'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::EscapeIntermediate); + mach.ProcessCharacter(L'('); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::EscapeIntermediate); + mach.ProcessCharacter(L')'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::EscapeIntermediate); + mach.ProcessCharacter(L'#'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::EscapeIntermediate); + mach.ProcessCharacter(L'6'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestEscapeThenC0Path) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + // When we see a C0 control char in the escape state, the Output engine + // should execute it, without interrupting the sequence it's currently + // processing + mach.ProcessCharacter(L'\x03'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'1'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + + TEST_METHOD(TestGroundPrint) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(L'a'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestCsiEntry) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestC1CsiEntry) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(L'\x9b'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestCsiImmediate) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'$'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIntermediate); + mach.ProcessCharacter(L'#'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIntermediate); + mach.ProcessCharacter(L'%'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIntermediate); + mach.ProcessCharacter(L'v'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestCsiParam) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'2'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'4'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'8'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestCsiIgnore) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'q'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'4'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'8'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'['); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiEntry); + mach.ProcessCharacter(L'4'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiParam); + mach.ProcessCharacter(L'#'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIntermediate); + mach.ProcessCharacter(L':'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'8'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::CsiIgnore); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestOscStringSimple) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'o'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'e'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L' '); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L't'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'e'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'x'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L't'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'o'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'e'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L' '); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L't'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'e'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L'x'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L't'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscTermination); + mach.ProcessCharacter(L'\\'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + TEST_METHOD(TestLongOscString) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L'0'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + mach.ProcessCharacter(L';'); + for (int i = 0; i < MAX_PATH; i++) // The buffer is only 256 long, so any longer value should work :P + { + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + } + VERIFY_ARE_EQUAL(mach._sOscNextChar, mach.s_cOscStringMaxLength - 1); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(NormalTestOscParam) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + for (int i = 0; i < 5; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'1' + i)); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + } + VERIFY_ARE_EQUAL(mach._sOscParam, 12345); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + TEST_METHOD(TestLongOscParam) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + for (int i = 0; i < 6; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'1' + i)); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + } + VERIFY_ARE_EQUAL(mach._sOscParam, SHORT_MAX); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + Log::Comment(L"Make sure we cap the param value to SHORT_MAX"); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L']'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + for (int i = 0; i < 5; i++) // We're only expecting to be able to keep 5 digits max + { + mach.ProcessCharacter((wchar_t)(L'4' + i)); // 45678 > (SHORT_MAX===32767) + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscParam); + } + VERIFY_ARE_EQUAL(mach._sOscParam, SHORT_MAX); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(L's'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::OscString); + mach.ProcessCharacter(AsciiChars::BEL); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestSs3Entry) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L'm'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestSs3Immediate) + { + // Intermediates aren't supported by Ss3 - they just get dispatched + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L'$'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L'#'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L'%'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L'?'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } + + TEST_METHOD(TestSs3Param) + { + StateMachine mach(new OutputStateMachineEngine(new DummyDispatch)); + + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + mach.ProcessCharacter(AsciiChars::ESC); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Escape); + mach.ProcessCharacter(L'O'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Entry); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L'3'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L'2'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L'4'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L';'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L'8'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ss3Param); + mach.ProcessCharacter(L'J'); + VERIFY_ARE_EQUAL(mach._state, StateMachine::VTStates::Ground); + } +}; + +class StatefulDispatch final : public TermDispatch +{ +public: + + virtual void Execute(const wchar_t /*wchControl*/) override + { + } + + virtual void Print(const wchar_t /*wchPrintable*/) override + { + } + + virtual void PrintString(const wchar_t* const /*rgwch*/, const size_t /*cch*/) override + { + } + + StatefulDispatch() : + _uiCursorDistance{ 0 }, + _uiLine{ 0 }, + _uiColumn{ 0 }, + _fCursorUp{ false }, + _fCursorDown{ false }, + _fCursorBackward{ false }, + _fCursorForward{ false }, + _fCursorNextLine{ false }, + _fCursorPreviousLine{ false }, + _fCursorHorizontalPositionAbsolute{ false }, + _fVerticalLinePositionAbsolute{ false }, + _fCursorPosition{ false }, + _fCursorSave{ false }, + _fCursorLoad{ false }, + _fCursorVisible{ true }, + _fEraseDisplay{ false }, + _fEraseLine{ false }, + _fInsertCharacter{ false }, + _fDeleteCharacter{ false }, + _eraseType{ (DispatchTypes::EraseType)-1 }, + _fSetGraphics{ false }, + _statusReportType{ (DispatchTypes::AnsiStatusType)-1 }, + _fDeviceStatusReport{ false }, + _fDeviceAttributes{ false }, + _cOptions{ 0 }, + _fIsAltBuffer{ false }, + _fCursorKeysMode{ false }, + _fCursorBlinking{ true }, + _uiWindowWidth{ 80 } + { + memset(_rgOptions, s_uiGraphicsCleared, sizeof(_rgOptions)); + } + + void ClearState() + { + StatefulDispatch dispatch; + *this = dispatch; + } + + bool CursorUp(_In_ unsigned int const uiDistance) override + { + _fCursorUp = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorDown(_In_ unsigned int const uiDistance) override + { + _fCursorDown = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorBackward(_In_ unsigned int const uiDistance) override + { + _fCursorBackward = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorForward(_In_ unsigned int const uiDistance) override + { + _fCursorForward = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorNextLine(_In_ unsigned int const uiDistance) override + { + _fCursorNextLine = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorPrevLine(_In_ unsigned int const uiDistance) override + { + _fCursorPreviousLine = true; + _uiCursorDistance = uiDistance; + return true; + } + + bool CursorHorizontalPositionAbsolute(_In_ unsigned int const uiPosition) override + { + _fCursorHorizontalPositionAbsolute = true; + _uiCursorDistance = uiPosition; + return true; + } + + bool VerticalLinePositionAbsolute(_In_ unsigned int const uiPosition) override + { + _fVerticalLinePositionAbsolute = true; + _uiCursorDistance = uiPosition; + return true; + } + + bool CursorPosition(_In_ unsigned int const uiLine, _In_ unsigned int const uiColumn) override + { + _fCursorPosition = true; + _uiLine = uiLine; + _uiColumn = uiColumn; + return true; + } + + bool CursorSavePosition() override + { + _fCursorSave = true; + return true; + } + + bool CursorRestorePosition() override + { + _fCursorLoad = true; + return true; + } + + bool EraseInDisplay(const DispatchTypes::EraseType eraseType) override + { + _fEraseDisplay = true; + _eraseType = eraseType; + return true; + } + + bool EraseInLine(const DispatchTypes::EraseType eraseType) override + { + _fEraseLine = true; + _eraseType = eraseType; + return true; + } + + bool InsertCharacter(_In_ unsigned int const uiCount) override + { + _fInsertCharacter = true; + _uiCursorDistance = uiCount; + return true; + } + + bool DeleteCharacter(_In_ unsigned int const uiCount) override + { + _fDeleteCharacter = true; + _uiCursorDistance = uiCount; + return true; + } + + bool CursorVisibility(const bool fIsVisible) override + { + _fCursorVisible = fIsVisible; + return true; + } + + bool SetGraphicsRendition(_In_reads_(cOptions) const DispatchTypes::GraphicsOptions* const rgOptions, const size_t cOptions) override + { + size_t cCopyLength = std::min(cOptions, s_cMaxOptions); // whichever is smaller, our buffer size or the number given + _cOptions = cCopyLength; + memcpy(_rgOptions, rgOptions, _cOptions * sizeof(DispatchTypes::GraphicsOptions)); + + _fSetGraphics = true; + + return true; + } + + bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override + { + _fDeviceStatusReport = true; + _statusReportType = statusType; + + return true; + } + + bool DeviceAttributes() override + { + _fDeviceAttributes = true; + + return true; + } + + bool _PrivateModeParamsHelper(_In_ DispatchTypes::PrivateModeParams const param, const bool fEnable) + { + bool fSuccess = false; + switch(param) + { + case DispatchTypes::PrivateModeParams::DECCKM_CursorKeysMode: + // set - Enable Application Mode, reset - Numeric/normal mode + fSuccess = SetVirtualTerminalInputMode(fEnable); + break; + case DispatchTypes::PrivateModeParams::DECCOLM_SetNumberOfColumns: + fSuccess = SetColumns(static_cast(fEnable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns)); + break; + case DispatchTypes::PrivateModeParams::ATT610_StartCursorBlink: + fSuccess = EnableCursorBlinking(fEnable); + break; + case DispatchTypes::PrivateModeParams::DECTCEM_TextCursorEnableMode: + fSuccess = CursorVisibility(fEnable); + break; + case DispatchTypes::PrivateModeParams::ASB_AlternateScreenBuffer: + fSuccess = fEnable? UseAlternateScreenBuffer() : UseMainScreenBuffer(); + break; + default: + // If no functions to call, overall dispatch was a failure. + fSuccess = false; + break; + } + return fSuccess; + } + + bool _SetResetPrivateModesHelper(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rParams, + const size_t cParams, + const bool fEnable) + { + size_t cFailures = 0; + for (size_t i = 0; i < cParams; i++) + { + cFailures += _PrivateModeParamsHelper(rParams[i], fEnable)? 0 : 1; // increment the number of failures if we fail. + } + return cFailures == 0; + } + + bool SetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rParams, const size_t cParams) override + { + return _SetResetPrivateModesHelper(rParams, cParams, true); + } + + bool ResetPrivateModes(_In_reads_(cParams) const DispatchTypes::PrivateModeParams* const rParams, const size_t cParams) override + { + return _SetResetPrivateModesHelper(rParams, cParams, false); + } + + bool SetColumns(_In_ unsigned int const uiColumns) override + { + _uiWindowWidth = uiColumns; + return true; + } + + bool SetVirtualTerminalInputMode(const bool fApplicationMode) + { + _fCursorKeysMode = fApplicationMode; + return true; + } + + bool EnableCursorBlinking(const bool bEnable) override + { + _fCursorBlinking = bEnable; + return true; + } + + bool UseAlternateScreenBuffer() override + { + _fIsAltBuffer = true; + return true; + } + + bool UseMainScreenBuffer() override + { + _fIsAltBuffer = false; + return true; + } + + unsigned int _uiCursorDistance; + unsigned int _uiLine; + unsigned int _uiColumn; + bool _fCursorUp; + bool _fCursorDown; + bool _fCursorBackward; + bool _fCursorForward; + bool _fCursorNextLine; + bool _fCursorPreviousLine; + bool _fCursorHorizontalPositionAbsolute; + bool _fVerticalLinePositionAbsolute; + bool _fCursorPosition; + bool _fCursorSave; + bool _fCursorLoad; + bool _fCursorVisible; + bool _fEraseDisplay; + bool _fEraseLine; + bool _fInsertCharacter; + bool _fDeleteCharacter; + DispatchTypes::EraseType _eraseType; + bool _fSetGraphics; + DispatchTypes::AnsiStatusType _statusReportType; + bool _fDeviceStatusReport; + bool _fDeviceAttributes; + bool _fIsAltBuffer; + bool _fCursorKeysMode; + bool _fCursorBlinking; + unsigned int _uiWindowWidth; + + static const size_t s_cMaxOptions = 16; + static const unsigned int s_uiGraphicsCleared = UINT_MAX; + DispatchTypes::GraphicsOptions _rgOptions[s_cMaxOptions]; + size_t _cOptions; +}; + +class StateMachineExternalTest final +{ + TEST_CLASS(StateMachineExternalTest); + + TEST_METHOD_SETUP(SetupState) + { + return true; + } + + void TestEscCursorMovement(wchar_t const wchCommand, + const bool* const pfFlag, + StateMachine& mach, + StatefulDispatch& dispatch) + { + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(wchCommand); + + VERIFY_IS_TRUE(*pfFlag); + VERIFY_ARE_EQUAL(dispatch._uiCursorDistance, 1u); + } + + TEST_METHOD(TestEscCursorMovement) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + TestEscCursorMovement(L'A', &pDispatch->_fCursorUp, mach, *pDispatch); + TestEscCursorMovement(L'B', &pDispatch->_fCursorDown, mach, *pDispatch); + TestEscCursorMovement(L'C', &pDispatch->_fCursorForward, mach, *pDispatch); + TestEscCursorMovement(L'D', &pDispatch->_fCursorBackward, mach, *pDispatch); + } + + void InsertNumberToMachine(StateMachine* const pMachine, unsigned int uiNumber) + { + static const size_t cchBufferMax = 20; + + wchar_t pwszDistance[cchBufferMax]; + int cchDistance = swprintf_s(pwszDistance, cchBufferMax, L"%d", uiNumber); + + if (cchDistance > 0 && cchDistance < cchBufferMax) + { + for (int i = 0; i < cchDistance; i++) + { + pMachine->ProcessCharacter(pwszDistance[i]); + } + } + } + + void ApplyParameterBoundary(unsigned int* uiExpected, unsigned int uiGiven) + { + // 0 and 1 should be 1. Use the preset value. + // 1-SHORT_MAX should be what we set. + // > SHORT_MAX should be SHORT_MAX. + if (uiGiven <= 1) + { + *uiExpected = 1u; + } + else if (uiGiven > 1 && uiGiven <= SHORT_MAX) + { + *uiExpected = uiGiven; + } + else if (uiGiven > SHORT_MAX) + { + *uiExpected = SHORT_MAX; // 16383 is our max value. + } + } + + void TestCsiCursorMovement(wchar_t const wchCommand, unsigned int const uiDistance, + const bool fUseDistance, + const bool* const pfFlag, + StateMachine& mach, + StatefulDispatch& dispatch) + { + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + + if (fUseDistance) + { + InsertNumberToMachine(&mach, uiDistance); + } + + mach.ProcessCharacter(wchCommand); + + VERIFY_IS_TRUE(*pfFlag); + + unsigned int uiExpectedDistance = 1u; + + if (fUseDistance) + { + ApplyParameterBoundary(&uiExpectedDistance, uiDistance); + } + + VERIFY_ARE_EQUAL(dispatch._uiCursorDistance, uiExpectedDistance); + } + + TEST_METHOD(TestCsiCursorMovementWithValues) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiDistance", PARAM_VALUES) + END_TEST_METHOD_PROPERTIES() + + unsigned int uiDistance; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDistance", uiDistance)); + + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + TestCsiCursorMovement(L'A', uiDistance, true, &pDispatch->_fCursorUp, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'B', uiDistance, true, &pDispatch->_fCursorDown, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'C', uiDistance, true, &pDispatch->_fCursorForward, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'D', uiDistance, true, &pDispatch->_fCursorBackward, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'E', uiDistance, true, &pDispatch->_fCursorNextLine, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'F', uiDistance, true, &pDispatch->_fCursorPreviousLine, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'G', uiDistance, true, &pDispatch->_fCursorHorizontalPositionAbsolute, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'd', uiDistance, true, &pDispatch->_fVerticalLinePositionAbsolute, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'@', uiDistance, true, &pDispatch->_fInsertCharacter, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'P', uiDistance, true, &pDispatch->_fDeleteCharacter, mach, *pDispatch); + } + + TEST_METHOD(TestCsiCursorMovementWithoutValues) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + unsigned int uiDistance = 9999; // this value should be ignored with the false below. + TestCsiCursorMovement(L'A', uiDistance, false, &pDispatch->_fCursorUp, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'B', uiDistance, false, &pDispatch->_fCursorDown, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'C', uiDistance, false, &pDispatch->_fCursorForward, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'D', uiDistance, false, &pDispatch->_fCursorBackward, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'E', uiDistance, false, &pDispatch->_fCursorNextLine, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'F', uiDistance, false, &pDispatch->_fCursorPreviousLine, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'G', uiDistance, false, &pDispatch->_fCursorHorizontalPositionAbsolute, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'd', uiDistance, false, &pDispatch->_fVerticalLinePositionAbsolute, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'@', uiDistance, false, &pDispatch->_fInsertCharacter, mach, *pDispatch); + pDispatch->ClearState(); + TestCsiCursorMovement(L'P', uiDistance, false, &pDispatch->_fDeleteCharacter, mach, *pDispatch); + } + + TEST_METHOD(TestCsiCursorPosition) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiRow", PARAM_VALUES) + TEST_METHOD_PROPERTY(L"Data:uiCol", PARAM_VALUES) + END_TEST_METHOD_PROPERTIES() + + unsigned int uiRow; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiRow", uiRow)); + unsigned int uiCol; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiCol", uiCol)); + + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + + InsertNumberToMachine(&mach, uiRow); + mach.ProcessCharacter(L';'); + InsertNumberToMachine(&mach, uiCol); + mach.ProcessCharacter(L'H'); + + // bound the row/col values by the max we expect + ApplyParameterBoundary(&uiRow, uiRow); + ApplyParameterBoundary(&uiCol, uiCol); + + VERIFY_IS_TRUE(pDispatch->_fCursorPosition); + VERIFY_ARE_EQUAL(pDispatch->_uiLine, uiRow); + VERIFY_ARE_EQUAL(pDispatch->_uiColumn, uiCol); + } + + TEST_METHOD(TestCsiCursorPositionWithOnlyRow) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiRow", PARAM_VALUES) + END_TEST_METHOD_PROPERTIES() + + unsigned int uiRow; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiRow", uiRow)); + + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + + InsertNumberToMachine(&mach, uiRow); + mach.ProcessCharacter(L'H'); + + // bound the row/col values by the max we expect + ApplyParameterBoundary(&uiRow, uiRow); + + VERIFY_IS_TRUE(pDispatch->_fCursorPosition); + VERIFY_ARE_EQUAL(pDispatch->_uiLine, uiRow); + VERIFY_ARE_EQUAL(pDispatch->_uiColumn, (unsigned int)1); // Without the second param, the column should always be the default + } + + TEST_METHOD(TestCursorSaveLoad) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'7'); + VERIFY_IS_TRUE(pDispatch->_fCursorSave); + + pDispatch->ClearState(); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'8'); + VERIFY_IS_TRUE(pDispatch->_fCursorLoad); + + pDispatch->ClearState(); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L's'); + VERIFY_IS_TRUE(pDispatch->_fCursorSave); + + pDispatch->ClearState(); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'u'); + VERIFY_IS_TRUE(pDispatch->_fCursorLoad); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestCursorKeysMode) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessString(L"\x1b[?1h", 5); + VERIFY_IS_TRUE(pDispatch->_fCursorKeysMode); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?1l", 5); + VERIFY_IS_FALSE(pDispatch->_fCursorKeysMode); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestSetNumberOfColumns) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessString(L"\x1b[?3h", 5); + VERIFY_ARE_EQUAL(pDispatch->_uiWindowWidth, static_cast(DispatchTypes::s_sDECCOLMSetColumns)); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?3l", 5); + VERIFY_ARE_EQUAL(pDispatch->_uiWindowWidth, static_cast(DispatchTypes::s_sDECCOLMResetColumns)); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestCursorBlinking) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessString(L"\x1b[?12h", 6); + VERIFY_IS_TRUE(pDispatch->_fCursorBlinking); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?12l", 6); + VERIFY_IS_FALSE(pDispatch->_fCursorBlinking); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestCursorVisibility) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessString(L"\x1b[?25h", 6); + VERIFY_IS_TRUE(pDispatch->_fCursorVisible); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?25l", 6); + VERIFY_IS_FALSE(pDispatch->_fCursorVisible); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestAltBufferSwapping) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + mach.ProcessString(L"\x1b[?1049h", 8); + VERIFY_IS_TRUE(pDispatch->_fIsAltBuffer); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?1049h", 8); + VERIFY_IS_TRUE(pDispatch->_fIsAltBuffer); + mach.ProcessString(L"\x1b[?1049h", 8); + VERIFY_IS_TRUE(pDispatch->_fIsAltBuffer); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?1049l", 8); + VERIFY_IS_FALSE(pDispatch->_fIsAltBuffer); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?1049h", 8); + VERIFY_IS_TRUE(pDispatch->_fIsAltBuffer); + mach.ProcessString(L"\x1b[?1049l", 8); + VERIFY_IS_FALSE(pDispatch->_fIsAltBuffer); + + pDispatch->ClearState(); + + mach.ProcessString(L"\x1b[?1049l", 8); + VERIFY_IS_FALSE(pDispatch->_fIsAltBuffer); + mach.ProcessString(L"\x1b[?1049l", 8); + VERIFY_IS_FALSE(pDispatch->_fIsAltBuffer); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestErase) + { + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:uiEraseOperation", L"{0, 1}") // for "display" and "line" type erase operations + TEST_METHOD_PROPERTY(L"Data:uiDispatchTypes::EraseType", L"{0, 1, 2, 10}") // maps to DispatchTypes::EraseType enum class options. + END_TEST_METHOD_PROPERTIES() + + unsigned int uiEraseOperation; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiEraseOperation", uiEraseOperation)); + unsigned int uiDispatchTypes; + VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"uiDispatchTypes::EraseType", uiDispatchTypes)); + + WCHAR wchOp = L'\0'; + bool* pfOperationCallback = nullptr; + + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + switch (uiEraseOperation) + { + case 0: + wchOp = L'J'; + pfOperationCallback = &pDispatch->_fEraseDisplay; + break; + case 1: + wchOp = L'K'; + pfOperationCallback = &pDispatch->_fEraseLine; + break; + default: + VERIFY_FAIL(L"Unknown erase operation permutation."); + } + + VERIFY_IS_NOT_NULL(wchOp); + VERIFY_IS_NOT_NULL(pfOperationCallback); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + + DispatchTypes::EraseType expectedDispatchTypes; + + switch (uiDispatchTypes) + { + case 0: + expectedDispatchTypes = DispatchTypes::EraseType::ToEnd; + InsertNumberToMachine(&mach, uiDispatchTypes); + break; + case 1: + expectedDispatchTypes = DispatchTypes::EraseType::FromBeginning; + InsertNumberToMachine(&mach, uiDispatchTypes); + break; + case 2: + expectedDispatchTypes = DispatchTypes::EraseType::All; + InsertNumberToMachine(&mach, uiDispatchTypes); + break; + case 10: + // Do nothing. Default case of 10 should be like a 0 to the end. + expectedDispatchTypes = DispatchTypes::EraseType::ToEnd; + break; + } + + mach.ProcessCharacter(wchOp); + + + VERIFY_IS_TRUE(*pfOperationCallback); + VERIFY_ARE_EQUAL(expectedDispatchTypes, pDispatch->_eraseType); + } + + void VerifyDispatchTypes(_In_reads_(cExpectedOptions) const DispatchTypes::GraphicsOptions* const rgExpectedOptions, + const size_t cExpectedOptions, + const StatefulDispatch& dispatch) + { + VERIFY_ARE_EQUAL(cExpectedOptions, dispatch._cOptions); + bool fOptionsValid = true; + + for (size_t i = 0; i < dispatch.s_cMaxOptions; i++) + { + auto expectedOption = (DispatchTypes::GraphicsOptions)dispatch.s_uiGraphicsCleared; + + if (i < cExpectedOptions) + { + expectedOption = rgExpectedOptions[i]; + } + + fOptionsValid = expectedOption == dispatch._rgOptions[i]; + + if (!fOptionsValid) + { + Log::Comment(NoThrowString().Format(L"Graphics option match failed, index [%zu]. Expected: '%d' Actual: '%d'", i, expectedOption, dispatch._rgOptions[i])); + break; + } + } + + VERIFY_IS_TRUE(fOptionsValid); + } + + TEST_METHOD(TestSetGraphicsRendition) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + DispatchTypes::GraphicsOptions rgExpected[16]; + + Log::Comment(L"Test 1: Check default case."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'm'); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::Off; + VerifyDispatchTypes(rgExpected, 1, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 2: Check clear/0 case."); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'0'); + mach.ProcessCharacter(L'm'); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::Off; + VerifyDispatchTypes(rgExpected, 1, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 3: Check 'handful of options' case."); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'7'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'3'); + mach.ProcessCharacter(L'0'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L'5'); + mach.ProcessCharacter(L'm'); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[2] = DispatchTypes::GraphicsOptions::Negative; + rgExpected[3] = DispatchTypes::GraphicsOptions::ForegroundBlack; + rgExpected[4] = DispatchTypes::GraphicsOptions::BackgroundMagenta; + VerifyDispatchTypes(rgExpected, 5, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 4: Check 'too many options' (>16) case."); + + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'4'); + mach.ProcessCharacter(L';'); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L'm'); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[2] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[3] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[4] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[5] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[6] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[7] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[8] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[9] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[10] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[11] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[12] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[13] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[14] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[15] = DispatchTypes::GraphicsOptions::Underline; + VerifyDispatchTypes(rgExpected, 16, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 5.a: Test an empty param at the end of a sequence"); + + std::wstring sequence = L"\x1b[1;m"; + mach.ProcessString(&sequence[0], sequence.length()); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::Off; + VerifyDispatchTypes(rgExpected, 2, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 5.b: Test an empty param in the middle of a sequence"); + + sequence = L"\x1b[1;;1m"; + mach.ProcessString(&sequence[0], sequence.length()); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::Off; + rgExpected[2] = DispatchTypes::GraphicsOptions::BoldBright; + VerifyDispatchTypes(rgExpected, 3, *pDispatch); + + pDispatch->ClearState(); + + Log::Comment(L"Test 5.c: Test an empty param at the start of a sequence"); + + sequence = L"\x1b[;31;1m"; + mach.ProcessString(&sequence[0], sequence.length()); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + rgExpected[0] = DispatchTypes::GraphicsOptions::Off; + rgExpected[1] = DispatchTypes::GraphicsOptions::ForegroundRed; + rgExpected[2] = DispatchTypes::GraphicsOptions::BoldBright; + VerifyDispatchTypes(rgExpected, 3, *pDispatch); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestDeviceStatusReport) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + Log::Comment(L"Test 1: Check empty case. Should fail."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'n'); + + VERIFY_IS_FALSE(pDispatch->_fDeviceStatusReport); + + pDispatch->ClearState(); + + Log::Comment(L"Test 2: Check CSR (cursor position command) case 6. Should succeed."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'6'); + mach.ProcessCharacter(L'n'); + + VERIFY_IS_TRUE(pDispatch->_fDeviceStatusReport); + VERIFY_ARE_EQUAL(DispatchTypes::AnsiStatusType::CPR_CursorPositionReport, pDispatch->_statusReportType); + + pDispatch->ClearState(); + + Log::Comment(L"Test 3: Check unimplemented case 1. Should fail."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L'n'); + + VERIFY_IS_FALSE(pDispatch->_fDeviceStatusReport); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestDeviceAttributes) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + Log::Comment(L"Test 1: Check default case, no params."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'c'); + + VERIFY_IS_TRUE(pDispatch->_fDeviceAttributes); + + pDispatch->ClearState(); + + Log::Comment(L"Test 2: Check default case, 0 param."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'0'); + mach.ProcessCharacter(L'c'); + + VERIFY_IS_TRUE(pDispatch->_fDeviceAttributes); + + pDispatch->ClearState(); + + Log::Comment(L"Test 3: Check fail case, 1 (or any other) param."); + mach.ProcessCharacter(AsciiChars::ESC); + mach.ProcessCharacter(L'['); + mach.ProcessCharacter(L'1'); + mach.ProcessCharacter(L'c'); + + VERIFY_IS_FALSE(pDispatch->_fDeviceAttributes); + + pDispatch->ClearState(); + } + + TEST_METHOD(TestStrings) + { + StatefulDispatch* pDispatch = new StatefulDispatch; + VERIFY_IS_NOT_NULL(pDispatch); + StateMachine mach(new OutputStateMachineEngine(pDispatch)); + + DispatchTypes::GraphicsOptions rgExpected[16]; + DispatchTypes::EraseType expectedDispatchTypes; + /////////////////////////////////////////////////////////////////////// + + Log::Comment(L"Test 1: Basic String processing. One sequence in a string."); + mach.ProcessString(L"\x1b[0m", 4); + + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + + pDispatch->ClearState(); + + /////////////////////////////////////////////////////////////////////// + + Log::Comment(L"Test 2: A couple of sequences all in one string"); + + mach.ProcessString(L"\x1b[1;4;7;30;45m\x1b[2J", 18); + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + VERIFY_IS_TRUE(pDispatch->_fEraseDisplay); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::Underline; + rgExpected[2] = DispatchTypes::GraphicsOptions::Negative; + rgExpected[3] = DispatchTypes::GraphicsOptions::ForegroundBlack; + rgExpected[4] = DispatchTypes::GraphicsOptions::BackgroundMagenta; + expectedDispatchTypes = DispatchTypes::EraseType::All; + VerifyDispatchTypes(rgExpected, 5, *pDispatch); + VERIFY_ARE_EQUAL(expectedDispatchTypes, pDispatch->_eraseType); + + pDispatch->ClearState(); + + /////////////////////////////////////////////////////////////////////// + Log::Comment(L"Test 3: Two sequences seperated by a non-sequence of characters"); + + mach.ProcessString(L"\x1b[1;30mHello World\x1b[2J", 22); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::ForegroundBlack; + expectedDispatchTypes = DispatchTypes::EraseType::All; + + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + VERIFY_IS_TRUE(pDispatch->_fEraseDisplay); + + VerifyDispatchTypes(rgExpected, 2, *pDispatch); + VERIFY_ARE_EQUAL(expectedDispatchTypes, pDispatch->_eraseType); + + pDispatch->ClearState(); + + /////////////////////////////////////////////////////////////////////// + Log::Comment(L"Test 4: An entire sequence broke into multiple strings"); + mach.ProcessString(L"\x1b[1;", 4); + VERIFY_IS_FALSE(pDispatch->_fSetGraphics); + VERIFY_IS_FALSE(pDispatch->_fEraseDisplay); + + mach.ProcessString(L"30mHello World\x1b[2J", 18); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::ForegroundBlack; + expectedDispatchTypes = DispatchTypes::EraseType::All; + + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + VERIFY_IS_TRUE(pDispatch->_fEraseDisplay); + + VerifyDispatchTypes(rgExpected, 2, *pDispatch); + VERIFY_ARE_EQUAL(expectedDispatchTypes, pDispatch->_eraseType); + + pDispatch->ClearState(); + + /////////////////////////////////////////////////////////////////////// + Log::Comment(L"Test 5: A sequence with mixed ProcessCharacter and ProcessString calls"); + + rgExpected[0] = DispatchTypes::GraphicsOptions::BoldBright; + rgExpected[1] = DispatchTypes::GraphicsOptions::ForegroundBlack; + + mach.ProcessString(L"\x1b[1;", 4); + VERIFY_IS_FALSE(pDispatch->_fSetGraphics); + VERIFY_IS_FALSE(pDispatch->_fEraseDisplay); + + mach.ProcessCharacter(L'3'); + VERIFY_IS_FALSE(pDispatch->_fSetGraphics); + VERIFY_IS_FALSE(pDispatch->_fEraseDisplay); + + mach.ProcessCharacter(L'0'); + VERIFY_IS_FALSE(pDispatch->_fSetGraphics); + VERIFY_IS_FALSE(pDispatch->_fEraseDisplay); + + mach.ProcessCharacter(L'm'); + + + VERIFY_IS_TRUE(pDispatch->_fSetGraphics); + VERIFY_IS_FALSE(pDispatch->_fEraseDisplay); + VerifyDispatchTypes(rgExpected, 2, *pDispatch); + + mach.ProcessString(L"Hello World\x1b[2J", 15); + + expectedDispatchTypes = DispatchTypes::EraseType::All; + + VERIFY_IS_TRUE(pDispatch->_fEraseDisplay); + + VERIFY_ARE_EQUAL(expectedDispatchTypes, pDispatch->_eraseType); + + pDispatch->ClearState(); + + } +}; diff --git a/src/terminal/parser/ut_parser/Parser.UnitTests-common.vcxproj b/src/terminal/parser/ut_parser/Parser.UnitTests-common.vcxproj new file mode 100644 index 000000000..24754a9dd --- /dev/null +++ b/src/terminal/parser/ut_parser/Parser.UnitTests-common.vcxproj @@ -0,0 +1,19 @@ + + + + + + Create + + + + + + + + + + ..;%(AdditionalIncludeDirectories) + + + diff --git a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj new file mode 100644 index 000000000..57daef497 --- /dev/null +++ b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + {18d09a24-8240-42d6-8cb6-236eee820263} + + + {dcf55140-ef6a-4736-a403-957e4f7430bb} + + + {3ae13314-1939-4dfa-9c14-38ca0834050c} + + + + + {12144E07-FE63-4D33-9231-748B8D8C3792} + Win32Proj + ParserUnitTests + TerminalParser.UnitTests + ConParser.Unit.Tests + + + + + + + diff --git a/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters new file mode 100644 index 000000000..6aeb1ba04 --- /dev/null +++ b/src/terminal/parser/ut_parser/Parser.UnitTests.vcxproj.filters @@ -0,0 +1,30 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + \ No newline at end of file diff --git a/src/terminal/parser/ut_parser/packages.config b/src/terminal/parser/ut_parser/packages.config new file mode 100644 index 000000000..24feaf0e2 --- /dev/null +++ b/src/terminal/parser/ut_parser/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/src/terminal/parser/ut_parser/product.pbxproj b/src/terminal/parser/ut_parser/product.pbxproj new file mode 100644 index 000000000..9e5ef9830 --- /dev/null +++ b/src/terminal/parser/ut_parser/product.pbxproj @@ -0,0 +1,4 @@ + + + + diff --git a/src/terminal/parser/ut_parser/run.bat b/src/terminal/parser/ut_parser/run.bat new file mode 100644 index 000000000..ffe6c6c85 --- /dev/null +++ b/src/terminal/parser/ut_parser/run.bat @@ -0,0 +1 @@ +te %_NTTREE%\unittests\conterm.parser.tests.dll %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/terminal/parser/ut_parser/sources b/src/terminal/parser/ut_parser/sources new file mode 100644 index 000000000..2be096158 --- /dev/null +++ b/src/terminal/parser/ut_parser/sources @@ -0,0 +1,131 @@ +!include ..\sources.inc +!include ..\..\..\project.unittest.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = Microsoft.Console.VirtualTerminal.Parser.UnitTests +TARGETTYPE = DYNLINK +DLLDEF = + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +SOURCES = \ + $(SOURCES) \ + OutputEngineTest.cpp \ + InputEngineTest.cpp \ + +# The InputEngineTest requires VTRedirMapVirtualKeyW, which means we need the +# ServiceLocator, which means we need the entire host and all it's dependencies, +# as well as the -DBUILD_ONECORE_INTERACTIVITY above + +TARGETLIBS = \ + $(TARGETLIBS) \ + $(ONECORE_INTERNAL_SDK_LIB_PATH)\onecoreuuid.lib \ + $(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + $(SDK_LIB_PATH)\d2d1.lib \ + $(SDK_LIB_PATH)\dwrite.lib \ + $(SDK_LIB_PATH)\dxgi.lib \ + $(SDK_LIB_PATH)\d3d11.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-mm-playsound-l1.lib \ + $(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-dwmapi-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-edputil-policy-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-dc-create-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-gdi-font-l1.lib \ + $(ONECOREWINDOWS_INTERNAL_LIB_PATH_L)\ext-ms-win-gdi-internal-desktop-l1-1-0.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-caret-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-dialogbox-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-draw-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-gui-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-menu-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-misc-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-mouse-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-rectangle-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-server-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-ntuser-window-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-object-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-gdi-rgn-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-cursor-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-dc-access-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-rawinput-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-sysparams-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-rtcore-ntuser-window-ext-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-shell-shell32-l1.lib \ + $(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uxtheme-themes-l1.lib \ + $(MODERNCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\ext-ms-win-uiacore-l1.lib \ + $(WINCORE_OBJ_PATH)\console\conint\$(O)\conint.lib \ + $(CONSOLE_OBJ_PATH)\buffer\out\lib\$(O)\conbufferout.lib \ + $(CONSOLE_OBJ_PATH)\host\lib\$(O)\conhostv2.lib \ + $(CONSOLE_OBJ_PATH)\tsf\$(O)\contsf.lib \ + $(CONSOLE_OBJ_PATH)\propslib\$(O)\conprops.lib \ + $(CONSOLE_OBJ_PATH)\terminal\input\lib\$(O)\ConTermInput.lib \ + $(CONSOLE_OBJ_PATH)\terminal\adapter\lib\$(O)\ConTermAdapter.lib \ + $(CONSOLE_OBJ_PATH)\terminal\parser\lib\$(O)\ConTermParser.lib \ + $(CONSOLE_OBJ_PATH)\renderer\base\lib\$(O)\ConRenderBase.lib \ + $(CONSOLE_OBJ_PATH)\renderer\dx\lib\$(O)\ConRenderDx.lib \ + $(CONSOLE_OBJ_PATH)\renderer\gdi\lib\$(O)\ConRenderGdi.lib \ + $(CONSOLE_OBJ_PATH)\renderer\wddmcon\lib\$(O)\ConRenderWddmCon.lib \ + $(CONSOLE_OBJ_PATH)\renderer\vt\lib\$(O)\ConRenderVt.lib \ + $(CONSOLE_OBJ_PATH)\server\lib\$(O)\ConServer.lib \ + $(CONSOLE_OBJ_PATH)\interactivity\base\lib\$(O)\ConInteractivityBaseLib.lib \ + $(CONSOLE_OBJ_PATH)\interactivity\win32\lib\$(O)\ConInteractivityWin32Lib.lib \ + $(CONSOLE_OBJ_PATH)\interactivity\onecore\lib\$(O)\ConInteractivityOneCoreLib.lib \ + $(CONSOLE_OBJ_PATH)\types\lib\$(O)\ConTypes.lib \ + + +DELAYLOAD = \ + PROPSYS.dll; \ + D2D1.dll; \ + DWrite.dll; \ + DXGI.dll; \ + D3D11.dll; \ + OLEAUT32.dll; \ + api-ms-win-mm-playsound-l1.dll; \ + api-ms-win-shcore-scaling-l1.dll; \ + api-ms-win-shell-namespace-l1.dll; \ + ext-ms-win-dwmapi-ext-l1.dll; \ + ext-ms-win-edputil-policy-l1.dll; \ + ext-ms-win-gdi-dc-l1.dll; \ + ext-ms-win-gdi-dc-create-l1.dll; \ + ext-ms-win-gdi-draw-l1.dll; \ + ext-ms-win-gdi-font-l1.dll; \ + ext-ms-win-gdi-internal-desktop-l1.dll; \ + ext-ms-win-ntuser-caret-l1.dll; \ + ext-ms-win-ntuser-dialogbox-l1.dll; \ + ext-ms-win-ntuser-draw-l1.dll; \ + ext-ms-win-ntuser-keyboard-l1.dll; \ + ext-ms-win-ntuser-gui-l1.dll; \ + ext-ms-win-ntuser-menu-l1.dll; \ + ext-ms-win-ntuser-message-l1.dll; \ + ext-ms-win-ntuser-misc-l1.dll; \ + ext-ms-win-ntuser-mouse-l1.dll; \ + ext-ms-win-ntuser-rectangle-ext-l1.dll; \ + ext-ms-win-ntuser-server-l1.dll; \ + ext-ms-win-ntuser-sysparams-ext-l1.dll; \ + ext-ms-win-ntuser-window-l1.dll; \ + ext-ms-win-rtcore-gdi-object-l1.dll; \ + ext-ms-win-rtcore-gdi-rgn-l1.dll; \ + ext-ms-win-rtcore-ntuser-cursor-l1.dll; \ + ext-ms-win-rtcore-ntuser-dc-access-l1.dll; \ + ext-ms-win-rtcore-ntuser-rawinput-l1.dll; \ + ext-ms-win-rtcore-ntuser-sysparams-l1.dll; \ + ext-ms-win-rtcore-ntuser-window-ext-l1.dll; \ + ext-ms-win-shell-shell32-l1.dll; \ + ext-ms-win-uiacore-l1.dll; \ + ext-ms-win-uxtheme-themes-l1.dll; \ + +DLOAD_ERROR_HANDLER = kernelbase + diff --git a/src/terminal/parser/ut_parser/sources.dep b/src/terminal/parser/ut_parser/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/terminal/parser/ut_parser/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/terminal/parser/ut_parser/testmd.definition b/src/terminal/parser/ut_parser/testmd.definition new file mode 100644 index 000000000..aad8a7d35 --- /dev/null +++ b/src/terminal/parser/ut_parser/testmd.definition @@ -0,0 +1,18 @@ +{ + "$schema": "http://universaltest/schema/testmddefinition-2.json", + "Package": { + "ComponentName": "Console", + "SubComponentName": "VirtualTerminal-Parser-UnitTests" + }, + "Execution": { + "Type": "TAEF", + "Parameter": "" + }, + "Dependencies": { + "Files": [ ], + "RemoteFiles": [ ], + "Packages": [ ] + }, + "Logs": [ ], + "Plugins": [ ] +} diff --git a/src/testlist/Microsoft.Console.TestLab.Desktop.testlist b/src/testlist/Microsoft.Console.TestLab.Desktop.testlist new file mode 100644 index 000000000..7d49e2b83 --- /dev/null +++ b/src/testlist/Microsoft.Console.TestLab.Desktop.testlist @@ -0,0 +1,6 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "Testlists": [ + { "FilePath": "Microsoft.Console.Tests.testlist" } + ] +} \ No newline at end of file diff --git a/src/testlist/Microsoft.Console.TestLab.OneCoreUap.testlist b/src/testlist/Microsoft.Console.TestLab.OneCoreUap.testlist new file mode 100644 index 000000000..7d49e2b83 --- /dev/null +++ b/src/testlist/Microsoft.Console.TestLab.OneCoreUap.testlist @@ -0,0 +1,6 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "Testlists": [ + { "FilePath": "Microsoft.Console.Tests.testlist" } + ] +} \ No newline at end of file diff --git a/src/testlist/Microsoft.Console.TestLab.Performance.testlist b/src/testlist/Microsoft.Console.TestLab.Performance.testlist new file mode 100644 index 000000000..4434b7d7c --- /dev/null +++ b/src/testlist/Microsoft.Console.TestLab.Performance.testlist @@ -0,0 +1,9 @@ +{ + "$schema": "http://universaltest/schema/testlist-3.json", + "TestMDs": [ + { + "FilePath": "Microsoft.Console.Host.FeatureTests.testmd", + "Profile": "Performance" + } + ] +} \ No newline at end of file diff --git a/src/testlist/Microsoft.Console.Tests.DesktopOnly.testlist b/src/testlist/Microsoft.Console.Tests.DesktopOnly.testlist new file mode 100644 index 000000000..3654542b9 --- /dev/null +++ b/src/testlist/Microsoft.Console.Tests.DesktopOnly.testlist @@ -0,0 +1,8 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs" : [ + { "FilePath": "Microsoft.Console.Host.UIAutomationTests.testmd" }, + { "FilePath": "Microsoft.Console.Host.IntegrityTests.testmd" }, + { "FilePath": "Microsoft.Console.Interactivity.Win32.UnitTests.testmd" }, + ] +} \ No newline at end of file diff --git a/src/testlist/Microsoft.Console.Tests.testlist b/src/testlist/Microsoft.Console.Tests.testlist new file mode 100644 index 000000000..14e907e28 --- /dev/null +++ b/src/testlist/Microsoft.Console.Tests.testlist @@ -0,0 +1,11 @@ +{ + "$schema": "http://universaltest/schema/testlist-2.json", + "TestMDs" : [ + { "FilePath": "Microsoft.Console.Host.UnitTests.testmd" }, + { "FilePath": "Microsoft.Console.TextBuffer.UnitTests.testmd" }, + { "FilePath": "Microsoft.Console.Host.FeatureTests.testmd" }, + { "FilePath": "Microsoft.Console.VirtualTerminal.Adapter.UnitTests.testmd" }, + { "FilePath": "Microsoft.Console.VirtualTerminal.Parser.UnitTests.testmd" }, + { "FilePath": "Microsoft.Console.Conpty.UnitTests.testmd" }, + ] +} diff --git a/src/testlist/sources b/src/testlist/sources new file mode 100644 index 000000000..1319cfbd8 --- /dev/null +++ b/src/testlist/sources @@ -0,0 +1,21 @@ +TARGETTYPE=TESTLIST + +TEST_CODE=1 + +TESTLIST_SOURCES=\ + Microsoft.Console.Tests.testlist \ + Microsoft.Console.Tests.DesktopOnly.testlist \ + Microsoft.Console.TestLab.Desktop.testlist \ + Microsoft.Console.TestLab.OneCoreUap.testlist \ + Microsoft.Console.TestLab.Performance.testlist \ + +TESTLIST_REFERENCE_DIRS=\ + $(OBJ_PATH)\..\interactivity\win32\ut_interactivity_win32\$O \ + $(OBJ_PATH)\..\host\ut_host\$O \ + $(OBJ_PATH)\..\buffer\out\ut_textbuffer\$O \ + $(OBJ_PATH)\..\host\ft_host\$O \ + $(OBJ_PATH)\..\host\ft_integrity\$O \ + $(OBJ_PATH)\..\host\ft_uia\$O \ + $(OBJ_PATH)\..\terminal\adapter\ut_adapter\$O \ + $(OBJ_PATH)\..\terminal\parser\ut_parser\$O \ + $(ONECORE_PRIVATE_PATH)\minkernel\lib\$(TARGET_DIRECTORY)\testmetadata\console \ diff --git a/src/tools/buffersize/buffersize.vcxproj b/src/tools/buffersize/buffersize.vcxproj new file mode 100644 index 000000000..4d0a270d4 --- /dev/null +++ b/src/tools/buffersize/buffersize.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {ED82003F-FC5D-4E94-8B47-F480018ED064} + Win32Proj + buffersize + buffersize + buffersize + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + \ No newline at end of file diff --git a/src/tools/buffersize/buffersize.vcxproj.filters b/src/tools/buffersize/buffersize.vcxproj.filters new file mode 100644 index 000000000..87852d306 --- /dev/null +++ b/src/tools/buffersize/buffersize.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/buffersize/main.cpp b/src/tools/buffersize/main.cpp new file mode 100644 index 000000000..6f7dfa8be --- /dev/null +++ b/src/tools/buffersize/main.cpp @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include +#include +#include +#include /* srand, rand */ +#include /* time */ + +#include +#include +#include +#include +#include +#include + +using namespace std; +//////////////////////////////////////////////////////////////////////////////// +// State +HANDLE hOut; +HANDLE hIn; + +std::string csi(std::string seq) +{ + std::string fullSeq = "\x1b["; + fullSeq += seq; + return fullSeq; +} + +void printCSI(std::string seq) +{ + printf("%s", csi(seq).c_str()); // save cursor +} + +void printCUP(int x, int y) +{ + printf("\x1b[%d;%dH", y+1, x+1); // save cursor +} + +void print256color(int bg) +{ + printf("\x1b[48;5;%dm", bg); // save cursor +} + +// bin\x64\Debug\buffersize.exe +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + hIn = GetStdHandle(STD_INPUT_HANDLE); + + DWORD dwMode = 0; + THROW_LAST_ERROR_IF(!GetConsoleMode(hOut, &dwMode)); + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + dwMode |= DISABLE_NEWLINE_AUTO_RETURN; + // THROW_LAST_ERROR_IF_FALSE(SetConsoleMode(hOut, dwMode)); + SetConsoleMode(hOut, dwMode); + // the resize event doesn't actually have the info we want. + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = !!GetConsoleScreenBufferInfoEx(hOut, &csbiex); + if (fSuccess) + { + SMALL_RECT srViewport = csbiex.srWindow; + short width = srViewport.Right - srViewport.Left + 1; + short height = srViewport.Bottom - srViewport.Top + 1; + + std::string topBorder = std::string(width, '-'); + std::string bottomBorder = std::string(width, '='); + + int color = 17; + int const colorStep = 1; + printf("Buffer size is wxh=%dx%d\n", width, height); + printCSI("s"); // save cursor + printCSI("H"); // Go Home + print256color(color); color += colorStep; + printf("%s", topBorder.c_str()); + printCUP(0, height-1); + print256color(color); color += colorStep; + printf("%s", bottomBorder.c_str()); + + for (int y = 1; y < height-1; y++) + { + printCUP(0, y); + print256color(color); color += colorStep; + printf("L"); + + printCUP(width-1, y); + print256color(color); color += colorStep; + printf("R\n"); + } + + printCSI("u"); // restore cursor + printCSI("m"); // restore color + + } + + return 0; +} diff --git a/src/tools/closetest/CloseTest.vcxproj b/src/tools/closetest/CloseTest.vcxproj new file mode 100644 index 000000000..218568f1b --- /dev/null +++ b/src/tools/closetest/CloseTest.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {C7A6A5D9-60BE-4AEB-A5F6-AFE352F86CBB} + Win32Proj + CloseTest + CloseTest + CloseTest + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + \ No newline at end of file diff --git a/src/tools/closetest/CloseTest.vcxproj.filters b/src/tools/closetest/CloseTest.vcxproj.filters new file mode 100644 index 000000000..b1657ef6d --- /dev/null +++ b/src/tools/closetest/CloseTest.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/closetest/closetest.cpp b/src/tools/closetest/closetest.cpp new file mode 100644 index 000000000..476ed3a71 --- /dev/null +++ b/src/tools/closetest/closetest.cpp @@ -0,0 +1,744 @@ +// Source: https://gist.github.com/rprichard/7ec3fe1b199f513bee82fea196a82a79 +// Date Imported: 7/11/2017 @ 2:43PM PDT by miniksa@microsoft.com + +/* +The MIT License (MIT) +Copyright (c) 2017 Ryan Prichard +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* +QUICK START: +Build with one of: + - cl /EHsc /nologo closetest.cc + - i686-w64-mingw32-g++ -Wall -static -std=c++11 closetest.cc -o closetest.exe + - x86_64-w64-mingw32-g++ -Wall -static -std=c++11 closetest.cc -o closetest.exe +Use Sysinternals DbgView to see messages generated while the test runs. +Run the test: + - Run closetest.exe: e.g.: + - Run with no arguments to see the order in which processes are signaled. + - Run `closetest.exe -d alternate --gap -n 4` to require multiple Close + button clicks. + - Observe the "closetest: child nnn: attached to console" messages in DbgView + - Click the console's Close button. + - Observe the `CTRL_CLOSE_EVENT` messages in DbgView. +DETAILS: +Use --help to see options. The program detaches from its console, creates a +new console, then spawns multiple instances of itself. Some of the children +are configured such that when another child exits, they too exit. The +dependency is implemented with either a pipe or a job object. (When A kills B, +either A holds the write-end of a pipe that B reads from, or A holds a job +object that B is assigned to.) +The test demonstrates how it can be necessary to click the console's Close +button multiple times to kill all the processes in the console, even though no +new processes start during the test. +OBSERVATIONS: +Aftering closing a console, Windows delivers a CTRL_CLOSE_EVENT event to each +attached process, giving it 5 seconds to handle it before terminating the +process. If a process-to-signal has already died, Windows apparently aborts +the signaling process, so all the other processes remain. +Different Windows versions have different behavior: + * XP: Windows signals processes from last-to-first and skips over + already-dead processes. However, it pops up a "Windows can't end this + program" dialog for every process. + * Vista and Win 7: Same as XP, but the dialogs don't appear. + * Win 8 and Win 8.1: Windows signals processes from last-to-first and aborts + on an already-dead process. + * Win 10.0.14393: v1 and v2 consoles behave the same as Win 8.1. + * Win 10.0.15063: v1 behaves as before, but v2 signals in first-to-last + order. +Use Sysinternals DbgView to see messages generated while the test runs. +EXAMPLE 1: Sample DbgView output (-d none -n 4) + Last-to-first (legacy order) + 0.04571486 [15948] closetest: child 1: attached to console + 0.08462632 [5132] closetest: child 2: attached to console + 0.09434677 [7300] closetest: child 3: attached to console + 0.13065934 [3920] closetest: child 4: attached to console + 0.13111357 [6888] closetest: attached process list: 3920, 7300, 5132, 15948, 6888 + 4.51475239 [3920] closetest: child 4: CTRL_CLOSE_EVENT received, pausing... + 4.77758837 [3920] closetest: child 4: CTRL_CLOSE_EVENT received, exiting... + 4.78069544 [7300] closetest: child 3: CTRL_CLOSE_EVENT received, pausing... + 5.03125525 [7300] closetest: child 3: CTRL_CLOSE_EVENT received, exiting... + 5.03372812 [5132] closetest: child 2: CTRL_CLOSE_EVENT received, pausing... + 5.29285145 [5132] closetest: child 2: CTRL_CLOSE_EVENT received, exiting... + 5.29587698 [15948] closetest: child 1: CTRL_CLOSE_EVENT received, pausing... + 5.55155897 [15948] closetest: child 1: CTRL_CLOSE_EVENT received, exiting... + First-to-last (order in 15063 v2 console) + 0.06677166 [7144] closetest: child 1: attached to console + 0.07975050 [4568] closetest: child 2: attached to console + 0.08957358 [1100] closetest: child 3: attached to console + 0.10003299 [7904] closetest: child 4: attached to console + 0.10047545 [816] closetest: attached process list: 816, 7144, 4568, 1100, 7904 + 4.97500944 [7144] closetest: child 1: CTRL_CLOSE_EVENT received, pausing... + 5.23319721 [7144] closetest: child 1: CTRL_CLOSE_EVENT received, exiting... + 5.23594141 [4568] closetest: child 2: CTRL_CLOSE_EVENT received, pausing... + 5.49145126 [4568] closetest: child 2: CTRL_CLOSE_EVENT received, exiting... + 5.49386692 [1100] closetest: child 3: CTRL_CLOSE_EVENT received, pausing... + 5.74885798 [1100] closetest: child 3: CTRL_CLOSE_EVENT received, exiting... + 5.75202417 [7904] closetest: child 4: CTRL_CLOSE_EVENT received, pausing... + 6.02625036 [7904] closetest: child 4: CTRL_CLOSE_EVENT received, exiting... +EXAMPLE 2: multiple close clicks required + Run closetest.exe with triggering order equal to the console signaling + order (backward for v1 or Windows <= 14393, forward for 15063 v2): + closetest.exe -d backward/forward --gap -m pipe -n 4 + The Close button must be clicked 4 times to finish killing the processes: + 0.10701734 [3776] closetest: child 1: attached to console (child 1 kills child 3) + 0.13791052 [9172] closetest: child 2: attached to console + 0.15273562 [5488] closetest: child 3: attached to console + 0.18887523 [2096] closetest: child 4: attached to console (child 4 kills child 6) + 0.20132381 [9820] closetest: child 5: attached to console + 0.24170215 [1020] closetest: child 6: attached to console + 0.25150394 [7008] closetest: child 7: attached to console (child 7 kills child 9) + 0.29081795 [9148] closetest: child 8: attached to console + 0.30123973 [6040] closetest: child 9: attached to console + 0.33976573 [15356] closetest: child 10: attached to console (child 10 kills child 12) + 0.35181633 [1068] closetest: child 11: attached to console + 0.38771760 [9028] closetest: child 12: attached to console + 0.38878006 [1880] closetest: attached process list: 1880, 3776, 9172, 5488, 2096, 9820, 1020, 7008, 9148, 6040, 15356, 1068, 9028 + 6.56705523 [3776] closetest: child 1: CTRL_CLOSE_EVENT received, pausing... + 6.81864643 [3776] closetest: child 1: CTRL_CLOSE_EVENT received, exiting... + 6.82033300 [5488] closetest: child 3: ReadFile() returned, exiting... + 6.82126617 [9172] closetest: child 2: CTRL_CLOSE_EVENT received, pausing... + 7.08446360 [9172] closetest: child 2: CTRL_CLOSE_EVENT received, exiting... + 11.20391464 [2096] closetest: child 4: CTRL_CLOSE_EVENT received, pausing... + 11.45907879 [2096] closetest: child 4: CTRL_CLOSE_EVENT received, exiting... + 11.46081257 [1020] closetest: child 6: ReadFile() returned, exiting... + 11.46188736 [9820] closetest: child 5: CTRL_CLOSE_EVENT received, pausing... + 11.72536087 [9820] closetest: child 5: CTRL_CLOSE_EVENT received, exiting... + 15.43600082 [7008] closetest: child 7: CTRL_CLOSE_EVENT received, pausing... + 15.69347382 [7008] closetest: child 7: CTRL_CLOSE_EVENT received, exiting... + 15.69517517 [6040] closetest: child 9: ReadFile() returned, exiting... + 15.69644451 [9148] closetest: child 8: CTRL_CLOSE_EVENT received, pausing... + 15.95952320 [9148] closetest: child 8: CTRL_CLOSE_EVENT received, exiting... + 19.46793747 [15356] closetest: child 10: CTRL_CLOSE_EVENT received, pausing... + 19.72507477 [15356] closetest: child 10: CTRL_CLOSE_EVENT received, exiting... + 19.72667694 [9028] closetest: child 12: ReadFile() returned, exiting... + 19.72741890 [1068] closetest: child 11: CTRL_CLOSE_EVENT received, pausing... + 19.99090767 [1068] closetest: child 11: CTRL_CLOSE_EVENT received, exiting... +EXAMPLE 3: race condition between process cleanup and close signaling + Run closetest.exe with triggering order equal to the console signaling + order (backward for v1 or Windows <= 14393, forward for 15063 v2): + closetest.exe -d backward/forward -m job -n 50 + Try to close the console. Sometimes only one process exits each time the + Close button is closed, sometimes all of them exit, and sometimes several + exit. + The `--alloc` setting defaults to 0. When I tested in a VM, increasing the + value increased the frequency at which the console got "stuck" closing + processes. At 10, it sticks every few processes. At 100, each Close click + only kills one pair of processes: + closetest.exe -d backward/forward -m job -n 2 --alloc 100 + I've reproduced this race on Win 8.1 and Win 10.0.15063.332. + Example on 15063 v2 console (closetest.exe -d forward -m job -n 10 --alloc 1): + 0.11303009 [13236] closetest: child 1: attached to console (child 1 kills child 2) + 0.14890400 [12104] closetest: child 2: attached to console + 0.16104582 [12504] closetest: child 3: attached to console (child 3 kills child 4) + 0.20047462 [13064] closetest: child 4: attached to console + 0.21102031 [9744] closetest: child 5: attached to console (child 5 kills child 6) + 0.25124130 [13380] closetest: child 6: attached to console + 0.27692872 [13016] closetest: child 7: attached to console (child 7 kills child 8) + 0.29775897 [16036] closetest: child 8: attached to console + 0.33673882 [6284] closetest: child 9: attached to console (child 9 kills child 10) + 0.34709904 [13544] closetest: child 10: attached to console + 0.36960980 [13080] closetest: child 11: attached to console (child 11 kills child 12) + 0.39724991 [13656] closetest: child 12: attached to console + 0.43287444 [13524] closetest: child 13: attached to console (child 13 kills child 14) + 0.44224671 [13716] closetest: child 14: attached to console + 0.47673470 [13820] closetest: child 15: attached to console (child 15 kills child 16) + 0.49054298 [14792] closetest: child 16: attached to console + 0.50006652 [15448] closetest: child 17: attached to console (child 17 kills child 18) + 0.53570819 [13764] closetest: child 18: attached to console + 0.54578292 [14000] closetest: child 19: attached to console (child 19 kills child 20) + 0.55962348 [14040] closetest: child 20: attached to console + 0.57240164 [16336] closetest: attached process list: 16336, 13236, 12104, 12504, 13064, 9744, 13380, 13016, 16036, 6284, 13544, 13080, 13656, 13524, 13716, 13820, 14792, 15448, 13764, 14000, 14040 + ... wait a moment before clicking Close ... + 8.84291363 [13236] closetest: child 1: CTRL_CLOSE_EVENT received, pausing... + 9.10296917 [13236] closetest: child 1: CTRL_CLOSE_EVENT received, exiting... + 9.10612774 [12504] closetest: child 3: CTRL_CLOSE_EVENT received, pausing... + 9.36894321 [12504] closetest: child 3: CTRL_CLOSE_EVENT received, exiting... + 9.37183952 [9744] closetest: child 5: CTRL_CLOSE_EVENT received, pausing... + 9.63473129 [9744] closetest: child 5: CTRL_CLOSE_EVENT received, exiting... + 9.63787746 [13016] closetest: child 7: CTRL_CLOSE_EVENT received, pausing... + 9.89983940 [13016] closetest: child 7: CTRL_CLOSE_EVENT received, exiting... + ... the close operation stops here, wait a bit before clicking Close again ... + 13.31911087 [6284] closetest: child 9: CTRL_CLOSE_EVENT received, pausing... + 13.57188797 [6284] closetest: child 9: CTRL_CLOSE_EVENT received, exiting... + 13.57480621 [13080] closetest: child 11: CTRL_CLOSE_EVENT received, pausing... + 13.83760452 [13080] closetest: child 11: CTRL_CLOSE_EVENT received, exiting... + 13.84090614 [13524] closetest: child 13: CTRL_CLOSE_EVENT received, pausing... + 14.10908413 [13524] closetest: child 13: CTRL_CLOSE_EVENT received, exiting... + 14.11201859 [13820] closetest: child 15: CTRL_CLOSE_EVENT received, pausing... + 14.36907291 [13820] closetest: child 15: CTRL_CLOSE_EVENT received, exiting... + 14.37195015 [15448] closetest: child 17: CTRL_CLOSE_EVENT received, pausing... + 14.63435650 [15448] closetest: child 17: CTRL_CLOSE_EVENT received, exiting... + 14.63759804 [14000] closetest: child 19: CTRL_CLOSE_EVENT received, pausing... + 14.89994526 [14000] closetest: child 19: CTRL_CLOSE_EVENT received, exiting... +*/ + +#ifdef _MSC_VER +#pragma comment(lib, "Shell32.lib") +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static std::wstring g_pipestr; +static HANDLE g_hLogging = INVALID_HANDLE_VALUE; +static int g_childNum; + +static const wchar_t *const kChildDivider = L"--"; +static const wchar_t *const kChildCommand_Job = L"j"; +static const wchar_t *const kChildCommand_Read = L"r"; +static const wchar_t *const kChildCommand_Hold = L"h"; + +struct PipeHandles { + HANDLE rh; + HANDLE wh; +}; + +static PipeHandles createPipe() { + PipeHandles ret {}; + const BOOL success = CreatePipe(&ret.rh, &ret.wh, nullptr, 0); + UNREFERENCED_PARAMETER(success); // to make release builds happy. + assert(success && "CreatePipe failed"); + return ret; +} + +static HANDLE makeJob() { + HANDLE job = CreateJobObjectW(nullptr, nullptr); + assert(job); + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info {}; + info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_BREAKAWAY_OK | + JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK | + JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION | + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + BOOL success = SetInformationJobObject(job, + JobObjectExtendedLimitInformation, + &info, + sizeof(info)); + UNREFERENCED_PARAMETER(success); // to make release builds happy. + assert(success && "SetInformationJobObject failed"); + return job; +} + +static std::wstring exeName() { + std::array self {}; + DWORD len = GetModuleFileNameW(nullptr, self.data(), (DWORD)self.size()); + UNREFERENCED_PARAMETER(len); // to make release builds happy. + assert(len >= 1 && len < self.size() && "GetModuleFileNameW failed"); + return self.data(); +} + +#if defined(__GNUC__) +#define PL_PRINTF_FORMAT(fmtarg, vararg) __attribute__((format(ms_printf, (fmtarg), ((vararg))))) +#else +#define PL_PRINTF_FORMAT(fmtarg, vararg) +#endif + +#define TRACE(fmt, ...) trace("closetest: " fmt, ##__VA_ARGS__) +static void trace(const char *fmt, ...) PL_PRINTF_FORMAT(1, 2); +static void trace(const char *fmt, ...) { + std::array buf; + int written = 0; + va_list ap; + va_start(ap, fmt); + written = vsnprintf(buf.data(), buf.size(), fmt, ap); + va_end(ap); + + assert(written < 4094); + + // terminate string with \r\n so remote side can see it. + buf[written] = '\r'; + buf[written + 1] = '\n'; + if (g_hLogging != INVALID_HANDLE_VALUE) + { + WriteFile(g_hLogging, buf.data(), (DWORD)written + 2, nullptr, nullptr); + } + + OutputDebugStringA(buf.data()); +} + +static std::vector getConsoleProcessList() { + std::vector ret; + ret.resize(1); + const DWORD count1 = GetConsoleProcessList(&ret[0], (DWORD)ret.size()); + assert(count1 >= 1 && "GetConsoleProcessList failed"); + ret.resize(count1); + const DWORD count2 = GetConsoleProcessList(&ret[0], (DWORD)ret.size()); + assert(count1 == count2 && "GetConsoleProcessList failed"); + return ret; +} + +static void dumpConsoleProcessList() { + std::string msg; + for (DWORD pid : getConsoleProcessList()) { + if (!msg.empty()) { + msg += ", "; + } + msg += std::to_string(pid); + } + TRACE("attached process list: %s", msg.c_str()); +} + +static std::wstring argvToCommandLine(const std::vector &argv) { + std::wstring ret; + for (const auto &arg : argv) { + // Strictly incorrect, but good enough. + if (!ret.empty()) { + ret.push_back(L' '); + } + const bool quote = arg.empty() || arg.find(L' ') != std::wstring::npos; + if (quote) { ret.push_back(L'\"'); } + ret.append(arg); + if (quote) { ret.push_back(L'\"'); } + } + return ret; +} + +static void spawnChildTree(DWORD masterPid, const std::vector &extraArgs) { + if (extraArgs.empty()) { + return; + } + + const HANDLE readyEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + assert(readyEvent != nullptr); + + std::vector argv; + argv.push_back(exeName()); + argv.push_back(L"--child"); + argv.push_back(std::to_wstring(masterPid)); + argv.push_back(std::to_wstring(GetCurrentProcessId())); + argv.push_back(std::to_wstring((uintptr_t)readyEvent)); + + if (g_hLogging != INVALID_HANDLE_VALUE) + { + argv.push_back(g_pipestr); + } + + argv.insert(argv.end(), extraArgs.begin(), extraArgs.end()); + auto cmdline = argvToCommandLine(argv); + + //TRACE("spawning: %ls", cmdline.c_str()); + + BOOL success {}; + STARTUPINFOW sui { sizeof(sui) }; + PROCESS_INFORMATION pi {}; + success = CreateProcessW(exeName().c_str(), &cmdline[0], nullptr, nullptr, FALSE, + 0, nullptr, nullptr, &sui, &pi); + assert(success && "CreateProcessW failed"); + + const DWORD waitRet = WaitForSingleObject(readyEvent, INFINITE); + assert(waitRet == WAIT_OBJECT_0 && "WaitForSingleObject failed"); + CloseHandle(readyEvent); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); +} + +// Split args up into children and spawn each child as a sibling. +static void spawnSiblings(DWORD masterPid, const std::vector &args) { + auto it = args.begin(); + while (it != args.end()) { + assert(*it == kChildDivider); + const auto itEnd = std::find(it + 1, args.end(), kChildDivider); + const std::vector child(it, itEnd); + spawnChildTree(masterPid, child); + it = itEnd; + } +} + +static void genChild(int n, const std::wstring &desc, int allocChunk, std::vector &out) { + assert(desc != kChildDivider); // A divider as the desc would break spawnSiblings's parsing. + out.push_back(kChildDivider); + out.push_back(std::to_wstring(n)); + out.push_back(desc); + out.push_back(std::to_wstring(allocChunk / 1024)); +} + +static void genBatch(bool forward, bool useJob, bool useGapProcess, int allocChunk, + std::vector &out, std::vector &handles) { + static int cnt = 1; + + const PipeHandles pipe = createPipe(); + const HANDLE job = makeJob(); + handles.push_back(pipe.rh); + handles.push_back(pipe.wh); + handles.push_back(job); + + auto genVictim = [&](int n, int /*n2*/) { + genChild(n, L"", allocChunk, out); + if (useJob) { + out.push_back(kChildCommand_Job); + out.push_back(std::to_wstring((uintptr_t)job)); + } else { + out.push_back(kChildCommand_Read); + out.push_back(std::to_wstring((uintptr_t)pipe.rh)); + } + }; + + auto genKiller = [&](int n, int n2) { + const auto desc = L"child " + std::to_wstring(n) + + L" kills child " + std::to_wstring(n2); + genChild(n, desc, allocChunk, out); + out.push_back(kChildCommand_Hold); + out.push_back(std::to_wstring((uintptr_t)(useJob ? job : pipe.wh))); + }; + + const int first = cnt; + const int gap = cnt + 1; + const int second = cnt + 1 + useGapProcess; + + if (forward) { + genKiller(first, second); + if (useGapProcess) { + genChild(gap, L"", allocChunk, out); + } + genVictim(second, first); + } else { + genVictim(first, second); + if (useGapProcess) { + genChild(gap, L"", allocChunk, out); + } + genKiller(second, first); + } + cnt += 2 + useGapProcess; +} + +static BOOL WINAPI ctrlHandler(DWORD type) { + if (type == CTRL_CLOSE_EVENT) { + TRACE("child %d: CTRL_CLOSE_EVENT received, pausing...", g_childNum); + Sleep(250); + TRACE("child %d: CTRL_CLOSE_EVENT received, exiting...", g_childNum); + return TRUE; + } + return FALSE; +} + +// Duplicate a handle from `srcProc` into a non-inheritable handle in the +// current process. +static HANDLE duplicateHandle(HANDLE srcProc, HANDLE srcHandle) { + HANDLE ret {}; + const auto success = + DuplicateHandle(srcProc, srcHandle, + GetCurrentProcess(), &ret, + 0, FALSE, DUPLICATE_SAME_ACCESS); + assert(success && "DuplicateHandle failed"); + return ret; +} + +static HANDLE openProcess(DWORD pid) { + const HANDLE ret = OpenProcess(PROCESS_DUP_HANDLE, FALSE, pid); + assert(ret != nullptr && "OpenProcess failed"); + return ret; +} + +static std::deque getCommandLine() { + // Link with Shell32.lib for CommandLineToArgvW. + std::deque ret; + const auto cmdline = GetCommandLineW(); + int argc {}; + const auto argv = CommandLineToArgvW(cmdline, &argc); + for (int i = 0; i < argc; ++i) { + ret.push_back(argv[i]); + } + LocalFree((HLOCAL)argv); + return ret; +} + +template +static T shift(std::deque &container) { + assert(!container.empty()); + T ret = container.front(); + container.pop_front(); + return ret; +} + +static int shiftInt(std::deque &container) { + return _wtoi(shift(container).c_str()); +} + +static HANDLE shiftHandle(std::deque &container) { + return (HANDLE)(uintptr_t)shiftInt(container); +} + +static int doChild(std::deque argv) { + // closetest.exe --child -- [cmd arg] [...] + shift(argv); + assert(shift(argv) == L"--child"); + const DWORD masterPid = shiftInt(argv); + const DWORD parentPid = shiftInt(argv); + HANDLE masterProc = openProcess(masterPid); + HANDLE parentProc = openProcess(parentPid); + HANDLE readyEvent = duplicateHandle(parentProc, shiftHandle(argv)); + + auto optPipeName = shift(argv); + if (optPipeName != kChildDivider) + { + std::wstring fullPipeName(L"\\\\.\\pipe\\"); + fullPipeName.append(optPipeName); + /*TRACE("child %d: log pipe %ls", fullPipeName.c_str());*/ + g_hLogging = CreateFileW(fullPipeName.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + assert(g_hLogging != INVALID_HANDLE_VALUE); + g_pipestr = optPipeName; + + const auto childKeyword = shift(argv); + assert(childKeyword == kChildDivider); + } + + g_childNum = shiftInt(argv); + const auto desc = shift(argv); + const size_t allocChunk = shiftInt(argv) * 1024; + + SetConsoleCtrlHandler(ctrlHandler, TRUE); + + TRACE("child %d: attached to console%ls", g_childNum, + desc.empty() ? L"" : (std::wstring(L" (") + desc + L")").c_str()); + + if (allocChunk > 0) { + // Slow process termination down by allocating a chunk of memory. + char *buf = new char[allocChunk]; + memset(buf, 0xcc, allocChunk); + } + + HANDLE readHandle = nullptr; + HANDLE jobHandle = nullptr; + + while (!argv.empty() && argv.front() != kChildDivider) { + const auto cmd = shift(argv); + if (cmd == kChildCommand_Hold) { + // Duplicate the handle into this process, then forget it. + duplicateHandle(masterProc, shiftHandle(argv)); + } else if (cmd == kChildCommand_Read) { + assert(readHandle == nullptr); + readHandle = duplicateHandle(masterProc, shiftHandle(argv)); + } else if (cmd == kChildCommand_Job) { + assert(jobHandle == nullptr); + jobHandle = duplicateHandle(masterProc, shiftHandle(argv)); + } else { + TRACE("Invalid child command: %ls", cmd.c_str()); + exit(1); + } + } + + spawnChildTree(masterPid, std::vector(argv.begin(), argv.end())); + + // Assign self to a job object. + if (jobHandle != nullptr) { + const BOOL success = + AssignProcessToJobObject(jobHandle, GetCurrentProcess()); + assert(success && "AssignProcessToJobObject failed"); + CloseHandle(jobHandle); + jobHandle = nullptr; + } + + CloseHandle(masterProc); + masterProc = nullptr; + CloseHandle(parentProc); + parentProc = nullptr; + + // Signal the parent once all the subprocesses are spawned. + BOOL success = SetEvent(readyEvent); + UNREFERENCED_PARAMETER(success); // to make release builds happy. + assert(success && "SetEvent failed"); + CloseHandle(readyEvent); + readyEvent = nullptr; + + if (readHandle != nullptr) { + char buf {}; + DWORD actual {}; + ReadFile(readHandle, &buf, sizeof(buf), &actual, nullptr); + TRACE("child %d: ReadFile() returned, exiting...", g_childNum); + } else { + Sleep(300 * 1000); + } + + return 0; +} + +static void usage() { + printf("usage: %ls [options]\n", exeName().c_str()); + printf("Options:\n"); + printf(" -n NUM_BATCHES Start NUM_BATCHES batches of processes [default: 4]\n"); + printf(" -d DIR Set direction of process killing\n"); + printf(" forward: early process kills later process\n"); + printf(" backward: vice versa\n"); + printf(" alternate: alternate between forward/backward\n"); + printf(" none: no triggered process killing [default]\n"); + printf(" --gap Create a gap process between killer and target\n"); + printf(" --no-gap Disable the gap process [default]\n"); + printf(" --alloc SZ Allocate an SZ MiB buffer in each child [default: 0]\n"); + printf(" -m METHOD Method used to kill processes\n"); + printf(" pipe [default]\n"); + printf(" job\n"); + printf(" --log PIPENAME Log output to a named pipe\n"); + printf(" --graph GRAPH Process graph:\n"); + printf(" tree: degenerate tree of processes [default]\n"); + printf(" list: all processes are siblings\n"); + printf(" --delay TIME Time in milliseconds to delay starting the test\n"); + printf(" --no-realloc Skip free/alloc console to break out of the initial session\n"); +} + +static int doParent(std::deque argv) { + + int numBatches = 4; + int dir = 0; + bool useJob = false; + bool useGapProcess = false; + int allocChunk = 0; + bool useSiblings = false; + bool noRealloc = false; + + // Parse arguments + shift(argv); // discard the program name. + while (!argv.empty()) { + const auto arg = shift(argv); + const auto hasNext = !argv.empty(); + + if (arg == L"--help" || arg == L"-h") { + usage(); + exit(0); + } else if (arg == L"-n" && hasNext) { + numBatches = shiftInt(argv); + } else if (arg == L"-d" && hasNext) { + const auto next = shift(argv); + if (next == L"forward") { dir = 1; } + else if (next == L"backward") { dir = 2; } + else if (next == L"alternate") { dir = 3; } + else if (next == L"none") { dir = 0; } + else { + fprintf(stderr, "error: unrecognized -d argument: %ls\n", next.c_str()); + exit(1); + } + } else if (arg == L"--gap") { + useGapProcess = true; + } else if (arg == L"--no-gap") { + useGapProcess = false; + } else if (arg == L"--alloc" && hasNext) { + const auto next = shift(argv); + allocChunk = (int)(_wtof(next.c_str()) * 1024.0 * 1024.0); + } else if (arg == L"-m" && hasNext) { + const auto next = shift(argv); + if (next == L"pipe") { useJob = false; } + else if (next == L"job") { useJob = true; } + else { + fprintf(stderr, "error: unrecognized -m argument: %ls\n", next.c_str()); + exit(1); + } + } + else if (arg == L"--graph" && hasNext) { + const auto next = shift(argv); + if (next == L"tree") { useSiblings = false; } + else if (next == L"list") { useSiblings = true; } + else { + fprintf(stderr, "error: unrecognized --graph argument: %ls\n", next.c_str()); + exit(1); + } + } + else if (arg == L"--log" && hasNext) + { + const auto next = shift(argv); + std::wstring pipename(L"\\\\.\\pipe\\"); + pipename.append(next); + + g_hLogging = CreateFileW(pipename.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (g_hLogging != INVALID_HANDLE_VALUE) + { + g_pipestr = next; + } + else + { + fprintf(stderr, "error: pipe cannot be opened: %ls\n", next.c_str()); + exit(1); + } + } + else if (arg == L"--delay" && hasNext) + { + const auto next = shift(argv); + const long ms = std::stol(next); + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } + else if (arg == L"--no-realloc") + { + noRealloc = true; + } + else { + usage(); + fprintf(stderr, "\nerror: unrecognized argument: %ls\n", arg.c_str()); + exit(1); + } + } + + // Decide which children to start. + std::vector spawnList; + std::vector handles; + for (int i = 0; i < numBatches; ++i) { + if (dir == 0) { + genChild(i + 1, L"", allocChunk, spawnList); + } + if (dir & 1) { + genBatch(true, useJob, useGapProcess, allocChunk, spawnList, handles); + } + if (dir & 2) { + genBatch(false, useJob, useGapProcess, allocChunk, spawnList, handles); + } + } + + // Spawn the children. + if (!noRealloc) + { + FreeConsole(); + AllocConsole(); + } + + if (useSiblings) { + spawnSiblings(GetCurrentProcessId(), spawnList); + } else { + spawnChildTree(GetCurrentProcessId(), spawnList); + } + for (auto h : handles) { + CloseHandle(h); + } + + // Wait until we're killed. + dumpConsoleProcessList(); + Sleep(300 * 1000); + + return 0; +} + +int main() { + setlocale(LC_ALL, ""); + auto argv = getCommandLine(); + if (argv.size() >= 2 && argv[1] == L"--child") { + return doChild(std::move(argv)); + } else { + return doParent(std::move(argv)); + } +} diff --git a/src/tools/closetest/res.rc b/src/tools/closetest/res.rc new file mode 100644 index 000000000..4ba175699 --- /dev/null +++ b/src/tools/closetest/res.rc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Close Test" +#define VER_INTERNALNAME_STR "closetest" +#define VER_ORIGINALFILENAME_STR "closetest.EXE" + + +#include + + diff --git a/src/tools/closetest/sources b/src/tools/closetest/sources new file mode 100644 index 000000000..f790284c2 --- /dev/null +++ b/src/tools/closetest/sources @@ -0,0 +1,26 @@ +MSC_WARNING_LEVEL=/W4 /WX + +TARGETNAME=closetest +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_MSVCRT=1 +USE_UNICRT=1 + +C_DEFINES=-DUNICODE + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETLIBS=\ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + +SOURCES=closetest.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +INCLUDES= \ + $(INCLUDES);$(OBJ_PATH)\$(O);$(MINWIN_PRIV_SDK_INC_PATH) diff --git a/src/tools/dirs b/src/tools/dirs new file mode 100644 index 000000000..fd72a7b1a --- /dev/null +++ b/src/tools/dirs @@ -0,0 +1,4 @@ +DIRS=nihilist \ + vtapp \ + vtpipeterm \ + integrity \ diff --git a/src/tools/echokey/ConEchoKey.vcxproj b/src/tools/echokey/ConEchoKey.vcxproj new file mode 100644 index 000000000..b55eacfc1 --- /dev/null +++ b/src/tools/echokey/ConEchoKey.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {814CBEEE-894E-4327-A6E1-740504850098} + Win32Proj + ConEchoKey + ConEchoKey + ConEchoKey + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + diff --git a/src/tools/echokey/main.cpp b/src/tools/echokey/main.cpp new file mode 100644 index 000000000..a8c294e3c --- /dev/null +++ b/src/tools/echokey/main.cpp @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define DEFINE_CONSOLEV2_PROPERTIES + +// System headers +#include + +// Standard library C-style +#include +#include +#include + +#include +using namespace std; + +// WIL +#define WIL_SUPPORT_BITOPERATION_PASCAL_NAMES +#include +#include + +bool gVtInput = false; +bool gVtOutput = true; +bool gWindowInput = false; +bool gUseAltBuffer = false; + +bool gExitRequested = false; + +HANDLE g_hOut = INVALID_HANDLE_VALUE; +HANDLE g_hIn = INVALID_HANDLE_VALUE; + +static const char CTRL_D = 0x4; + +void csi(string seq) +{ + if (!gVtOutput) return; + string fullSeq = "\x1b["; + fullSeq += seq; + printf(fullSeq.c_str()); +} + +void useAltBuffer() +{ + csi("?1049h"); +} + +void useMainBuffer() +{ + csi("?1049l"); +} + +void toPrintableBuffer(char c, char* printBuffer, int* printCch) +{ + if (c == '\x1b') + { + printBuffer[0] = '^'; + printBuffer[1] = '['; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\x03') { + printBuffer[0] = '^'; + printBuffer[1] = 'C'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\x0') + { + printBuffer[0] = '\\'; + printBuffer[1] = '0'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\r') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'r'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\n') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'n'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\t') + { + printBuffer[0] = '\\'; + printBuffer[1] = 't'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else if (c == '\b') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'b'; + printBuffer[2] = '\0'; + *printCch = 2; + } + else + { + printBuffer[0] = (char)c; + printBuffer[1] = ' '; + printBuffer[2] = '\0'; + *printCch = 2; + } + +} + +void handleKeyEvent(KEY_EVENT_RECORD keyEvent) +{ + char printBuffer[3]; + int printCch = 0; + const char c = keyEvent.uChar.AsciiChar; + toPrintableBuffer(c, printBuffer, &printCch); + + if (!keyEvent.bKeyDown) + { + // Print in grey + csi("38;5;242m"); + } + + wprintf(L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char: %hs (0x%x) KeyState: 0x%x\r\n", + keyEvent.bKeyDown, + keyEvent.wRepeatCount, + keyEvent.wVirtualKeyCode, + keyEvent.wVirtualScanCode, + printBuffer, + keyEvent.uChar.AsciiChar, + keyEvent.dwControlKeyState); + + // restore colors + csi("0m"); + + // Die on Ctrl+C + if (keyEvent.uChar.AsciiChar == CTRL_D) + { + gExitRequested = true; + } +} + +void handleWindowEvent(WINDOW_BUFFER_SIZE_RECORD windowEvent) +{ + SHORT bufferWidth = windowEvent.dwSize.X; + SHORT bufferHeight = windowEvent.dwSize.Y; + + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = !!GetConsoleScreenBufferInfoEx(g_hOut, &csbiex); + if (fSuccess) + { + SMALL_RECT srViewport = csbiex.srWindow; + + unsigned short viewX = srViewport.Left; + unsigned short viewY = srViewport.Top; + unsigned short viewWidth = srViewport.Right - srViewport.Left + 1; + unsigned short viewHeight = srViewport.Bottom - srViewport.Top + 1; + wprintf(L"BufferSize: (%d,%d) Viewport:(x, y, w, h)=(%d,%d,%d,%d)\r\n", + bufferWidth, bufferHeight, viewX, viewY, viewWidth, viewHeight); + } + +} + +BOOL CtrlHandler( DWORD fdwCtrlType ) +{ + switch( fdwCtrlType ) + { + // Handle the CTRL-C signal. + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + return true; + } + + return false; +} + +void usage() +{ + wprintf(L"usage: echokey [options]\n"); + wprintf(L"options:\n"); + wprintf(L"\t-i: enable reading VT input mode.\n"); + wprintf(L"\t-o: disable VT output.\n"); + wprintf(L"\t-w: enable reading window events.\n"); + wprintf(L"\t--alt: run in the alt buffer. Cannot be combined with `-o`\n"); + wprintf(L"\t-?: print this help message\n"); +} + +int __cdecl wmain(int argc, wchar_t* argv[]) +{ + gVtInput = false; + gVtOutput = true; + gWindowInput = false; + gUseAltBuffer = false; + gExitRequested = false; + + for(int i = 1; i < argc; i++) + { + wstring arg = wstring(argv[i]); + wprintf(L"arg=%s\n", arg.c_str()); + if (arg.compare(L"-i") == 0) + { + gVtInput = true; + wprintf(L"Using VT Input\n"); + } + else if (arg.compare(L"-w") == 0) + { + gVtInput = true; + wprintf(L"Reading Window Input\n"); + } + else if (arg.compare(L"--alt") == 0) + { + gUseAltBuffer = true; + wprintf(L"Using Alt Buffer.\n"); + } + else if (arg.compare(L"-o") == 0) + { + gVtOutput = false; + wprintf(L"Disabling VT Output\n"); + } + else if (arg.compare(L"-?") == 0) + { + usage(); + exit(0); + } + else + { + wprintf(L"Didn't recognize arg `%s`\n", arg.c_str()); + usage(); + exit(0); + } + } + + g_hOut = GetStdHandle(STD_OUTPUT_HANDLE); + g_hIn = GetStdHandle(STD_INPUT_HANDLE); + + DWORD dwOutMode = 0; + DWORD dwInMode = 0; + GetConsoleMode(g_hOut, &dwOutMode); + GetConsoleMode(g_hIn, &dwInMode); + SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE ); + const DWORD initialInMode = dwInMode; + const DWORD initialOutMode = dwOutMode; + + if (gVtOutput) + { + dwOutMode = WI_SetAllFlags(dwOutMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN); + } + + if (gVtInput) + { + dwInMode = WI_SetFlag(dwInMode, ENABLE_VIRTUAL_TERMINAL_INPUT); + } + + if (gWindowInput) + { + dwInMode = WI_SetFlag(dwInMode, ENABLE_WINDOW_INPUT); + } + + if (gUseAltBuffer && !gVtOutput) + { + wprintf(L"Specified `--alt` to use the alternate buffer with `-o`, which disables VT. --alt requires VT output to be enabled.\n"); + Sleep(2000); + exit(EXIT_FAILURE); + } + + SetConsoleMode(g_hOut, dwOutMode); + SetConsoleMode(g_hIn, dwInMode); + + if (gUseAltBuffer) + { + useAltBuffer(); + } + + wprintf(L"Start Mode (i/o):(0x%4x, 0x%4x)\n", initialInMode, initialOutMode); + wprintf(L"New Mode (i/o):(0x%4x, 0x%4x)\n", dwInMode, dwOutMode); + wprintf(L"Press ^D to exit\n"); + + while(!gExitRequested) + { + INPUT_RECORD rc; + DWORD dwRead = 0; + ReadConsoleInputA(g_hIn, &rc, 1, &dwRead); + + switch (rc.EventType) + { + case KEY_EVENT: + { + handleKeyEvent(rc.Event.KeyEvent); + break; + } + case WINDOW_BUFFER_SIZE_EVENT: + { + handleWindowEvent(rc.Event.WindowBufferSizeEvent); + break; + } + + } + } + + if (gUseAltBuffer) + { + useMainBuffer(); + } + SetConsoleMode(g_hOut, initialOutMode); + SetConsoleMode(g_hIn, initialInMode); + + exit (EXIT_FAILURE); + +} diff --git a/src/tools/echokey/res.rc b/src/tools/echokey/res.rc new file mode 100644 index 000000000..48fea858f --- /dev/null +++ b/src/tools/echokey/res.rc @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Input Key Echo Tool" +#define VER_INTERNALNAME_STR "echokey" +#define VER_ORIGINALFILENAME_STR "ECHOKEY.EXE" + + +#include + diff --git a/src/tools/echokey/sources b/src/tools/echokey/sources new file mode 100644 index 000000000..2439a4277 --- /dev/null +++ b/src/tools/echokey/sources @@ -0,0 +1,53 @@ +MSC_WARNING_LEVEL=/W4 /WX +!include $(NTMAKEENV)\system_defaultmk.inc +!include $(WINCORE_PATH)\core.inc +SOURCES_USED=$(WINCORE_PATH)\core.inc + +TARGETNAME=conechokey +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +BUILD_FOR_CORESYSTEM=1 +USE_DEFAULT_WIN32_LIBS=0 +BUFFER_OVERFLOW_CHECKS=1 + +TEST_CODE=1 +USE_MSVCRT=1 +USE_STL=1 +STL_VER=STL_VER_CURRENT +USE_IOSTREAM=1 +USE_NATIVE_EH=1 + +C_DEFINES= -DUNICODE -D_UNICODE + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETLIBS=\ + $(MINWIN_SDK_LIB_PATH)\ntdll.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore_legacy.lib \ + $(SDK_LIB_PATH)\ole32.lib \ + $(SDK_LIB_PATH)\uuid.lib \ + $(SDK_LIB_PATH)\kernel32.lib \ + $(WINDOWS_LIB_PATH)\user32p.lib \ + $(SDK_LIB_PATH)\shcore.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +PRECOMPILED_CXX=1 +PRECOMPILED_INCLUDE=precomp.h +PRECOMPILED_PCH=precomp.pch +PRECOMPILED_OBJ=precomp.obj + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + $(MINWIN_PRIV_SDK_INC_PATH); \ + $(MINCORE_PRIV_SDK_INC_PATH); \ + $(SHELL_INC_PATH); \ diff --git a/src/tools/fontlist/FontList.vcxproj b/src/tools/fontlist/FontList.vcxproj new file mode 100644 index 000000000..2e8038ba9 --- /dev/null +++ b/src/tools/fontlist/FontList.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {919544AC-D39B-463F-8414-3C3C67CF727C} + Win32Proj + FontList + FontList + FontList + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + \ No newline at end of file diff --git a/src/tools/fontlist/FontList.vcxproj.filters b/src/tools/fontlist/FontList.vcxproj.filters new file mode 100644 index 000000000..87852d306 --- /dev/null +++ b/src/tools/fontlist/FontList.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/fontlist/main.cpp b/src/tools/fontlist/main.cpp new file mode 100644 index 000000000..53a7e8166 --- /dev/null +++ b/src/tools/fontlist/main.cpp @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include + +int FontEnumForV2Console(ENUMLOGFONT *pelf, NEWTEXTMETRIC *pntm, int nFontType, LPARAM lParam); +int +AddFont( + ENUMLOGFONT *pelf, + NEWTEXTMETRIC* pntm, + int nFontType, + HDC hDC); + +// This application exists to be connected to a console session while doing exactly nothing. +// This keeps a console session alive and doesn't interfere with tests or other hooks. +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + wil::unique_hdc hdc(CreateCompatibleDC(nullptr)); + RETURN_LAST_ERROR_IF_NULL(hdc); + + LOGFONTW LogFont = { 0 }; + LogFont.lfCharSet = DEFAULT_CHARSET; + wcscpy_s(LogFont.lfFaceName, L"Terminal"); + + EnumFontFamiliesExW(hdc.get(), &LogFont, (FONTENUMPROC)FontEnumForV2Console, (LPARAM)hdc.get(), 0); + + return 0; +} + +#define CONTINUE_ENUM 1 +#define END_ENUM 0 + +#define IS_ANY_DBCS_CHARSET( CharSet ) \ + ( ((CharSet) == SHIFTJIS_CHARSET) ? TRUE : \ + ((CharSet) == HANGEUL_CHARSET) ? TRUE : \ + ((CharSet) == CHINESEBIG5_CHARSET) ? TRUE : \ + ((CharSet) == GB2312_CHARSET) ? TRUE : FALSE ) + +#define CP_US ((UINT)437) +#define CP_JPN ((UINT)932) +#define CP_WANSUNG ((UINT)949) +#define CP_TC ((UINT)950) +#define CP_SC ((UINT)936) +#define IsEastAsianCP(cp) ((cp)==CP_JPN || (cp)==CP_WANSUNG || (cp)==CP_TC || (cp)==CP_SC) + +/* +* TTPoints -- Initial font pixel heights for TT fonts +* NOTE: +* Font pixel heights for TT fonts of DBCS are the same list except +* odd point size because font width is (SBCS:DBCS != 1:2). +*/ +SHORT TTPoints[] = { + 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 24, 28, 36, 72 +}; + + +int FontEnumForV2Console(ENUMLOGFONT *pelf, NEWTEXTMETRIC *pntm, int nFontType, LPARAM lParam) +{ + UINT i; + LPCWSTR pwszFace = pelf->elfLogFont.lfFaceName; + + BOOL const fIsEastAsianCP = IsEastAsianCP(GetACP()); + + LPCWSTR pwszCharSet; + + switch (pelf->elfLogFont.lfCharSet) + { + case ANSI_CHARSET: + pwszCharSet = L"ANSI"; + break; + case CHINESEBIG5_CHARSET: + pwszCharSet = L"Chinese Big5"; + break; + case EASTEUROPE_CHARSET: + pwszCharSet = L"East Europe"; + break; + case GREEK_CHARSET: + pwszCharSet = L"Greek"; + break; + case MAC_CHARSET: + pwszCharSet = L"Mac"; + break; + case RUSSIAN_CHARSET: + pwszCharSet = L"Russian"; + break; + case SYMBOL_CHARSET: + pwszCharSet = L"Symbol"; + break; + case BALTIC_CHARSET: + pwszCharSet = L"Baltic"; + break; + case DEFAULT_CHARSET: + pwszCharSet = L"Default"; + break; + case GB2312_CHARSET: + pwszCharSet = L"Chinese GB2312"; + break; + case HANGUL_CHARSET: + pwszCharSet = L"Korean Hangul"; + break; + case OEM_CHARSET: + pwszCharSet = L"OEM"; + break; + case SHIFTJIS_CHARSET: + pwszCharSet = L"Japanese Shift-JIS"; + break; + case TURKISH_CHARSET: + pwszCharSet = L"Turkish"; + break; + default: + pwszCharSet = L"Unknown"; + break; + + } + + wprintf(L"Enum'd font: '%ls' (X: %d, Y: %d) weight 0x%lx (%d) charset %s \r\n", + pelf->elfLogFont.lfFaceName, + pelf->elfLogFont.lfWidth, + pelf->elfLogFont.lfHeight, + pelf->elfLogFont.lfWeight, + pelf->elfLogFont.lfWeight, + pwszCharSet); + + // reject non-monospaced fonts + if (!(pelf->elfLogFont.lfPitchAndFamily & FIXED_PITCH)) + { + wprintf(L"Rejecting non-monospaced font. \r\n"); + return CONTINUE_ENUM; + } + + // reject non-modern or italic TT fonts + if ((nFontType == TRUETYPE_FONTTYPE) && + (((pelf->elfLogFont.lfPitchAndFamily & 0xf0) != FF_MODERN) || + pelf->elfLogFont.lfItalic)) + { + wprintf(L"Rejecting non-FF_MODERN or Italic TrueType font.\r\n"); + return CONTINUE_ENUM; + } + + // reject non-TT fonts that aren't OEM + if ((nFontType != TRUETYPE_FONTTYPE) && + (!fIsEastAsianCP || !IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) && + (pelf->elfLogFont.lfCharSet != OEM_CHARSET)) { + wprintf(L"Rejecting raster font that isn't OEM_CHARSET.\r\n"); + return CONTINUE_ENUM; + } + + // reject fonts that are vertical + if (pwszFace[0] == TEXT('@')) + { + wprintf(L"Rejecting font face designed for vertical text.\r\n"); + return CONTINUE_ENUM; + } + + // reject non-TT fonts that aren't terminal + if ((nFontType != TRUETYPE_FONTTYPE) && (0 != lstrcmp(pwszFace, L"Terminal"))) + { + wprintf(L"Rejecting raster font that isn't 'Terminal'.\r\n"); + return CONTINUE_ENUM; + } + + // reject East Asian TT fonts that aren't East Asian charset. + if (fIsEastAsianCP && !IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) { + wprintf(L"Rejecting East Asian TrueType font that isn't marked with East Asian charsets.\r\n"); + return CONTINUE_ENUM; + } + + // reject East Asian TT fonts on non-East Asian systems + if (!fIsEastAsianCP && IS_ANY_DBCS_CHARSET(pelf->elfLogFont.lfCharSet)) + { + wprintf(L"Rejecting East Asian TrueType font when Windows non-Unicode codepage isn't from CJK country.\r\n"); + return CONTINUE_ENUM; + } + + if (nFontType & TRUETYPE_FONTTYPE) { + for (i = 0; i < ARRAYSIZE(TTPoints); i++) { + pelf->elfLogFont.lfHeight = TTPoints[i]; + + // If it's an East Asian enum, skip all odd height fonts. + if (fIsEastAsianCP && (pelf->elfLogFont.lfHeight % 2) != 0) + { + continue; + } + + pelf->elfLogFont.lfWidth = 0; + pelf->elfLogFont.lfWeight = pntm->tmWeight; + AddFont(pelf, pntm, nFontType, (HDC)lParam); + } + } + else { + AddFont(pelf, pntm, nFontType, (HDC)lParam); + } + + return CONTINUE_ENUM; // and continue enumeration +} + +// Routine Description: +// - Add the font described by the LOGFONT structure to the font table if +// it's not already there. +int +AddFont( + ENUMLOGFONT *pelf, + NEWTEXTMETRIC* /*pntm*/, + int /*nFontType*/, + HDC hDC) +{ + wil::unique_hfont hfont(CreateFontIndirectW(&pelf->elfLogFont)); + RETURN_LAST_ERROR_IF_NULL(hfont); + + hfont.reset(SelectFont(hDC, hfont.release())); + + TEXTMETRIC tm; + GetTextMetricsW(hDC, &tm); + + SIZE sz; + GetTextExtentPoint32W(hDC, L"0", 1, &sz); + + wprintf(L" Actual Size: (X: %d, Y: %d)\r\n", sz.cx, tm.tmHeight + tm.tmExternalLeading); + + // restore original DC font + hfont.reset(SelectFont(hDC, hfont.release())); + + return CONTINUE_ENUM; +} diff --git a/src/tools/integrity/dirs b/src/tools/integrity/dirs new file mode 100644 index 000000000..9e6329ab9 --- /dev/null +++ b/src/tools/integrity/dirs @@ -0,0 +1,4 @@ +DIRS=exeuwp \ + exewin32 \ + lib \ + packageuwp \ diff --git a/src/tools/integrity/exeuwp/DefaultResource.rc b/src/tools/integrity/exeuwp/DefaultResource.rc new file mode 100644 index 000000000..b806c71bb --- /dev/null +++ b/src/tools/integrity/exeuwp/DefaultResource.rc @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_UNKNOWN +#define VER_FILESUBTYPE VFT2_UNKNOWN +#define VER_FILEDESCRIPTION_STR ___TARGETNAME +#define VER_INTERNALNAME_STR ___TARGETNAME +#define VER_ORIGINALFILENAME_STR ___TARGETNAME + +#include "common.ver" diff --git a/src/tools/integrity/exeuwp/consoleuwp.cpp b/src/tools/integrity/exeuwp/consoleuwp.cpp new file mode 100644 index 000000000..d2c3c36b9 --- /dev/null +++ b/src/tools/integrity/exeuwp/consoleuwp.cpp @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include +#include + +#include "util.h" + +#pragma optimize("", off) +#pragma warning(disable: 4748) + +int __cdecl wmain(int /*argc*/, __in_ecount(argc) PCWSTR* /*argv*/) +{ + TestLibFunc(); + + return 0; +} diff --git a/src/tools/integrity/exeuwp/sources b/src/tools/integrity/exeuwp/sources new file mode 100644 index 000000000..6bb1d0a60 --- /dev/null +++ b/src/tools/integrity/exeuwp/sources @@ -0,0 +1,39 @@ +TARGETNAME=conintegrityUWP +TARGETTYPE=PROGRAM + +C_DEFINES = $(C_DEFINES) -DUNICODE -D_UNICODE + +MSC_WARNING_LEVEL = /W4 /WX + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TEST_CODE=1 +UMTYPE=console +UMENTRY=wmain +USE_MSVCRT=1 +USE_UNICRT=1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + +INCLUDES= \ + $(INCLUDES); \ + $(PROJECT_PRIV_INC_PATH); \ + $(SHELL_INC_PATH); \ + ..\..\..\inc; \ + ..\lib; \ + +TARGETLIBS= \ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore_legacy.lib \ + $(SDK_LIB_PATH)\onecoreuap.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\tools\integrity\lib\$(O)\conintegritylib.lib \ + +SOURCES= \ + ConsoleUWP.cpp \ + DefaultResource.rc # Autogenerated file name + version for Device Guard whitelisting effort + +# Autogenerated. Sets file name for Device Guard whitelisting effort, used in RC.exe. +C_DEFINES=$(C_DEFINES) -D___TARGETNAME="""$(TARGETNAME).$(TARGETTYPE)""" +MUI_VERIFY_NO_LOC_RESOURCE=1 diff --git a/src/tools/integrity/exewin32/Sources.pkg.xml b/src/tools/integrity/exewin32/Sources.pkg.xml new file mode 100644 index 000000000..b766c235a --- /dev/null +++ b/src/tools/integrity/exewin32/Sources.pkg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/integrity/exewin32/main.cpp b/src/tools/integrity/exewin32/main.cpp new file mode 100644 index 000000000..108ea1aa1 --- /dev/null +++ b/src/tools/integrity/exewin32/main.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include "util.h" + +// This application exists to be connected to a console session while doing exactly nothing. +// This keeps a console session alive and doesn't interfere with tests or other hooks. +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + TestLibFunc(); + + return 0; +} diff --git a/src/tools/integrity/exewin32/product.pbxproj b/src/tools/integrity/exewin32/product.pbxproj new file mode 100644 index 000000000..c3c52d2ae --- /dev/null +++ b/src/tools/integrity/exewin32/product.pbxproj @@ -0,0 +1,13 @@ + + + + + false + + + + LegacyTestPackage + + + + \ No newline at end of file diff --git a/src/tools/integrity/exewin32/res.rc b/src/tools/integrity/exewin32/res.rc new file mode 100644 index 000000000..014eec8ad --- /dev/null +++ b/src/tools/integrity/exewin32/res.rc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Integrity Test Helper" +#define VER_INTERNALNAME_STR "conintegrity" +#define VER_ORIGINALFILENAME_STR "conintegrity.EXE" + + +#include + + diff --git a/src/tools/integrity/exewin32/sources b/src/tools/integrity/exewin32/sources new file mode 100644 index 000000000..b9efd10f5 --- /dev/null +++ b/src/tools/integrity/exewin32/sources @@ -0,0 +1,37 @@ +MSC_WARNING_LEVEL=/W4 /WX + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETNAME=conintegrity +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_MSVCRT=1 +USE_UNICRT=1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + + +C_DEFINES=-DUNICODE + +TARGETLIBS=\ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(WINCORE_OBJ_PATH)\console\open\src\tools\integrity\lib\$(O)\conintegritylib.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=unittests + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + ..\lib + +SPKG_SOURCES = A LegacyTestPackage entry in product.pbxproj is now required for each pkg.xml file to build packages. This exact string triggers PASS2, do not alter it. diff --git a/src/tools/integrity/exewin32/sources.dep b/src/tools/integrity/exewin32/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/tools/integrity/exewin32/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/tools/integrity/lib/sources b/src/tools/integrity/lib/sources new file mode 100644 index 000000000..410c83e32 --- /dev/null +++ b/src/tools/integrity/lib/sources @@ -0,0 +1,26 @@ +MSC_WARNING_LEVEL=/W4 /WX + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETNAME=conintegritylib +TARGETTYPE=LIBRARY + +TEST_CODE=1 +USE_MSVCRT=1 +USE_UNICRT=1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + + +C_DEFINES=-DUNICODE + +SOURCES=util.cpp \ + +INCLUDES= \ + ..; \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + $(MINCORE_INTERNAL_PRIV_SDK_INC_PATH_L); \ + + diff --git a/src/tools/integrity/lib/util.cpp b/src/tools/integrity/lib/util.cpp new file mode 100644 index 000000000..3273b63ea --- /dev/null +++ b/src/tools/integrity/lib/util.cpp @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include "util.h" +#include +#include +#include + +PCWSTR GetIntegrityLevel() +{ + DWORD dwIntegrityLevel = 0; + + // Get the Integrity level. + wistd::unique_ptr tokenLabel; + THROW_IF_FAILED(wil::GetTokenInformationNoThrow(tokenLabel, GetCurrentProcessToken())); + + dwIntegrityLevel = *GetSidSubAuthority(tokenLabel->Label.Sid, + (DWORD)(UCHAR)(*GetSidSubAuthorityCount(tokenLabel->Label.Sid) - 1)); + + switch (dwIntegrityLevel) + { + case SECURITY_MANDATORY_LOW_RID: + return L"Low Integrity\r\n"; + case SECURITY_MANDATORY_MEDIUM_RID: + return L"Medium Integrity\r\n"; + case SECURITY_MANDATORY_HIGH_RID: + return L"High Integrity\r\n"; + case SECURITY_MANDATORY_SYSTEM_RID: + return L"System Integrity\r\n"; + default: + return L"UNKNOWN INTEGRITY\r\n"; + } +} + +void WriteToConsole(_In_ PCWSTR pwszText) +{ + DWORD dwWritten = 0; + WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), + pwszText, + (DWORD)wcslen(pwszText), + &dwWritten, + nullptr); +} + +void FormatToConsole(_In_ PCWSTR pwszFunc, const BOOL bResult, const DWORD dwError) +{ + std::unique_ptr pwszBuffer = std::make_unique(MAX_PATH); + THROW_IF_FAILED(StringCchPrintfW(pwszBuffer.get(), MAX_PATH, L"%s;%d;%d\r\n", pwszFunc, bResult, dwError)); + WriteToConsole(pwszBuffer.get()); +} + +PCWSTR TryReadConsoleOutputW(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cCharInfos = 1; + std::unique_ptr rgCharInfos = std::make_unique(cCharInfos); + COORD coordBuffer; + coordBuffer.X = 1; + coordBuffer.Y = 1; + + COORD coordRead = { 0 }; + SMALL_RECT srReadRegion = { 0 }; + + SetLastError(0); + *pbResult = ReadConsoleOutputW(GetStdHandle(STD_OUTPUT_HANDLE), + rgCharInfos.get(), + coordBuffer, + coordRead, + &srReadRegion); + *pdwError = GetLastError(); + return L"RCOW"; +} + +PCWSTR TryReadConsoleOutputA(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cCharInfos = 1; + std::unique_ptr rgCharInfos = std::make_unique(cCharInfos); + COORD coordBuffer; + coordBuffer.X = 1; + coordBuffer.Y = 1; + + COORD coordRead = { 0 }; + SMALL_RECT srReadRegion = { 0 }; + + SetLastError(0); + *pbResult = ReadConsoleOutputA(GetStdHandle(STD_OUTPUT_HANDLE), + rgCharInfos.get(), + coordBuffer, + coordRead, + &srReadRegion); + *pdwError = GetLastError(); + return L"RCOA"; +} + +PCWSTR TryReadConsoleOutputCharacterW(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cchTest = 1; + std::unique_ptr pwszTest = std::make_unique(cchTest); + COORD coordRead = { 0 }; + DWORD dwRead = 0; + + SetLastError(0); + *pbResult = ReadConsoleOutputCharacterW(GetStdHandle(STD_OUTPUT_HANDLE), + pwszTest.get(), + cchTest, + coordRead, + &dwRead); + *pdwError = GetLastError(); + return L"RCOCW"; +} + +PCWSTR TryReadConsoleOutputCharacterA(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cchTest = 1; + std::unique_ptr pszTest = std::make_unique(cchTest); + COORD coordRead = { 0 }; + DWORD dwRead = 0; + + SetLastError(0); + *pbResult = ReadConsoleOutputCharacterA(GetStdHandle(STD_OUTPUT_HANDLE), + pszTest.get(), + cchTest, + coordRead, + &dwRead); + *pdwError = GetLastError(); + return L"RCOCA"; +} + +PCWSTR TryReadConsoleOutputAttribute(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cchTest = 1; + std::unique_ptr rgwTest = std::make_unique(cchTest); + COORD coordRead = { 0 }; + DWORD dwRead = 0; + + SetLastError(0); + *pbResult = ReadConsoleOutputAttribute(GetStdHandle(STD_OUTPUT_HANDLE), + rgwTest.get(), + cchTest, + coordRead, + &dwRead); + *pdwError = GetLastError(); + return L"RCOAttr"; +} + +PCWSTR TryWriteConsoleInputW(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cInputRecords = 1; + std::unique_ptr rgInputRecords = std::make_unique(cInputRecords); + rgInputRecords[0].EventType = KEY_EVENT; + rgInputRecords[0].Event.KeyEvent.bKeyDown = TRUE; + rgInputRecords[0].Event.KeyEvent.dwControlKeyState = 0; + rgInputRecords[0].Event.KeyEvent.uChar.UnicodeChar = L'A'; + rgInputRecords[0].Event.KeyEvent.wRepeatCount = 1; + rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode = L'A'; + rgInputRecords[0].Event.KeyEvent.wVirtualScanCode = L'A'; + + DWORD dwWritten = 0; + + SetLastError(0); + *pbResult = WriteConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), + rgInputRecords.get(), + cInputRecords, + &dwWritten); + *pdwError = GetLastError(); + return L"WCIW"; +} + +PCWSTR TryWriteConsoleInputA(_Out_ BOOL* const pbResult, + _Out_ DWORD* const pdwError) +{ + DWORD cInputRecords = 1; + std::unique_ptr rgInputRecords = std::make_unique(cInputRecords); + rgInputRecords[0].EventType = KEY_EVENT; + rgInputRecords[0].Event.KeyEvent.bKeyDown = TRUE; + rgInputRecords[0].Event.KeyEvent.dwControlKeyState = 0; + rgInputRecords[0].Event.KeyEvent.uChar.AsciiChar = 'A'; + rgInputRecords[0].Event.KeyEvent.wRepeatCount = 1; + rgInputRecords[0].Event.KeyEvent.wVirtualKeyCode = L'A'; + rgInputRecords[0].Event.KeyEvent.wVirtualScanCode = L'A'; + + DWORD dwWritten = 0; + + SetLastError(0); + *pbResult = WriteConsoleInputA(GetStdHandle(STD_INPUT_HANDLE), + rgInputRecords.get(), + cInputRecords, + &dwWritten); + *pdwError = GetLastError(); + return L"WCIA"; +} + +bool TestLibFunc() +{ + WriteToConsole(GetIntegrityLevel()); + + PCWSTR pwszFuncName; + BOOL bResult; + DWORD dwError; + + pwszFuncName = TryReadConsoleOutputW(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryReadConsoleOutputA(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryReadConsoleOutputCharacterW(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryReadConsoleOutputCharacterA(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryReadConsoleOutputAttribute(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryWriteConsoleInputA(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + pwszFuncName = TryWriteConsoleInputW(&bResult, &dwError); + FormatToConsole(pwszFuncName, bResult, dwError); + + return true; +} diff --git a/src/tools/integrity/lib/util.h b/src/tools/integrity/lib/util.h new file mode 100644 index 000000000..0a8295e6b --- /dev/null +++ b/src/tools/integrity/lib/util.h @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +bool TestLibFunc(); diff --git a/src/tools/integrity/packageuwp/ConsoleUWP.appxSources b/src/tools/integrity/packageuwp/ConsoleUWP.appxSources new file mode 100644 index 000000000..d34c99d78 --- /dev/null +++ b/src/tools/integrity/packageuwp/ConsoleUWP.appxSources @@ -0,0 +1,10 @@ +APPX_MANIFEST = Package.AppxManifest + +APPX_FILES = \ + { Assets\logo.scale-100.png = Assets\logo.scale-100.png } \ + { Assets\smalllogo.scale-100.png = Assets\smalllogo.scale-100.png } \ + { Assets\splashscreen.scale-100.png = Assets\splashscreen.scale-100.png } \ + { Assets\storelogo.scale-100.png = Assets\storelogo.scale-100.png } \ + +APPX_PACKAGE_FILES = \ + { $(OBJ_PATH)\..\exeuwp\$(O)\conintegrityUWP.exe = conintegrityUWP.exe } \ diff --git a/src/tools/integrity/packageuwp/Sources.pkg.xml b/src/tools/integrity/packageuwp/Sources.pkg.xml new file mode 100644 index 000000000..cbc5dd645 --- /dev/null +++ b/src/tools/integrity/packageuwp/Sources.pkg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/integrity/packageuwp/assets/AppList.scale-100.png b/src/tools/integrity/packageuwp/assets/AppList.scale-100.png new file mode 100644 index 000000000..92364765e Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/AppList.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/AppList.targetsize-16.png b/src/tools/integrity/packageuwp/assets/AppList.targetsize-16.png new file mode 100644 index 000000000..1ffb2f09d Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/AppList.targetsize-16.png differ diff --git a/src/tools/integrity/packageuwp/assets/AppList.targetsize-256.png b/src/tools/integrity/packageuwp/assets/AppList.targetsize-256.png new file mode 100644 index 000000000..24cad4a27 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/AppList.targetsize-256.png differ diff --git a/src/tools/integrity/packageuwp/assets/AppList.targetsize-32.png b/src/tools/integrity/packageuwp/assets/AppList.targetsize-32.png new file mode 100644 index 000000000..edf5096ba Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/AppList.targetsize-32.png differ diff --git a/src/tools/integrity/packageuwp/assets/AppList.targetsize-48.png b/src/tools/integrity/packageuwp/assets/AppList.targetsize-48.png new file mode 100644 index 000000000..a66326135 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/AppList.targetsize-48.png differ diff --git a/src/tools/integrity/packageuwp/assets/Default_User_Tile.scale-100.png b/src/tools/integrity/packageuwp/assets/Default_User_Tile.scale-100.png new file mode 100644 index 000000000..59c8fa468 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/Default_User_Tile.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/LargeLogo.scale-100.png b/src/tools/integrity/packageuwp/assets/LargeLogo.scale-100.png new file mode 100644 index 000000000..ee84b7da0 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/LargeLogo.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/Logo.scale-100.png b/src/tools/integrity/packageuwp/assets/Logo.scale-100.png new file mode 100644 index 000000000..323b26d4d Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/Logo.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/SmallLogo.scale-100.png b/src/tools/integrity/packageuwp/assets/SmallLogo.scale-100.png new file mode 100644 index 000000000..0b195a6c2 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/SmallLogo.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/SplashScreen.scale-100.png b/src/tools/integrity/packageuwp/assets/SplashScreen.scale-100.png new file mode 100644 index 000000000..fc70b858a Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/SplashScreen.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/StoreLogo.scale-100.png b/src/tools/integrity/packageuwp/assets/StoreLogo.scale-100.png new file mode 100644 index 000000000..57dfbb0d3 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/StoreLogo.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/assets/WideLogo.scale-100.png b/src/tools/integrity/packageuwp/assets/WideLogo.scale-100.png new file mode 100644 index 000000000..bdc2dcb08 Binary files /dev/null and b/src/tools/integrity/packageuwp/assets/WideLogo.scale-100.png differ diff --git a/src/tools/integrity/packageuwp/package.appxmanifest b/src/tools/integrity/packageuwp/package.appxmanifest new file mode 100644 index 000000000..9b35c6ce2 --- /dev/null +++ b/src/tools/integrity/packageuwp/package.appxmanifest @@ -0,0 +1,55 @@ + + + + + + ConsoleIntegrityUWP + Console Team + A console UWP application for testing console subsystem integrity level interactions + Assets\StoreLogo.scale-100.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/integrity/packageuwp/product.pbxproj b/src/tools/integrity/packageuwp/product.pbxproj new file mode 100644 index 000000000..c3c52d2ae --- /dev/null +++ b/src/tools/integrity/packageuwp/product.pbxproj @@ -0,0 +1,13 @@ + + + + + false + + + + LegacyTestPackage + + + + \ No newline at end of file diff --git a/src/tools/integrity/packageuwp/sources b/src/tools/integrity/packageuwp/sources new file mode 100644 index 000000000..56640c540 --- /dev/null +++ b/src/tools/integrity/packageuwp/sources @@ -0,0 +1,11 @@ +TARGETTYPE=APPX +TARGETNAME=ConsoleIntegrityUWP +TARGET_DESTINATION=UnitTests +CODE_SIGN_EKU=CS + +TEST_CODE=1 + +SOURCES=\ + ConsoleUWP.appxsources + +SPKG_SOURCES = A LegacyTestPackage entry in product.pbxproj is now required for each pkg.xml file to build packages. This exact string triggers PASS2, do not alter it. diff --git a/src/tools/integrity/packageuwp/sources.dep b/src/tools/integrity/packageuwp/sources.dep new file mode 100644 index 000000000..f3f583614 --- /dev/null +++ b/src/tools/integrity/packageuwp/sources.dep @@ -0,0 +1,6 @@ +BUILD_PASS2_CONSUMES= \ + onecore\windows\core\console\open\src\tools\integrity\exeuwp|PASS2 \ + +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ + diff --git a/src/tools/lnkd/lnkd.bat b/src/tools/lnkd/lnkd.bat new file mode 100644 index 000000000..148906698 --- /dev/null +++ b/src/tools/lnkd/lnkd.bat @@ -0,0 +1 @@ +%_NTTREE%\lnkd.exe %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/tools/lnkd/main.cpp b/src/tools/lnkd/main.cpp new file mode 100644 index 000000000..be38372b7 --- /dev/null +++ b/src/tools/lnkd/main.cpp @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +void PrintUsage() +{ + wprintf(L"lnkd usage:\n"); + wprintf(L"\tlnkd \n"); +} + +HRESULT GetPropertyBoolValue(_In_ IPropertyStore *pPropStore, _In_ REFPROPERTYKEY refPropKey, _Out_ BOOL *pfValue) +{ + PROPVARIANT propvar; + HRESULT hr = pPropStore->GetValue(refPropKey, &propvar); + if (SUCCEEDED(hr)) + { + hr = PropVariantToBoolean(propvar, pfValue); + } + + return hr; +} + +HRESULT GetPropertyByteValue(_In_ IPropertyStore *pPropStore, _In_ REFPROPERTYKEY refPropKey, _Out_ BYTE *pbValue) +{ + PROPVARIANT propvar; + HRESULT hr = pPropStore->GetValue(refPropKey, &propvar); + if (SUCCEEDED(hr)) + { + SHORT sValue; + hr = PropVariantToInt16(propvar, &sValue); + if (SUCCEEDED(hr)) + { + hr = (sValue >= 0 && sValue <= BYTE_MAX) ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) + { + *pbValue = (BYTE)sValue; + } + } + } + + return hr; +} + +void DumpV2Properties(_In_ IShellLink *pslConsole) +{ + IPropertyStore *pPropStoreLnk; + HRESULT hr = pslConsole->QueryInterface(IID_PPV_ARGS(&pPropStoreLnk)); + if (SUCCEEDED(hr)) + { + wprintf(L"V2 Properties:\n"); + BOOL fForceV2; + hr = GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_ForceV2, &fForceV2); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_ForceV2: %s\n", (fForceV2) ? L"true" : L"false"); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_ForceV2. (HRESULT: 0x%08x)\n", hr); + } + + BOOL fWrapText; + hr = GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_WrapText, &fWrapText); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_WrapText: %s\n", (fWrapText) ? L"true" : L"false"); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_WrapText. (HRESULT: 0x%08x)\n", hr); + } + + BOOL fFilterOnPaste; + hr = GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_FilterOnPaste, &fFilterOnPaste); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_FilterOnPaste: %s\n", (fFilterOnPaste) ? L"true" : L"false"); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_FilterOnPaste. (HRESULT: 0x%08x)\n", hr); + } + + BOOL fCtrlKeyShortcutsDisabled; + hr = GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_CtrlKeyShortcutsDisabled, &fCtrlKeyShortcutsDisabled); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_CtrlKeyShortcutsDisabled: %s\n", (fCtrlKeyShortcutsDisabled) ? L"true" : L"false"); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_CtrlKeyShortcutsDisabled. (HRESULT: 0x%08x)\n", hr); + } + + BOOL fLineSelection; + hr = GetPropertyBoolValue(pPropStoreLnk, PKEY_Console_LineSelection, &fLineSelection); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_LineSelection: %s\n", (fLineSelection) ? L"true" : L"false"); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_LineSelection. (HRESULT: 0x%08x)\n", hr); + } + + BYTE bWindowTransparency; + hr = GetPropertyByteValue(pPropStoreLnk, PKEY_Console_WindowTransparency, &bWindowTransparency); + if (SUCCEEDED(hr)) + { + wprintf(L"\tPKEY_Console_WindowTransparency: %d\n", bWindowTransparency); + } + else + { + wprintf(L"ERROR: Unable to retreive value of PKEY_Console_WindowTransparency. (HRESULT: 0x%08x)\n", hr); + } + + pPropStoreLnk->Release(); + } +} + +void DumpCoord(_In_ PCWSTR pszAttrName, const COORD coord) +{ + wprintf(L"\t%s: (%d, %d) (0x%x)\n", + pszAttrName, + coord.X, + coord.Y, + coord); +} + +void DumpBool(_In_ PCWSTR pszAttrName, const BOOL fEnabled) +{ + wprintf(L"\t%s: %s\n", pszAttrName, fEnabled ? L"true" : L"false"); +} + +HRESULT DumpV1Properties(_In_ IShellLink *pslConsole) +{ + IShellLinkDataList *pConsoleLnkDataList; + HRESULT hr = pslConsole->QueryInterface(IID_PPV_ARGS(&pConsoleLnkDataList)); + if (SUCCEEDED(hr)) + { + NT_CONSOLE_PROPS *pNtConsoleProps = nullptr; + hr = pConsoleLnkDataList->CopyDataBlock(NT_CONSOLE_PROPS_SIG, (void**)&pNtConsoleProps); + if (SUCCEEDED(hr)) + { + wprintf(L"V1 Properties:\n"); + wprintf(L"\twFillAttribute: %x\n", pNtConsoleProps->wFillAttribute); + wprintf(L"\twPopupFillAttribute: %x\n", pNtConsoleProps->wPopupFillAttribute); + DumpCoord(L"dwScreenBufferSize", pNtConsoleProps->dwScreenBufferSize); + DumpCoord(L"dwWindowSize", pNtConsoleProps->dwWindowSize); + DumpCoord(L"dwWindowOrigin", pNtConsoleProps->dwWindowOrigin); + wprintf(L"\tnFont: %x\n", pNtConsoleProps->nFont); + wprintf(L"\tnInputBufferSize: %x\n", pNtConsoleProps->nInputBufferSize); + DumpCoord(L"dwWindowOrigin", pNtConsoleProps->dwWindowOrigin); + DumpCoord(L"dwFontSize", pNtConsoleProps->dwFontSize); + wprintf(L"\tuFontFamily: %d\n", pNtConsoleProps->uFontFamily); + wprintf(L"\tuFontWeight: %d\n", pNtConsoleProps->uFontWeight); + wprintf(L"\tFaceName: \"%s\"\n", pNtConsoleProps->FaceName); + wprintf(L"\tuCursorSize: %d\n", pNtConsoleProps->uCursorSize); + DumpBool(L"bFullScreen", pNtConsoleProps->bFullScreen); + DumpBool(L"bQuickEdit", pNtConsoleProps->bQuickEdit); + DumpBool(L"bInsertMode", pNtConsoleProps->bInsertMode); + DumpBool(L"bAutoPosition", pNtConsoleProps->bAutoPosition); + wprintf(L"\tuHistoryBufferSize: %d\n", pNtConsoleProps-> uHistoryBufferSize); + wprintf(L"\tuNumberOfHistoryBuffers: %d\n", pNtConsoleProps->uNumberOfHistoryBuffers); + DumpBool(L"bHistoryNoDup", pNtConsoleProps->bHistoryNoDup); + wprintf(L"\tColorTable:\n"); + for (int iCurrColor = 0; iCurrColor < 16; iCurrColor++) + { + wprintf(L"\t\t%d:\t(R:%d\tG:\t%d\tB:\t%d)\n", + iCurrColor, + GetRValue(pNtConsoleProps->ColorTable[iCurrColor]), + GetGValue(pNtConsoleProps->ColorTable[iCurrColor]), + GetBValue(pNtConsoleProps->ColorTable[iCurrColor])); + } + LocalFree(pNtConsoleProps); + } + + if (SUCCEEDED(hr)) + { + // now dump East Asian properties if we can + NT_FE_CONSOLE_PROPS *pNtFEConsoleProps; + if (SUCCEEDED(pConsoleLnkDataList->CopyDataBlock(NT_FE_CONSOLE_PROPS_SIG, + (void**)&pNtFEConsoleProps))) + { + wprintf(L"\tuCodePage: %d", pNtFEConsoleProps->uCodePage); + LocalFree(pNtFEConsoleProps); + } + else + { + wprintf(L"\t.lnk doesn't contain an explicit codepage setting.\n"); + } + } + + pConsoleLnkDataList->Release(); + } + + return hr; +} + +HRESULT DumpProperties(_In_ PCWSTR pszLnkFile) +{ + IShellLink *pslConsole; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC, IID_PPV_ARGS(&pslConsole)); + if (SUCCEEDED(hr)) + { + IPersistFile *pPersistFileLnk; + hr = pslConsole->QueryInterface(IID_PPV_ARGS(&pPersistFileLnk)); + if (SUCCEEDED(hr)) + { + hr = pPersistFileLnk->Load(pszLnkFile, 0 /*grfMode*/); + if (SUCCEEDED(hr)) + { + hr = DumpV1Properties(pslConsole); + if (SUCCEEDED(hr)) + { + wprintf(L"\n"); + DumpV2Properties(pslConsole); + } + else if (hr == E_FAIL) + { + wprintf(L"ERROR: .lnk file does not contain console properties.\n"); + } + } + else + { + wprintf(L"ERROR: Failed to load from lnk file (HRESULT: 0x%08x)\n", hr); + } + + pPersistFileLnk->Release(); + } + + pslConsole->Release(); + } + + return hr; +} + +int __cdecl wmain(int argc, WCHAR* argv[]) +{ + if (argc == 2) + { + PCWSTR pszLnkFile = argv[1]; + wprintf(L"%s: Dumping lnk details for \"%s\"\n\n", PathFindFileName(argv[0]), pszLnkFile); + if (PathFileExists(pszLnkFile)) + { + HRESULT hr = CoInitialize(NULL); + if (SUCCEEDED(hr)) + { + hr = DumpProperties(pszLnkFile); + CoUninitialize(); + } + } + else + { + wprintf(L"ERROR: Unable to open file: \"%s\". File does not exist.\n", pszLnkFile); + return 1; + } + } + else + { + PrintUsage(); + } + return 0; +} diff --git a/src/tools/lnkd/precomp.h b/src/tools/lnkd/precomp.h new file mode 100644 index 000000000..62cf38703 --- /dev/null +++ b/src/tools/lnkd/precomp.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define DEFINE_CONSOLEV2_PROPERTIES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" diff --git a/src/tools/lnkd/res.rc b/src/tools/lnkd/res.rc new file mode 100644 index 000000000..98a57d1c3 --- /dev/null +++ b/src/tools/lnkd/res.rc @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Lnk File Dumper" +#define VER_INTERNALNAME_STR "lnkd" +#define VER_ORIGINALFILENAME_STR "LNKD.EXE" + + +#include + diff --git a/src/tools/lnkd/sources b/src/tools/lnkd/sources new file mode 100644 index 000000000..9f671d262 --- /dev/null +++ b/src/tools/lnkd/sources @@ -0,0 +1,37 @@ +MSC_WARNING_LEVEL=/W4 /WX +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 +!include $(NTMAKEENV)\system_defaultmk.inc +!include $(WINCORE_PATH)\core.inc +SOURCES_USED=$(WINCORE_PATH)\core.inc + +TARGETNAME=lnkd +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_MSVCRT=1 + +C_DEFINES=-DUNICODE + +TARGETLIBS=\ + $(MINWIN_SDK_LIB_PATH)\ntdll.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore_legacy.lib \ + $(SDK_LIB_PATH)\ole32.lib \ + $(SDK_LIB_PATH)\uuid.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +PRECOMPILED_CXX=1 +PRECOMPILED_INCLUDE=precomp.h +PRECOMPILED_PCH=precomp.pch +PRECOMPILED_OBJ=precomp.obj + +INCLUDES= \ + $(INCLUDES);$(OBJ_PATH)\$(O);$(MINWIN_PRIV_SDK_INC_PATH) diff --git a/src/tools/nihilist/Nihilist.vcxproj b/src/tools/nihilist/Nihilist.vcxproj new file mode 100644 index 000000000..8a9c59936 --- /dev/null +++ b/src/tools/nihilist/Nihilist.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {FC802440-AD6A-4919-8F2C-7701F2B38D79} + Win32Proj + Nihilist + Nihilist + Nihilist + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + diff --git a/src/tools/nihilist/Nihilist.vcxproj.filters b/src/tools/nihilist/Nihilist.vcxproj.filters new file mode 100644 index 000000000..ef2028e27 --- /dev/null +++ b/src/tools/nihilist/Nihilist.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/nihilist/Sources.pkg.xml b/src/tools/nihilist/Sources.pkg.xml new file mode 100644 index 000000000..8b7b130f6 --- /dev/null +++ b/src/tools/nihilist/Sources.pkg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/nihilist/main.cpp b/src/tools/nihilist/main.cpp new file mode 100644 index 000000000..6970221ad --- /dev/null +++ b/src/tools/nihilist/main.cpp @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +// This application exists to be connected to a console session while doing exactly nothing. +// This keeps a console session alive and doesn't interfere with tests or other hooks. +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + while (true) + { + SleepEx(INFINITE, FALSE); + } + + return 0; +} diff --git a/src/tools/nihilist/product.pbxproj b/src/tools/nihilist/product.pbxproj new file mode 100644 index 000000000..c3c52d2ae --- /dev/null +++ b/src/tools/nihilist/product.pbxproj @@ -0,0 +1,13 @@ + + + + + false + + + + LegacyTestPackage + + + + \ No newline at end of file diff --git a/src/tools/nihilist/res.rc b/src/tools/nihilist/res.rc new file mode 100644 index 000000000..5d6aec46b --- /dev/null +++ b/src/tools/nihilist/res.rc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "who cares" +#define VER_INTERNALNAME_STR "nihilist" +#define VER_ORIGINALFILENAME_STR "nihilist.EXE" + + +#include + + diff --git a/src/tools/nihilist/sources b/src/tools/nihilist/sources new file mode 100644 index 000000000..1a34c86ce --- /dev/null +++ b/src/tools/nihilist/sources @@ -0,0 +1,30 @@ +MSC_WARNING_LEVEL=/W4 /WX + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETNAME=nihilist +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_MSVCRT=1 +USE_UNICRT=1 + +C_DEFINES=-DUNICODE + +TARGETLIBS=\ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=unittests + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L) + +SPKG_SOURCES = A LegacyTestPackage entry in product.pbxproj is now required for each pkg.xml file to build packages. This exact string triggers PASS2, do not alter it. diff --git a/src/tools/nihilist/sources.dep b/src/tools/nihilist/sources.dep new file mode 100644 index 000000000..ec5b51786 --- /dev/null +++ b/src/tools/nihilist/sources.dep @@ -0,0 +1,2 @@ +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/tools/pixels/main.cpp b/src/tools/pixels/main.cpp new file mode 100644 index 000000000..41d9868c3 --- /dev/null +++ b/src/tools/pixels/main.cpp @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +#include + +using namespace wil; +using namespace wistd; +using namespace std; +using namespace Windows::Internal; + +#define CONSOLE_WINDOW_FLAGS (WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL) +#define CONSOLE_WINDOW_EX_FLAGS (WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES | WS_EX_APPWINDOW ) + + +void PrintRect(LPCWSTR pwszLabel, RECT& rc) +{ + LocalMemNativeString foo; + foo.InitializeFormat(L" L: %5d R: %5d T: %5d B: %5d (W: %5d H: %5d)", rc.left, rc.right, rc.top, rc.bottom, rc.right - rc.left, rc.bottom - rc.top); + + wcout << pwszLabel << " (exclusive rect)" << endl; + wcout << foo.Get() << endl; +} + +void PrintRect(LPCWSTR pwszLabel, SMALL_RECT& rc) +{ + LocalMemNativeString foo; + foo.InitializeFormat(L" L: %5d R: %5d T: %5d B: %5d (W: %5d H: %5d)", rc.Left, rc.Right, rc.Top, rc.Bottom, rc.Right - rc.Left + 1, rc.Bottom - rc.Top + 1); + + wcout << pwszLabel << " (inclusive rect)" << endl; + wcout << foo.Get() << endl; +} + +void PrintSize(LPCWSTR pwszLabel, COORD& sz) +{ + LocalMemNativeString foo; + foo.InitializeFormat(L"%37s(W: %5d H: %5d)", L"", sz.X, sz.Y); + + wcout << pwszLabel << endl; + wcout << foo.Get() << endl; +} + +void PrintSize(LPCWSTR pwszLabel, SIZE& sz) +{ + LocalMemNativeString foo; + foo.InitializeFormat(L"%37s(W: %5d H: %5d)", L"", sz.cx, sz.cy); + + wcout << pwszLabel << endl; + wcout << foo.Get() << endl; +} + +HRESULT PrintMonitorInfo(LPCWSTR pwszLabel, HMONITOR hmon) +{ + MONITORINFOEXW mi; + mi.cbSize = sizeof(mi); + RETURN_IF_WIN32_BOOL_FALSE(GetMonitorInfoW(hmon, &mi)); + + bool const IsPrimary = mi.dwFlags & MONITORINFOF_PRIMARY; + + wcout << pwszLabel << endl; + wcout << "- Monitor Name: " << mi.szDevice << endl; + wcout << "- Is Primary? " << IsPrimary << endl; + PrintRect(L"- Monitor Rect:", mi.rcMonitor); + PrintRect(L"- Work Rect:", mi.rcWork); + + SIZE sz; + RETURN_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, (UINT*)&sz.cx, (UINT*)&sz.cy)); + PrintSize(L"Effective DPI:", sz); + + return S_OK; +} + +BOOL CALLBACK MonitorEnumProc( + _In_ HMONITOR hMonitor, + _In_ HDC /*hdcMonitor*/, + _In_ LPRECT /*lprcMonitor*/, + _In_ LPARAM /*dwData*/ + ) +{ + PrintMonitorInfo(L"--- Monitor ---", hMonitor); + wcout << endl; + return TRUE; +} + +BOOL s_AdjustWindowRectEx(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle) +{ + return AdjustWindowRectEx(prc, dwStyle, fMenu, dwExStyle); +} + +BOOL s_UnadjustWindowRectEx(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle) +{ + RECT rc; + SetRectEmpty(&rc); + BOOL fRc = s_AdjustWindowRectEx(&rc, dwStyle, fMenu, dwExStyle); + if (fRc) + { + prc->left -= rc.left; + prc->top -= rc.top; + prc->right -= rc.right; + prc->bottom -= rc.bottom; + } + return fRc; +} + +BOOL s_AdjustWindowRectExForDpi(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle, _In_ UINT dpi) +{ + return AdjustWindowRectExForDpi(prc, dwStyle, fMenu, dwExStyle, dpi); +} + +BOOL s_UnadjustWindowRectExForDpi(_Inout_ LPRECT prc, const DWORD dwStyle, const BOOL fMenu, const DWORD dwExStyle, _In_ UINT dpi) +{ + RECT rc; + SetRectEmpty(&rc); + BOOL fRc = s_AdjustWindowRectExForDpi(&rc, dwStyle, fMenu, dwExStyle, dpi); + if (fRc) + { + prc->left -= rc.left; + prc->top -= rc.top; + prc->right -= rc.right; + prc->bottom -= rc.bottom; + } + return fRc; +} + +int __cdecl wmain(int /*argc*/, WCHAR* /*argv*/[]) +{ + // set this or we'll get false values when asking for the DPI. + SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + + HANDLE hOut = CreateFileW(L"CONOUT$", + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_WRITE, + 0, + OPEN_EXISTING, + 0, + 0); + + RETURN_IF_HANDLE_INVALID(hOut); + + HWND hwnd = GetConsoleWindow(); + wcout << "Console Window Handle: " << hwnd << endl; + + HMONITOR hmon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + RETURN_IF_HANDLE_NULL(hmon); + + UINT dpix; + UINT dpiy; + RETURN_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy)); + + RECT rc = { 0 }; + RETURN_IF_WIN32_BOOL_FALSE(GetWindowRect(hwnd, &rc)); + + PrintRect(L"Window Rect:", rc); + + RECT rcWindow = rc; + + RETURN_IF_WIN32_BOOL_FALSE(s_UnadjustWindowRectEx(&rc, CONSOLE_WINDOW_FLAGS, FALSE, CONSOLE_WINDOW_EX_FLAGS)); + PrintRect(L"Adjusted Window Rect (unscaled):", rc); + + rc = rcWindow; + RETURN_IF_WIN32_BOOL_FALSE(s_UnadjustWindowRectExForDpi(&rc, CONSOLE_WINDOW_FLAGS, FALSE, CONSOLE_WINDOW_EX_FLAGS, dpix)); + PrintRect(L"Adjusted Window Rect (scaled):", rc); + + SIZE szClient; + szClient.cx = rc.right - rc.left; + szClient.cy = rc.bottom - rc.top; + + RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(hwnd, &rc)); + + PrintRect(L"Client Rect:", rc); + + SIZE sz = { 0 }; + sz.cx = GetSystemMetrics(SM_CXHSCROLL); + sz.cy = GetSystemMetrics(SM_CYVSCROLL); + + PrintSize(L"Scroll Bar Reservations (unscaled):", sz); + + HMODULE hUser32 = LoadLibraryW(L"user32.dll"); + typedef int(*PfnGetDpiMetrics)(int nIndex, int dpi); + bool fGotMetrics = false; + + if (hUser32 != nullptr) + { + // First try the TH1/TH2 name of the function. + PfnGetDpiMetrics pfn = (PfnGetDpiMetrics)GetProcAddress(hUser32, "GetDpiMetrics"); + + if (pfn == nullptr) + { + // If not, then try the RS1 name of the function. + pfn = (PfnGetDpiMetrics)GetProcAddress(hUser32, "GetSystemMetricsForDpi"); + } + + if (pfn != nullptr) + { + sz.cx = (SHORT)pfn(SM_CXVSCROLL, dpix); + sz.cy = (SHORT)pfn(SM_CYHSCROLL, dpiy); + fGotMetrics = true; + } + + } + + if (!fGotMetrics) + { + sz.cx = (SHORT)GetSystemMetrics(SM_CXVSCROLL); + sz.cy = (SHORT)GetSystemMetrics(SM_CYHSCROLL); + } + + PrintSize(L"Scroll Bar Reservations (scaled):", sz); + + SIZE szScrollScaled = sz; + + COORD coordFont = GetConsoleFontSize(hOut, 0); + + PrintSize(L"Font Size (unscaled):", coordFont); + + sz.cx = MulDiv(coordFont.X, dpix, 96); + sz.cy = MulDiv(coordFont.Y, dpiy, 96); + + PrintSize(L"Font Size (scaled):", sz); + + SIZE szFontScaled = sz; + + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(csbiex); + + BOOL b = GetConsoleScreenBufferInfoEx(hOut, &csbiex); + + if (b == FALSE) + { + wcout << GetLastError() << endl; + } + + PrintRect(L"Viewport (chars):", csbiex.srWindow); + PrintSize(L"Max Window Size (chars):", csbiex.dwMaximumWindowSize); + PrintSize(L"Cursor Pos (chars):", csbiex.dwCursorPosition); + PrintSize(L"Buffer Size (chars):", csbiex.dwSize); + + PrintMonitorInfo(L"Primary Monitor Data:", hmon); + + wcout << endl; + + wcout << "All monitors data:" << endl; + EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, NULL); + + wcout << "------ MATH ------" << endl; + + if (szFontScaled.cx != 0 && szFontScaled.cy != 0) + { + SIZE szCharFit; + szCharFit.cx = szClient.cx / szFontScaled.cx; + szCharFit.cy = szClient.cy / szFontScaled.cy; + SIZE szCharLeftover; + szCharLeftover.cx = szClient.cx % szFontScaled.cx; + szCharLeftover.cy = szClient.cy % szFontScaled.cy; + bool fHorizScroll = (csbiex.dwSize.X > (szClient.cx / szFontScaled.cx)); + bool fVertScroll = (csbiex.dwSize.Y > (szClient.cy / szFontScaled.cy)); + + wcout << "Start with adjusted window dimensions (scaled for DPI). We take the outer window rect and ask the system to scale it down to what we could use for a client." << endl << endl; + wcout << "Width: " << endl; + wcout << " Window Adjusted: " << szClient.cx << endl; + wcout << " / Font : " << szFontScaled.cx << endl; + wcout << " = " << szCharFit.cx << " chars"; + wcout << " with " << szCharLeftover.cx << " pixels leftover" << endl; + wcout << "This is the number of characters we could fit in the window if Vertical doesn't need its scroll bar." << endl; + wcout << "Now check if we will need to steal some of Vertical's space for our Horizontal scroll bar." << endl; + wcout << " Is < buffer of : " << csbiex.dwSize.X << endl; + wcout << " H-scroll needed= " << fHorizScroll << endl; + wcout << endl; + wcout << "Height: " << endl; + wcout << " Window Adjusted: " << szClient.cy << endl; + wcout << " / Font : " << szFontScaled.cy << endl; + wcout << " = " << szCharFit.cy << " chars"; + wcout << " with " << szCharLeftover.cy << " pixels leftover" << endl; + wcout << "This is the number of characters we could fit in the window if Horizontal doesn't need its scroll bar." << endl; + wcout << "Now check if we will need to steal some of Horizontal's space for our Vertical scroll bar." << endl; + wcout << " Is < buffer of : " << csbiex.dwSize.Y << endl; + wcout << " V-scroll needed= " << fVertScroll << endl; + wcout << endl; + + SIZE szAvailableClient; + szAvailableClient = szClient; + + SIZE szRemoveBars; + szRemoveBars.cx = fVertScroll ? szScrollScaled.cx : 0; + szRemoveBars.cy = fHorizScroll ? szScrollScaled.cy : 0; + + szAvailableClient.cx -= szRemoveBars.cx; + szAvailableClient.cy -= szRemoveBars.cy; + + SIZE szCharFinal; + szCharFinal.cx = szAvailableClient.cx / szFontScaled.cx; + szCharFinal.cy = szAvailableClient.cy / szFontScaled.cy; + + SIZE szCharLeftoverFinal; + szCharLeftoverFinal.cx = szAvailableClient.cx % szFontScaled.cx; + szCharLeftoverFinal.cy = szAvailableClient.cy % szFontScaled.cy; + + wcout << "Now math out the space we actually have for the viewport with scroll bars if necessary." << endl << endl; + wcout << "Width: " << endl; + wcout << " Window Adjusted: " << szClient.cx << endl; + wcout << " - Vert Scroll : " << szRemoveBars.cx << endl; + wcout << " = " << szAvailableClient.cx << endl; + wcout << " / Font : " << szFontScaled.cx << endl; + wcout << " = " << szCharFinal.cx << " chars"; + wcout << " with " << szCharLeftoverFinal.cx << " pixels leftover" << endl; + wcout << endl; + wcout << "Height: " << endl; + wcout << " Window Adjusted: " << szClient.cy << endl; + wcout << " - Horiz Scroll : " << szRemoveBars.cy << endl; + wcout << " = " << szAvailableClient.cy << endl; + wcout << " / Font : " << szFontScaled.cy << endl; + wcout << " = " << szCharFinal.cy << " chars"; + wcout << " with " << szCharLeftoverFinal.cy << " pixels leftover" << endl; + + wcout << endl; + wcout << "------ TEST PATTERN ------" << endl; + sz = szCharFinal; + + for (LONG rows = 0; rows < sz.cy; rows++) + { + LONG foo = 0; + + for (LONG cols = 0; cols < sz.cx; cols++) + { + wcout << foo % 10; + foo++; + } + wcout << endl; + } + } + else + { + wcout << "Your font has a 0 size in it. That's sad. No more math for me." << endl; + } + + return 0; +} diff --git a/src/tools/pixels/pixels.bat b/src/tools/pixels/pixels.bat new file mode 100644 index 000000000..a760e2089 --- /dev/null +++ b/src/tools/pixels/pixels.bat @@ -0,0 +1 @@ +%_NTTREE%\conpixels.exe %1 %2 %3 %4 %5 %6 %7 %8 %9 \ No newline at end of file diff --git a/src/tools/pixels/precomp.h b/src/tools/pixels/precomp.h new file mode 100644 index 000000000..f8fba2b28 --- /dev/null +++ b/src/tools/pixels/precomp.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define DEFINE_CONSOLEV2_PROPERTIES + +// System headers +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// Annotations +#include + +// Standard library C-style +#include +#include +#include + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +// Safe path handling +#include + +// COM +#include + +// Console headers +#include diff --git a/src/tools/pixels/res.rc b/src/tools/pixels/res.rc new file mode 100644 index 000000000..b320d65b3 --- /dev/null +++ b/src/tools/pixels/res.rc @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Pixel Math Helper" +#define VER_INTERNALNAME_STR "pixels" +#define VER_ORIGINALFILENAME_STR "PIXELS.EXE" + + +#include + diff --git a/src/tools/pixels/sources b/src/tools/pixels/sources new file mode 100644 index 000000000..2550a1396 --- /dev/null +++ b/src/tools/pixels/sources @@ -0,0 +1,52 @@ +MSC_WARNING_LEVEL=/W4 /WX +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 +!include $(NTMAKEENV)\system_defaultmk.inc +!include $(WINCORE_PATH)\core.inc +SOURCES_USED=$(WINCORE_PATH)\core.inc + +TARGETNAME=conpixels +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +BUILD_FOR_CORESYSTEM=1 +USE_DEFAULT_WIN32_LIBS=0 +BUFFER_OVERFLOW_CHECKS=1 + +TEST_CODE=1 +USE_MSVCRT=1 +USE_STL=1 +STL_VER=STL_VER_CURRENT +USE_IOSTREAM=1 +USE_NATIVE_EH=1 + +C_DEFINES= -DUNICODE -D_UNICODE + +TARGETLIBS=\ + $(MINWIN_SDK_LIB_PATH)\ntdll.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore_legacy.lib \ + $(SDK_LIB_PATH)\ole32.lib \ + $(SDK_LIB_PATH)\uuid.lib \ + $(SDK_LIB_PATH)\kernel32.lib \ + $(WINDOWS_LIB_PATH)\user32p.lib \ + $(SDK_LIB_PATH)\shcore.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +PRECOMPILED_CXX=1 +PRECOMPILED_INCLUDE=precomp.h +PRECOMPILED_PCH=precomp.pch +PRECOMPILED_OBJ=precomp.obj + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + $(MINWIN_PRIV_SDK_INC_PATH); \ + $(MINCORE_PRIV_SDK_INC_PATH); \ + $(SHELL_INC_PATH); \ diff --git a/src/tools/scratch/Scratch.vcxproj b/src/tools/scratch/Scratch.vcxproj new file mode 100644 index 000000000..6ecc9b612 --- /dev/null +++ b/src/tools/scratch/Scratch.vcxproj @@ -0,0 +1,27 @@ + + + + + + + + {ED82003F-FC5D-4E94-8B36-F480018ED064} + Win32Proj + Scratch + Scratch + Scratch + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + diff --git a/src/tools/scratch/Scratch.vcxproj.filters b/src/tools/scratch/Scratch.vcxproj.filters new file mode 100644 index 000000000..ef2028e27 --- /dev/null +++ b/src/tools/scratch/Scratch.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/scratch/main.cpp b/src/tools/scratch/main.cpp new file mode 100644 index 000000000..c6f9cc901 --- /dev/null +++ b/src/tools/scratch/main.cpp @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include + +// This wmain exists for help in writing scratch programs while debugging. +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + return 0; +} diff --git a/src/tools/test/main.cpp b/src/tools/test/main.cpp new file mode 100644 index 000000000..789462cc8 --- /dev/null +++ b/src/tools/test/main.cpp @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include + + +int TestSetViewport(HANDLE hIn, HANDLE hOut); +int TestGetchar(HANDLE hIn, HANDLE hOut); + +int __cdecl wmain(int /*argc*/, WCHAR* /*argv[]*/) +{ + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + if (hIn == INVALID_HANDLE_VALUE) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut == INVALID_HANDLE_VALUE) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + + TestGetchar(hIn, hOut); + + return 0; +} + + +int TestSetViewport(HANDLE /*hIn*/, HANDLE hOut) +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = GetConsoleScreenBufferInfoEx(hOut, &csbiex); + if (fSuccess) { + const SMALL_RECT Screen = csbiex.srWindow; + const short sWidth = Screen.Right - Screen.Left; + const short sHeight = Screen.Bottom - Screen.Top; + + csbiex.srWindow.Top = 50; + csbiex.srWindow.Bottom = sHeight + 50; + csbiex.srWindow.Left = 0; + csbiex.srWindow.Right = sWidth; + + SetConsoleScreenBufferInfoEx(hOut, &csbiex); + + } + return 0; +} + +int TestGetchar(HANDLE hIn, HANDLE /*hOut*/) +{ + DWORD dwInputModes; + if (!GetConsoleMode(hIn, &dwInputModes)) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + + DWORD const dwEnableVirtualTerminalInput = 0x200; // Until the new wincon.h is published + if (!SetConsoleMode(hIn, dwInputModes | dwEnableVirtualTerminalInput)) + { + return HRESULT_FROM_WIN32(GetLastError()); + } + + while (hIn != nullptr) + { + int ch = getchar(); + printf("0x%x\r\n", ch); + } + + return 0; +} diff --git a/src/tools/test/precomp.h b/src/tools/test/precomp.h new file mode 100644 index 000000000..2a2f83f42 --- /dev/null +++ b/src/tools/test/precomp.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define DEFINE_CONSOLEV2_PROPERTIES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/tools/test/res.rc b/src/tools/test/res.rc new file mode 100644 index 000000000..60eb136f5 --- /dev/null +++ b/src/tools/test/res.rc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Console Experiments Tester" +#define VER_INTERNALNAME_STR "conhost.test" +#define VER_ORIGINALFILENAME_STR "conhost.test.EXE" + + +#include + + diff --git a/src/tools/test/sources b/src/tools/test/sources new file mode 100644 index 000000000..cfab9754c --- /dev/null +++ b/src/tools/test/sources @@ -0,0 +1,36 @@ +MSC_WARNING_LEVEL=/W4 /WX +!include $(NTMAKEENV)\system_defaultmk.inc +!include $(WINCORE_PATH)\core.inc +SOURCES_USED=$(WINCORE_PATH)\core.inc + +TARGETNAME=conhost.test +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_MSVCRT=1 + +C_DEFINES=-DUNICODE + +TARGETLIBS=\ + $(MINWIN_SDK_LIB_PATH)\ntdll.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore.lib \ + $(MINCORE_SDK_LIB_PATH)\mincore_legacy.lib \ + $(SDK_LIB_PATH)\ole32.lib \ + $(SDK_LIB_PATH)\uuid.lib \ + $(SDK_LIB_PATH)\propsys.lib \ + +SOURCES=main.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +PRECOMPILED_CXX=1 +PRECOMPILED_INCLUDE=precomp.h +PRECOMPILED_PCH=precomp.pch +PRECOMPILED_OBJ=precomp.obj + +INCLUDES= \ + $(INCLUDES);$(OBJ_PATH)\$(O);$(MINWIN_PRIV_SDK_INC_PATH) diff --git a/src/tools/texttests/fira.txt b/src/tools/texttests/fira.txt new file mode 100644 index 000000000..a44f55c21 --- /dev/null +++ b/src/tools/texttests/fira.txt @@ -0,0 +1,31 @@ +.= ..= .- := =:= =!= +== != === !== =/= + +<-< <<- <-- <- <-> -> --> ->> >-> +<=< <<= <== <=> <==> => ==> =>> >=> +>>= >>- >- <~> -< -<< =<< +<~~ <~ ~~ ~> ~~> + +<<< << <= <> >= >> >>> +{| [| <: :< >: :> |] |} +<|||<||<| <|> |>||>|||> +<$ <$> $> +<+ <+> +> +<* <*> *> + +/* */ /// // + --> /> +0xFF 10x10 +9:45 [:] m-x m+x *ptr + +;; :: ::: .. ... ..< +!! ?? %% && || ?. ?: ++ ++ +++ +- -- --- +* ** *** + +~= ~- www -~ -@ +^= ?= /= /== +-| _|_ |- |= ||= +#! #= #: ## ### #### +#{ #[ ]# #( #? #_ #_( \ No newline at end of file diff --git a/src/tools/texttests/foo.txt b/src/tools/texttests/foo.txt new file mode 100644 index 000000000..7fef1c3b4 --- /dev/null +++ b/src/tools/texttests/foo.txt @@ -0,0 +1,2 @@ +aardvark!😍🤑🤢👽🦄HiThere!ひらがなemoji + diff --git a/src/tools/vtapp/App.config b/src/tools/vtapp/App.config new file mode 100644 index 000000000..2c307fa31 --- /dev/null +++ b/src/tools/vtapp/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/tools/vtapp/Program.cs b/src/tools/vtapp/Program.cs new file mode 100644 index 000000000..6c3818806 --- /dev/null +++ b/src/tools/vtapp/Program.cs @@ -0,0 +1,434 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace VTApp +{ + class Program + { + static string CSI = ((char)0x1b)+"["; + static void Main(string[] args) + { + Console.WindowHeight = 25; + Console.BufferHeight = 9000; + Console.WindowWidth = 80; + Console.BufferWidth = 80; + + Console.WriteLine("VT Tester"); + + while (true) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + + switch (keyInfo.KeyChar) + { + case '\x1b': // escape + // this case is for receiving replies from the console host + StringBuilder builder = new StringBuilder(); + builder.Append(keyInfo.KeyChar); + + keyInfo = Console.ReadKey(true); + + // 40-7E are the "dispatch" characters meaning the sequence is done. + // 0x5B '[' is expected after the escape. So ignore that. We don't know a of a sequence terminated with it, so it also continues the loop. + // keep collecting characters as the "reply" until then + while (keyInfo.KeyChar < 0x40 || keyInfo.KeyChar > 0x7E || keyInfo.KeyChar == '[') + { + builder.Append(keyInfo.KeyChar); + + keyInfo = Console.ReadKey(true); + } + builder.Append(keyInfo.KeyChar); + + Console.Title = string.Format(CultureInfo.InvariantCulture, "Response Received: {0}", builder.ToString()); + break; + case '\x8': // backspace + Console.Write('\x8'); + break; + case '\x9': // horizontal tab (tab key) + Console.Write('\x9'); + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'P': + case 'S': + case 'T': + case 'L': + case 'M': + Console.Write(CSI); + Console.Write(keyInfo.KeyChar); + break; + case 'O': + Console.Write(CSI); + Console.Write("@"); + break; + case 'G': + Console.Write(CSI); + Console.Write('1'); + Console.Write('4'); + Console.Write('G'); + break; + case 'v': + Console.Write(CSI); + Console.Write('1'); + Console.Write('4'); + Console.Write('d'); + break; + case 'H': + Console.Write(CSI); + Console.Write('5'); + Console.Write(';'); + Console.Write('1'); + Console.Write('H'); + break; + case 'h': + Console.Write(CSI); + Console.Write('?'); + Console.Write('2'); + Console.Write('5'); + Console.Write('h'); + break; + case 'l': + Console.Write(CSI); + Console.Write('?'); + Console.Write('2'); + Console.Write('5'); + Console.Write('l'); + break; + case '7': + Console.Write((char)0x1b); + Console.Write('7'); + break; + case '8': + Console.Write((char)0x1b); + Console.Write('8'); + break; + case 'y': + Console.Write(CSI); + Console.Write('s'); + break; + case 'u': + Console.Write(CSI); + Console.Write('u'); + break; + case '~': + // move to top left corner + Console.Write(CSI); + Console.Write('H'); + + // write out a ton of Zs + for (int i = 0; i < 24; i++) + { + for (int j = 0; j < 80; j++) + { + Console.Write("Z"); + } + } + + for (int j = 0; j < 79; j++) + { + Console.Write("Z"); + } + + // move to middle-ish + Console.Write(CSI); + Console.Write("15"); + Console.Write(';'); + Console.Write("15"); + Console.Write('H'); + break; + case 'J': + Console.Write(CSI); + Console.Write("J"); + break; + case 'j': + Console.Write(CSI); + Console.Write("1"); + Console.Write("J"); + break; + case 'K': + Console.Write(CSI); + Console.Write("K"); + break; + case 'k': + Console.Write(CSI); + Console.Write("1"); + Console.Write("K"); + break; + case '0': + Console.Write(CSI); + Console.Write("m"); + break; + case '1': + Console.Write(CSI); + Console.Write("1m"); + break; + case '2': + Console.Write(CSI); + Console.Write("32m"); + break; + case '3': + Console.Write(CSI); + Console.Write("33m"); + break; + case '4': + Console.Write(CSI); + Console.Write("34m"); + break; + case '5': + Console.Write(CSI); + Console.Write("35m"); + break; + case '6': + Console.Write(CSI); + Console.Write("36m"); + break; + case '!': + Console.Write(CSI); + Console.Write("91m"); + break; + case '@': + Console.Write(CSI); + Console.Write("94m"); + break; + case '#': + Console.Write(CSI); + Console.Write("96m"); + break; + case '$': + Console.Write(CSI); + Console.Write("101m"); + break; + case '%': + Console.Write(CSI); + Console.Write("104m"); + break; + case '^': + Console.Write(CSI); + Console.Write("106m"); + break; + case 'Q': + Console.Write(CSI); + Console.Write("40m"); + break; + case 'W': + Console.Write(CSI); + Console.Write("47m"); + break; + case 'q': + Console.Write(CSI); + Console.Write("41m"); + break; + case 'w': + Console.Write(CSI); + Console.Write("43m"); + break; + case 'e': + Console.Write(CSI); + Console.Write("4m"); + break; + case 'd': + Console.Write(CSI); + Console.Write("24m"); + break; + case 'r': + Console.Write(CSI); + Console.Write("7m"); + break; + case 'f': + Console.Write(CSI); + Console.Write("27m"); + break; + case 'R': + Console.Write(CSI); + Console.Write("6n"); + break; + case 'c': + Console.Write(CSI); + Console.Write("0c"); + break; + case '9': + Console.Write(CSI); + Console.Write("1;37;43;4m"); + break; + case '(': + Console.Write(CSI); + Console.Write("39m"); + break; + case ')': + Console.Write(CSI); + Console.Write("49m"); + break; + case '<': + Console.Write('\xD'); // carriage return \r + break; + case '>': + Console.Write('\xA'); // line feed/new line \n + break; + case '`': + Console.Write("z"); + break; + case '-': + { + IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_OUTPUT_HANDLE); + + int mode; + + if (Pinvoke.GetConsoleMode(hCon, out mode)) + { + if ((mode & Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) + { + mode &= ~Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + else + { + mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + + Pinvoke.SetConsoleMode(hCon, mode); + } + break; + } + case '_': + { + IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_INPUT_HANDLE); + + int mode; + if (Pinvoke.GetConsoleMode(hCon, out mode)) + { + if ((mode & Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT) != 0) + { + mode &= ~Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT; + } + else + { + mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT; + } + mode &= ~Pinvoke.ENABLE_PROCESSED_INPUT; + Pinvoke.SetConsoleMode(hCon, mode); + } + break; + } + case 's': + Console.Write(CSI); + Console.Write("5S"); + break; + case 't': + Console.Write(CSI); + Console.Write("5T"); + break; + case '\'': + Console.Write(CSI); + Console.Write("3L"); + break; + case '"': + Console.Write(CSI); + Console.Write("3M"); + break; + case '\\': + Console.Write(CSI); + Console.Write("?3l"); + break; + case '|': + Console.Write(CSI); + Console.Write("?3h"); + break; + case '&': + Console.Write(CSI); + Console.Write("0;0r"); + break; + case '*': + Console.Write(CSI + "3;1H"); + Console.Write(CSI + "1;42m"); + Console.Write("VVVVVVVVVVVVVVVV"); + Console.Write(CSI + "4;1H"); + Console.Write(CSI + "43m"); + Console.Write("----------------"); + Console.Write(CSI + "12;1H"); + Console.Write(CSI + "44m"); + Console.Write("----------------"); + Console.Write(CSI + "13;1H"); + Console.Write(CSI + "45m"); + Console.Write("^^^^^^^^^^^^^^^^"); + Console.Write(CSI + "m"); + Console.Write(CSI + "1m"); + Console.Write(CSI + "5;2Ha"); + Console.Write(CSI + "6;3Hb"); + Console.Write(CSI + "7;4Hc"); + Console.Write(CSI + "8;5Hd"); + Console.Write(CSI + "9;6He"); + Console.Write(CSI + "10;7Hf"); + Console.Write(CSI + "11;8Hg"); + Console.Write(CSI + "12;9Hh"); + Console.Write(CSI + "m"); + Console.Write(CSI + "4;12r"); + break; + case '{': + // move to top left corner + Console.Write(CSI); + Console.Write('H'); + + // write out a ton of Zs + for (int i = 0; i < 24; i++) + { + for (int j = 0; j < 80; j++) + { + if (j == 0) + Console.Write(i%10); + else + Console.Write("Z"); + } + } + + for (int j = 0; j < 79; j++) + { + Console.Write("Z"); + } + + // move to middle-ish + Console.Write(CSI); + Console.Write("15"); + Console.Write(';'); + Console.Write("15"); + Console.Write('H'); + break; + case '=': //Go to the v2 app + VTApp2.Program.Main2(args); + break; + } + } + } + } + + public static class Pinvoke + { + [DllImport("kernel32.dll")] + public static extern bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode); + + [DllImport("kernel32.dll")] + public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out int lpMode); + + public const int ENABLE_PROCESSED_OUTPUT = 0x1; + public const int ENABLE_WRAP_AT_EOL_OUTPUT = 0x2; + public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4; + + public const int ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; + public const int ENABLE_PROCESSED_INPUT = 0x0001; + public const int STD_INPUT_HANDLE = -10; + public const int STD_OUTPUT_HANDLE = -11; + + [DllImport("kernel32.dll")] + public static extern IntPtr GetStdHandle(int nStdHandle); + } +} diff --git a/src/tools/vtapp/Program2.cs b/src/tools/vtapp/Program2.cs new file mode 100644 index 000000000..22060f292 --- /dev/null +++ b/src/tools/vtapp/Program2.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace VTApp2 +{ + class Program + { + + public static void Main2(string[] args) + { + string backupTitle = Console.Title; + + Console.WindowHeight = 25; + Console.BufferHeight = 9000; + Console.WindowWidth = 80; + Console.BufferWidth = 80; + + Console.WriteLine("VT Tester 2, way better than v1"); + + IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_INPUT_HANDLE); + int mode; + bool fSuccess = Pinvoke.GetConsoleMode(hCon, out mode); + if (fSuccess) + { + mode &= ~Pinvoke.ENABLE_PROCESSED_INPUT; + mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_INPUT; + fSuccess = Pinvoke.SetConsoleMode(hCon, mode); + } + if (!fSuccess) return; + + while (true) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(false); + switch (keyInfo.KeyChar) + { + case '=': + case '>': + enableVT(); + Console.Write((char)0x1b); + Console.Write(keyInfo.KeyChar); + disableVT(); + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'P': + case 'S': + case 'T': + enableVT(); + Console.Write((char)0x1b); + Console.Write((char)0x5b); + Console.Write(keyInfo.KeyChar); + disableVT(); + break; + + } + } + } + public static void enableVT() + { + IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_OUTPUT_HANDLE); + + int mode; + bool fSuccess = Pinvoke.GetConsoleMode(hCon, out mode); + + if (fSuccess) + { + mode |= Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + fSuccess = Pinvoke.SetConsoleMode(hCon, mode); + } + } + public static void disableVT() + { + IntPtr hCon = Pinvoke.GetStdHandle(Pinvoke.STD_OUTPUT_HANDLE); + int mode; + bool fSuccess = Pinvoke.GetConsoleMode(hCon, out mode); + + if (fSuccess) + { + mode &= ~Pinvoke.ENABLE_VIRTUAL_TERMINAL_PROCESSING; + fSuccess = Pinvoke.SetConsoleMode(hCon, mode); + } + } + + } + + + public static class Pinvoke + { + [DllImport("kernel32.dll")] + public static extern bool SetConsoleMode(IntPtr hConsoleHandle, int dwMode); + + [DllImport("kernel32.dll")] + public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out int lpMode); + + public const int ENABLE_PROCESSED_OUTPUT = 0x1; + public const int ENABLE_WRAP_AT_EOL_OUTPUT = 0x2; + public const int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4; + + public const int ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; + public const int ENABLE_PROCESSED_INPUT = 0x0001; + public const int STD_INPUT_HANDLE = -10; + public const int STD_OUTPUT_HANDLE = -11; + + [DllImport("kernel32.dll")] + public static extern IntPtr GetStdHandle(int nStdHandle); + } +} diff --git a/src/tools/vtapp/Properties/AssemblyInfo.cs b/src/tools/vtapp/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..7a13539b7 --- /dev/null +++ b/src/tools/vtapp/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("VTApp")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("099193a0-1e43-4bbc-ba7f-7b351e1342df")] diff --git a/src/tools/vtapp/Properties/AssemblyInfoVsSpecific.cs b/src/tools/vtapp/Properties/AssemblyInfoVsSpecific.cs new file mode 100644 index 000000000..921b6e920 --- /dev/null +++ b/src/tools/vtapp/Properties/AssemblyInfoVsSpecific.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// These can only be defined when building in Visual Studio. The Windows build system defines these for us. + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VTApp")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/tools/vtapp/Sources.pkg.xml b/src/tools/vtapp/Sources.pkg.xml new file mode 100644 index 000000000..e7df9b1e9 --- /dev/null +++ b/src/tools/vtapp/Sources.pkg.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/vtapp/VTApp.csproj b/src/tools/vtapp/VTApp.csproj new file mode 100644 index 000000000..a61d04075 --- /dev/null +++ b/src/tools/vtapp/VTApp.csproj @@ -0,0 +1,66 @@ + + + + + {099193A0-1E43-4BBC-BA7F-7B351E1342DF} + Exe + Properties + VTApp + VTApp + v4.0 + 512 + true + + $(SolutionDir)\bin\$(Platform)\$(Configuration) + prompt + 4 + + + ARM64 + + + x64 + + + x86 + + + true + full + false + DEBUG;TRACE + + + pdbonly + true + TRACE + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tools/vtapp/product.pbxproj b/src/tools/vtapp/product.pbxproj new file mode 100644 index 000000000..c3c52d2ae --- /dev/null +++ b/src/tools/vtapp/product.pbxproj @@ -0,0 +1,13 @@ + + + + + false + + + + LegacyTestPackage + + + + \ No newline at end of file diff --git a/src/tools/vtapp/sources b/src/tools/vtapp/sources new file mode 100644 index 000000000..2e35afd39 --- /dev/null +++ b/src/tools/vtapp/sources @@ -0,0 +1,21 @@ +TARGETNAME = VtApp +TARGETTYPE = PROGRAM +UMTYPE = console +TARGET_DESTINATION = UnitTests + +MANAGED_CODE = 1 +TEST_CODE = 1 +URT_VER = 4.5 + +USER_CS_FLAGS = $(USER_CS_FLAGS) /define:__INSIDE_WINDOWS + +SOURCES = \ + Program.cs \ + Program2.cs \ + +REFERENCES = $(CLR_REF_PATH)\System.metadata_dll; \ + $(CLR_REF_PATH)\System.Core.metadata_dll; \ + $(CLR_REF_PATH)\System.Data.metadata_dll; \ + $(CLR_REF_PATH)\System.Drawing.metadata_dll; \ + +SPKG_SOURCES = A LegacyTestPackage entry in product.pbxproj is now required for each pkg.xml file to build packages. This exact string triggers PASS2, do not alter it. diff --git a/src/tools/vtapp/sources.dep b/src/tools/vtapp/sources.dep new file mode 100644 index 000000000..02872581a --- /dev/null +++ b/src/tools/vtapp/sources.dep @@ -0,0 +1,5 @@ +PUBLIC_PASS0_CONSUMES= \ + onecore\redist\mspartners\netfx45\core\binary_release|PASS0 \ + +BUILD_PASS3_CONSUMES= \ + onecore\merged\mbs\bootableskus\prepareimagingtools|PASS3 \ diff --git a/src/tools/vtpipeterm/VtConsole.cpp b/src/tools/vtpipeterm/VtConsole.cpp new file mode 100644 index 000000000..9e9b37b13 --- /dev/null +++ b/src/tools/vtpipeterm/VtConsole.cpp @@ -0,0 +1,403 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + + +#include "..\..\inc\conpty.h" +#include "VtConsole.hpp" + +#include /* srand, rand */ +#include /* time */ + +#include +#include +#include +#include +#include +#include +#include + +VtConsole::VtConsole(PipeReadCallback const pfnReadCallback, + bool const fHeadless, + bool const fUseConpty, + COORD const initialSize) : + _signalPipe(INVALID_HANDLE_VALUE), + _outPipe(INVALID_HANDLE_VALUE), + _inPipe(INVALID_HANDLE_VALUE), + _dwOutputThreadId(0) +{ + _pfnReadCallback = pfnReadCallback; + _fHeadless = fHeadless; + _fUseConPty = fUseConpty; + _lastDimensions = initialSize; + +} + +void VtConsole::spawn() +{ + _spawn(L""); +} + +void VtConsole::spawn(const std::wstring& command) +{ + _spawn(command); +} + +// Prepares the `lpAttributeList` member of a STARTUPINFOEX for attaching a +// client application to a pseudoconsole. +// Prior to calling this function, hPty should be initialized with a call to +// CreatePseudoConsole, and the pAttrList should be initialized with a call +// to InitializeProcThreadAttributeList. The caller of +// InitializeProcThreadAttributeList should add one to the dwAttributeCount +// param when creating the attribute list for usage by this function. +HRESULT AttachPseudoConsole(HPCON hPC, LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList) +{ + BOOL fSuccess = UpdateProcThreadAttribute(lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + hPC, + sizeof(HPCON), + NULL, + NULL); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - Sample function which combines the creation of some basic anonymous pipes +// and passes them to CreatePseudoConsole. +// Arguments: +// - size: The size of the conpty to create, in characters. +// - phInput: Receives the handle to the newly-created anonymous pipe for writing input to the conpty. +// - phOutput: Receives the handle to the newly-created anonymous pipe for reading the output of the conpty. +// - phPty: Receives a token value to identify this conpty +HRESULT CreatePseudoConsoleAndHandles(COORD size, + _In_ DWORD dwFlags, + _Out_ HANDLE* phInput, + _Out_ HANDLE* phOutput, + _Out_ HPCON* phPC) +{ + if(phPC == NULL || phInput == NULL || phOutput == NULL) + { + return E_INVALIDARG; + } + + HANDLE outPipeOurSide; + HANDLE inPipeOurSide; + HANDLE outPipePseudoConsoleSide; + HANDLE inPipePseudoConsoleSide; + + HRESULT hr = S_OK; + if (!CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, NULL, 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + if (SUCCEEDED(hr)) + { + if (!CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, NULL, 0)) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + if (SUCCEEDED(hr)) + { + hr = CreatePseudoConsole(size, inPipePseudoConsoleSide, outPipePseudoConsoleSide, dwFlags, phPC); + if (FAILED(hr)) + { + CloseHandle(inPipeOurSide); + CloseHandle(outPipeOurSide); + } + else + { + *phInput = inPipeOurSide; + *phOutput = outPipeOurSide; + } + CloseHandle(outPipePseudoConsoleSide); + } + else + { + CloseHandle(inPipeOurSide); + } + CloseHandle(inPipePseudoConsoleSide); + } + return hr; +} + +// Method Description: +// - This version of _spawn uses the actual Pty API for creating the conhost, +// independent of the child process. We then attach the client later. +// Arguments: +// - command: commandline of the child application to attach +// Return Value: +// - +void VtConsole::_spawn(const std::wstring& command) +{ + if (_fUseConPty) + { + _createPseudoConsole(command); + } + else if (_fHeadless) + { + _createConptyManually(command); + } + else + { + _createConptyViaCommandline(command); + } + + _connected = true; + + // Create our own output handling thread + // Each console needs to make sure to drain the output from it's backing host. + _dwOutputThreadId = (DWORD)-1; + _hOutputThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)StaticOutputThreadProc, + this, + 0, + &_dwOutputThreadId); + +} + +PCWSTR GetCmdLine() +{ + return L"conhost.exe"; +} + +void VtConsole::_createPseudoConsole(const std::wstring& command) +{ + bool fSuccess; + + THROW_IF_FAILED(CreatePseudoConsoleAndHandles(_lastDimensions, 0, &_inPipe, &_outPipe, &_hPC)); + + STARTUPINFOEX siEx; + siEx = { 0 }; + siEx.StartupInfo.cb = sizeof(STARTUPINFOEX); + size_t size; + InitializeProcThreadAttributeList(NULL, 1, 0, (PSIZE_T)&size); + BYTE* attrList = new BYTE[size]; + siEx.lpAttributeList = reinterpret_cast(attrList); + fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, (PSIZE_T)&size); + THROW_LAST_ERROR_IF(!fSuccess); + + THROW_IF_FAILED(AttachPseudoConsole(_hPC, siEx.lpAttributeList)); + + std::wstring realCommand = command; + if (realCommand == L""){ + realCommand = L"cmd.exe"; + } + + std::unique_ptr mutableCommandline = std::make_unique(realCommand.length() + 1); + THROW_IF_NULL_ALLOC(mutableCommandline); + + HRESULT hr = StringCchCopy(mutableCommandline.get(), realCommand.length()+1, realCommand.c_str()); + THROW_IF_FAILED(hr); + fSuccess = !!CreateProcessW( + nullptr, + mutableCommandline.get(), + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + true, // bInheritHandles + EXTENDED_STARTUPINFO_PRESENT, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &siEx.StartupInfo, // lpStartupInfo + &_piClient // lpProcessInformation + ); + THROW_LAST_ERROR_IF(!fSuccess); + DeleteProcThreadAttributeList(siEx.lpAttributeList); +} + +void VtConsole::_createConptyManually(const std::wstring& command) +{ + if (_fHeadless) + { + THROW_IF_FAILED(CreateConPty(command, + _lastDimensions.X, + _lastDimensions.Y, + &_inPipe, + &_outPipe, + &_signalPipe, + &_piPty)); + + } + else + { + _createConptyViaCommandline(command); + } +} + +void VtConsole::_createConptyViaCommandline(const std::wstring& command) +{ + std::wstring cmdline(GetCmdLine()); + + if (_fHeadless) + { + cmdline += L" --headless"; + } + + // Create some anon pipes so we can pass handles down and into the console. + // IMPORTANT NOTE: + // We're creating the pipe here with un-inheritable handles, then marking + // the conhost sides of the pipes as inheritable. We do this because if + // the entire pipe is marked as inheritable, when we pass the handles + // to CreateProcess, at some point the entire pipe object is copied to + // the conhost process, which includes the terminal side of the pipes + // (_inPipe and _outPipe). This means that if we die, there's still + // outstanding handles to our side of the pipes, and those handles are + // in conhost, despite conhost being unable to reference those handles + // and close them. + + // CRITICAL: Close our side of the handles. Otherwise you'll get the same + // problem if you close conhost, but not us (the terminal). + // The conhost sides of the pipe will be unique_hfile's so that they'll get + // closed automatically at the end of the method. + wil::unique_hfile outPipeConhostSide; + wil::unique_hfile inPipeConhostSide; + wil::unique_hfile signalPipeConhostSide; + + SECURITY_ATTRIBUTES sa; + sa = { 0 }; + sa.nLength = sizeof(sa); + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = nullptr; + + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&inPipeConhostSide, &_inPipe, &sa, 0)); + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&_outPipe, &outPipeConhostSide, &sa, 0)); + + // Mark inheritable for signal handle when creating. It'll have the same value on the other side. + sa.bInheritHandle = TRUE; + THROW_IF_WIN32_BOOL_FALSE(CreatePipe(&signalPipeConhostSide, &_signalPipe, &sa, 0)); + + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(inPipeConhostSide.get(), HANDLE_FLAG_INHERIT, 1)); + THROW_IF_WIN32_BOOL_FALSE(SetHandleInformation(outPipeConhostSide.get(), HANDLE_FLAG_INHERIT, 1)); + + STARTUPINFO si = { 0 }; + si.cb = sizeof(STARTUPINFOW); + si.hStdInput = inPipeConhostSide.get(); + si.hStdOutput = outPipeConhostSide.get(); + si.hStdError = outPipeConhostSide.get(); + si.dwFlags |= STARTF_USESTDHANDLES; + + if(!(_lastDimensions.X == 0 && _lastDimensions.Y == 0)) + { + // STARTF_USECOUNTCHARS does not work. + // minkernel/console/client/dllinit will write that value to conhost + // during init of a cmdline application, but because we're starting + // conhost directly, that doesn't work for us. + std::wstringstream ss; + ss << L" --width " << _lastDimensions.X; + ss << L" --height " << _lastDimensions.Y; + cmdline += ss.str(); + } + + // Attach signal handle ID onto command line using string stream for formatting + std::wstringstream signalArg; + signalArg << L" --signal 0x" << std::hex << HandleToUlong(signalPipeConhostSide.get()); + cmdline += signalArg.str(); + + if (command.length() > 0) + { + cmdline += L" -- "; + cmdline += command; + } + else + { + si.dwFlags |= STARTF_USESHOWWINDOW; + si.wShowWindow = SW_MINIMIZE; + } + + bool fSuccess = !!CreateProcess( + nullptr, + &cmdline[0], + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + true, // bInheritHandles + 0, // dwCreationFlags + nullptr, // lpEnvironment + nullptr, // lpCurrentDirectory + &si, // lpStartupInfo + &_piPty // lpProcessInformation + ); + + if (!fSuccess) + { + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + std::string msg = "Failed to launch Openconsole"; + WriteFile(hOut, msg.c_str(), (DWORD)msg.length(), nullptr, nullptr); + } +} + +void VtConsole::activate() +{ + _active = true; +} + +void VtConsole::deactivate() +{ + _active = false; +} + +DWORD VtConsole::StaticOutputThreadProc(LPVOID lpParameter) +{ + VtConsole* const pInstance = (VtConsole*)lpParameter; + return pInstance->_OutputThread(); +} + +DWORD VtConsole::_OutputThread() +{ + BYTE buffer[256]; + DWORD dwRead; + while (true) + { + dwRead = 0; + bool fSuccess = false; + + fSuccess = !!ReadFile(this->outPipe(), buffer, ARRAYSIZE(buffer), &dwRead, nullptr); + + THROW_LAST_ERROR_IF(!fSuccess); + if (this->_active) + { + _pfnReadCallback(buffer, dwRead); + } + } +} + +bool VtConsole::Repaint() +{ + std::string seq = "\x1b[7t"; + return WriteInput(seq); +} + +bool VtConsole::Resize(const unsigned short rows, const unsigned short cols) +{ + if (_fUseConPty) { + return SUCCEEDED(ResizePseudoConsole(_hPC, {(SHORT)cols, (SHORT)rows})); + } + else + { + return SignalResizeWindow(_signalPipe, cols, rows); + } +} + +HANDLE VtConsole::inPipe() +{ + return _inPipe; +} +HANDLE VtConsole::outPipe() +{ + return _outPipe; +} + +void VtConsole::signalWindow(unsigned short sx, unsigned short sy) +{ + Resize(sy, sx); +} + +bool VtConsole::WriteInput(std::string& seq) +{ + bool fSuccess = !!WriteFile(inPipe(), seq.c_str(), (DWORD)seq.length(), nullptr, nullptr); + if (!fSuccess) + { + HRESULT hr = GetLastError(); + exit(hr); + } + return fSuccess; +} diff --git a/src/tools/vtpipeterm/VtConsole.hpp b/src/tools/vtpipeterm/VtConsole.hpp new file mode 100644 index 000000000..8270ec6e4 --- /dev/null +++ b/src/tools/vtpipeterm/VtConsole.hpp @@ -0,0 +1,84 @@ +/*++ +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: +- VtConsole.hpp + +Abstract: +- This serves as an abstraction to allow for a test connection to a conhost.exe running + in VT server mode. It's abstracted to allow multiple simultaneous connections to multiple + conhost.exe servers. + +Author(s): +- Mike Griese (MiGrie) 2017 +--*/ + + +#include +#include +#include + +#include + +typedef void(*PipeReadCallback)(BYTE* buffer, DWORD dwRead); + +class VtConsole +{ +public: + VtConsole(PipeReadCallback const pfnReadCallback, bool const fHeadless, bool const fUseConpty, COORD const initialSize); + void spawn(); + void spawn(const std::wstring& command); + + HANDLE inPipe(); + HANDLE outPipe(); + + static const DWORD sInPipeOpenMode = PIPE_ACCESS_DUPLEX; + static const DWORD sOutPipeOpenMode = PIPE_ACCESS_INBOUND; + + static const DWORD sInPipeMode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT; + static const DWORD sOutPipeMode = PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT; + + void activate(); + void deactivate(); + + void signalWindow(unsigned short sx, unsigned short sy); + + static DWORD StaticOutputThreadProc(LPVOID lpParameter); + + bool WriteInput(std::string& seq); + + bool Repaint(); + bool Resize(const unsigned short rows, const unsigned short cols); + +private: + COORD _lastDimensions; + + PROCESS_INFORMATION _piPty; + PROCESS_INFORMATION _piClient; + + HANDLE _outPipe; + HANDLE _inPipe; + HANDLE _signalPipe; + + HPCON _hPC; + + bool _connected = false; + bool _active = false; + bool _fUseConPty = false; + bool _fHeadless = false; + + PipeReadCallback _pfnReadCallback; + + DWORD _dwOutputThreadId; + HANDLE _hOutputThread = INVALID_HANDLE_VALUE; + + void _createPseudoConsole(const std::wstring& command); + void _createConptyManually(const std::wstring& command); + void _createConptyViaCommandline(const std::wstring& command); + + void _spawn(const std::wstring& command); + + DWORD _OutputThread(); + +}; diff --git a/src/tools/vtpipeterm/VtPipeTerm.vcxproj b/src/tools/vtpipeterm/VtPipeTerm.vcxproj new file mode 100644 index 000000000..347a996b1 --- /dev/null +++ b/src/tools/vtpipeterm/VtPipeTerm.vcxproj @@ -0,0 +1,32 @@ + + + + + + + + + + + + {814DBDDE-894E-4327-A6E1-740504850098} + Win32Proj + VtPipeTerm + VtPipeTerm + VtPipeTerm + 10.0.17763.0 + + + + _CONSOLE;%(PreprocessorDefinitions) + NotUsing + + + Console + + + + + + + diff --git a/src/tools/vtpipeterm/VtPipeTerm.vcxproj.filters b/src/tools/vtpipeterm/VtPipeTerm.vcxproj.filters new file mode 100644 index 000000000..87852d306 --- /dev/null +++ b/src/tools/vtpipeterm/VtPipeTerm.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + diff --git a/src/tools/vtpipeterm/main.cpp b/src/tools/vtpipeterm/main.cpp new file mode 100644 index 000000000..9e0a00815 --- /dev/null +++ b/src/tools/vtpipeterm/main.cpp @@ -0,0 +1,611 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include +#include +#include +#include /* srand, rand */ +#include /* time */ + +#include +#include +#include +#include +#include +#include + +#include "VtConsole.hpp" + +using namespace std; +//////////////////////////////////////////////////////////////////////////////// +// "Do Unicode" strings - C-b, u, then one of these characters to emit a string +// of characters that aren't really possible to read from the console currently. +const int TEST_LANG_NONE = 0; // 0 +const int TEST_LANG_CYRILLIC = 1; // 1 +const int TEST_LANG_CHINESE = 2; // 2 +const int TEST_LANG_JAPANESE = 3; // 3 +const int TEST_LANG_KOREAN = 4; // 4 +const int TEST_LANG_GOOD_POUND = 5; // # +const int TEST_LANG_BAD_POUND = 6; // $ + +//////////////////////////////////////////////////////////////////////////////// +// State +HANDLE hOut; +HANDLE hIn; +short lastTerminalWidth; +short lastTerminalHeight; + +std::deque consoles; +// A console for printing debug output to +VtConsole* debug; + +bool prefixPressed = false; +bool doUnicode = false; +int lang = TEST_LANG_NONE; + +bool g_headless = false; +bool g_useConpty = false; +bool g_useOutfile = false; +std::wstring outfile = L"vtpt.out"; +HANDLE hOutFile = INVALID_HANDLE_VALUE; +//////////////////////////////////////////////////////////////////////////////// +// Forward decls +std::string toPrintableString(std::string& inString); +void toPrintableBuffer(char c, char* printBuffer, int* printCch); +std::string csi(string seq); +void PrintInputToDebug(std::string& rawInput); +void PrintOutputToDebug(std::string& rawOutput); +//////////////////////////////////////////////////////////////////////////////// + +void ReadCallback(BYTE* buffer, DWORD dwRead) +{ + // We already set the console to UTF-8 CP, so we can just write straight to it + bool fSuccess = !!WriteFile(hOut, buffer, dwRead, nullptr, nullptr); + if (fSuccess && g_useOutfile) + { + fSuccess = !!WriteFile(hOutFile, buffer, dwRead, nullptr, nullptr); + } + if (fSuccess) + { + std::string renderData = std::string((char*)buffer, dwRead); + PrintOutputToDebug(renderData); + } + else + { + HRESULT hr = GetLastError(); + exit(hr); + } +} + +void DebugReadCallback(BYTE* /*buffer*/, DWORD /*dwRead*/) +{ + // do nothing. +} + +VtConsole* getConsole() +{ + return consoles[0]; +} + +void nextConsole() +{ + auto con = consoles[0]; + con->deactivate(); + consoles.pop_front(); + consoles.push_back(con); + con = consoles[0]; + con->activate(); + // Force the new console to repaint. + std::string seq = csi("7t"); + con->WriteInput(seq); +} + +HANDLE inPipe() +{ + return getConsole()->inPipe(); +} + +HANDLE outPipe() +{ + return getConsole()->outPipe(); +} + +void newConsole() +{ + auto con = new VtConsole(ReadCallback, g_headless, g_useConpty, {lastTerminalWidth, lastTerminalHeight}); + con->spawn(); + consoles.push_back(con); +} + +void signalConsole() +{ + // The 0th console is always our active one. + // This is a test-only scenario to set the window to 30 wide by 10 tall. + consoles.at(0)->signalWindow(30, 10); +} + +std::string csi(string seq) +{ + // Note: This doesn't do anything for the debug console currently. + // Somewhere, the TTY eats the control sequences. Still useful though. + string fullSeq = "\x1b["; + fullSeq += seq; + return fullSeq; +} + +void printKeyEvent(KEY_EVENT_RECORD keyEvent) +{ + // If printable: + if (keyEvent.uChar.AsciiChar > ' ' && keyEvent.uChar.AsciiChar != '\x7f') + { + wprintf(L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char: %c (0x%x) KeyState: 0x%x\r\n", + keyEvent.bKeyDown, + keyEvent.wRepeatCount, + keyEvent.wVirtualKeyCode, + keyEvent.wVirtualScanCode, + keyEvent.uChar.AsciiChar, + keyEvent.uChar.AsciiChar, + keyEvent.dwControlKeyState); + } + else + { + wprintf(L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char:(0x%x) KeyState: 0x%x\r\n", + keyEvent.bKeyDown, + keyEvent.wRepeatCount, + keyEvent.wVirtualKeyCode, + keyEvent.wVirtualScanCode, + keyEvent.uChar.AsciiChar, + keyEvent.dwControlKeyState); + } + +} + +void toPrintableBuffer(char c, char* printBuffer, int* printCch) +{ + if (c == '\x1b') + { + printBuffer[0] = '^'; + printBuffer[1] = '['; + *printCch = 2; + } + else if (c == '\x03') { + printBuffer[0] = '^'; + printBuffer[1] = 'C'; + *printCch = 2; + } + else if (c == '\x0') + { + printBuffer[0] = '\\'; + printBuffer[1] = '0'; + *printCch = 2; + } + else if (c == '\r') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'r'; + *printCch = 2; + } + else if (c == '\n') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'n'; + *printCch = 2; + } + else if (c < '\x20') + { + printBuffer[0] = '^'; + printBuffer[1] = c+0x40; + *printCch = 2; + } + else if (c == '\x7f') + { + printBuffer[0] = '\\'; + printBuffer[1] = 'x'; + printBuffer[2] = '7'; + printBuffer[3] = 'f'; + *printCch = 4; + } + else + { + printBuffer[0] = (char)c; + *printCch = 1; + } +} + +std::string toPrintableString(std::string& inString) +{ + std::string retval = ""; + for (size_t i = 0; i < inString.length(); i++) + { + char c = inString[i]; + if (c < '\x20') + { + retval += "^"; + char actual = (c + 0x40); + retval += std::string(1, actual); + } + else if (c == '\x7f') + { + retval += "\\x7f"; + } + else if (c == '\x20') + { + retval += "SPC"; + } + else + { + retval += std::string(1, c); + } + } + return retval; +} + +void doResize(const unsigned short width, const unsigned short height) +{ + lastTerminalWidth = width; + lastTerminalHeight = height; + + for (auto console : consoles) + { + console->Resize(height, width); + } +} + +void handleResize() +{ + CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 }; + csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); + bool fSuccess = !!GetConsoleScreenBufferInfoEx(hOut, &csbiex); + if (fSuccess) + { + SMALL_RECT srViewport = csbiex.srWindow; + + unsigned short width = srViewport.Right - srViewport.Left + 1; + unsigned short height = srViewport.Bottom - srViewport.Top + 1; + + doResize(width, height); + } +} + +void handleManyEvents(const INPUT_RECORD* const inputBuffer, int cEvents) +{ + char* const buffer = new char[cEvents]; + char* const printableBuffer = new char[cEvents * 4]; + memset(buffer, 0, cEvents); + memset(printableBuffer, 0, cEvents * 4); + + char* nextBuffer = buffer; + char* nextPrintable = printableBuffer; + int bufferCch = 0; + int printableCch = 0; + + for (int i = 0; i < cEvents; ++i) + { + INPUT_RECORD event = inputBuffer[i]; + + if (event.EventType == KEY_EVENT) + { + KEY_EVENT_RECORD keyEvent = event.Event.KeyEvent; + if (keyEvent.bKeyDown) + { + const char c = keyEvent.uChar.AsciiChar; + + if (c == '\0' && keyEvent.wVirtualScanCode != 0) + { + // This is a special keyboard key that was pressed, not actually NUL + continue; + } + if (doUnicode) + { + switch(c) + { + case '1': + lang = TEST_LANG_CYRILLIC; + break; + case '2': + lang = TEST_LANG_CHINESE; + break; + case '3': + lang = TEST_LANG_JAPANESE; + break; + case '4': + lang = TEST_LANG_KOREAN; + break; + case '#': + lang = TEST_LANG_GOOD_POUND; + break; + case '$': + lang = TEST_LANG_BAD_POUND; + break; + default: + doUnicode = false; + lang = TEST_LANG_NONE; + break; + } + } + else if (!prefixPressed) + { + if (c == '\x2') + { + prefixPressed = true; + } + else + { + *nextBuffer = c; + nextBuffer++; + bufferCch++; + } + } + else + { + switch(c) + { + case 'n': + case '\t': + nextConsole(); + break; + case 't': + newConsole(); + nextConsole(); + break; + case 'u': + doUnicode = true; + break; + case 'r': + signalConsole(); + break; + default: + *nextBuffer = c; + nextBuffer++; + bufferCch++; + } + prefixPressed = false; + } + int numPrintable = 0; + toPrintableBuffer(c, nextPrintable, &numPrintable); + nextPrintable += numPrintable; + printableCch += numPrintable; + } + } + else if (event.EventType == WINDOW_BUFFER_SIZE_EVENT) + { + WINDOW_BUFFER_SIZE_RECORD resize = event.Event.WindowBufferSizeEvent; + handleResize(); + } + + } + + if (bufferCch > 0) + { + std::string vtseq = std::string(buffer, bufferCch); + std::string printSeq = std::string(printableBuffer, printableCch); + + getConsole()->WriteInput(vtseq); + PrintInputToDebug(vtseq); + } + if (doUnicode && lang != TEST_LANG_NONE) + { + std::string str; + switch(lang) + { + case TEST_LANG_CYRILLIC: + str = "Лорем ипсум долор сит амет, пер цлита поссит ех, ат мунере фабулас петентиум сит."; + break; + case TEST_LANG_CHINESE: + str = "側経意責家方家閉討店暖育田庁載社転線宇。"; + break; + case TEST_LANG_JAPANESE: + str = "旅ロ京青利セムレ弱改フヨス波府かばぼ意送でぼ調掲察たス日西重ケアナ住橋ユムミク順待ふかんぼ人奨貯鏡すびそ。"; + break; + case TEST_LANG_KOREAN: + str = "국민경제의 발전을 위한 중요정책의 수립에 관하여 대통령의 자문에 응하기 위하여 국민경제자문회의를 둘 수 있다."; + break; + case TEST_LANG_GOOD_POUND: + str = "\xc2\xa3"; // UTF-8 £ + break; + case TEST_LANG_BAD_POUND: + str = "\xa3"; // UTF-16 £ + break; + default: + str = ""; + break; + } + getConsole()->WriteInput(str); + PrintInputToDebug(str); + + doUnicode = false; + lang = TEST_LANG_NONE; + } +} + +void PrintInputToDebug(std::string& rawInput) +{ + if (debug != nullptr) + { + std::string printable = toPrintableString(rawInput); + std::stringstream ss; + ss << "Input \"" << printable << "\" [" << rawInput.length() << "]\n"; + std::string output = ss.str(); + debug->WriteInput(output); + } +} + +void PrintOutputToDebug(std::string& rawOutput) +{ + if (debug != nullptr) + { + std::string printable = toPrintableString(rawOutput); + std::stringstream ss; + ss << printable << "\n"; + std::string output = ss.str(); + debug->WriteInput(output); + } +} + +void SetupOutput() +{ + DWORD dwMode = 0; + THROW_LAST_ERROR_IF(!GetConsoleMode(hOut, &dwMode)); + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + dwMode |= DISABLE_NEWLINE_AUTO_RETURN; + THROW_LAST_ERROR_IF(!SetConsoleMode(hOut, dwMode)); +} +void SetupInput() +{ + DWORD dwInMode = 0; + GetConsoleMode(hIn, &dwInMode); + dwInMode = ENABLE_VIRTUAL_TERMINAL_INPUT; + SetConsoleMode(hIn, dwInMode); +} + +DWORD InputThread(LPVOID /*lpParameter*/) +{ + // Because the input thread ends up owning the lifetime of the application, + // Set/restore the CP here. + + unsigned int launchOutputCP = GetConsoleOutputCP(); + unsigned int launchCP = GetConsoleCP(); + THROW_LAST_ERROR_IF(!SetConsoleOutputCP(CP_UTF8)); + THROW_LAST_ERROR_IF(!SetConsoleCP(CP_UTF8)); + auto restore = wil::scope_exit([&] + { + SetConsoleOutputCP(launchOutputCP); + SetConsoleCP(launchCP); + }); + + + for (;;) + { + INPUT_RECORD rc[256]; + DWORD dwRead = 0; + // Not to future self: You can't read utf-8 from the console yet. + bool fSuccess = !!ReadConsoleInput(hIn, rc, 256, &dwRead); + if (fSuccess) + { + handleManyEvents(rc, dwRead); + } + else + { + exit(GetLastError()); + } + } +} + + +void CreateIOThreads() +{ + // The VtConsoles themselves handle their output threads. + + DWORD dwInputThreadId = (DWORD) -1; + HANDLE hInputThread = CreateThread(nullptr, + 0, + (LPTHREAD_START_ROUTINE)InputThread, + nullptr, + 0, + &dwInputThreadId); + hInputThread; +} + + +BOOL CtrlHandler( DWORD fdwCtrlType ) +{ + switch( fdwCtrlType ) + { + // Handle the CTRL-C signal. + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + return true; + } + + return false; +} + +// this function has unreachable code due to its unusual lifetime. We +// disable the warning about it here. +#pragma warning(push) +#pragma warning(disable:4702) +int __cdecl wmain(int argc, WCHAR* argv[]) +{ + // initialize random seed: + srand((unsigned int)time(NULL)); + SetConsoleCtrlHandler( (PHANDLER_ROUTINE) CtrlHandler, TRUE ); + + hOut = GetStdHandle(STD_OUTPUT_HANDLE); + hIn = GetStdHandle(STD_INPUT_HANDLE); + + bool fUseDebug = false; + + if (argc > 1) + { + for (int i = 0; i < argc; ++i) + { + std::wstring arg = argv[i]; + if (arg == std::wstring(L"--headless")) + { + g_headless = true; + } + if (arg == std::wstring(L"--conpty")) + { + g_useConpty = true; + } + else if (arg == std::wstring(L"--debug")) + { + fUseDebug = true; + } + else if (arg == std::wstring(L"--out") && i+1 < argc) + { + g_useOutfile = true; + outfile = argv[i+1]; + i++; + } + } + } + + if (g_useConpty) + { + printf("Launching vtpipeterm with conpty API...\n"); + Sleep(1000); + } + + if (g_useOutfile) + { + hOutFile = CreateFileW(outfile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL , NULL); + if (hOutFile == INVALID_HANDLE_VALUE) + { + printf("Failed to open outfile (%ls) for writing\n", outfile.c_str()); + Sleep(1000); + exit(0); + + } + } + + SetupOutput(); + SetupInput(); + + // handleResize will get our initial terminal dimensions. + handleResize(); + + newConsole(); + getConsole()->activate(); + CreateIOThreads(); + + if (fUseDebug) + { + // Create a debug console for writting debugging output to. + debug = new VtConsole(DebugReadCallback, false, false, {80,32}); + // Echo stdin to stdout, but ignore newlines (so cat doesn't echo the input) + // debug->spawn(L"ubuntu run tr -d '\n' | cat -sA"); + debug->spawn(L"wsl tr -d '\n' | cat -sA"); + debug->activate(); + } + + // Exit the thread so the CRT won't clean us up and kill. The IO thread owns the lifetime now. + ExitThread(S_OK); + // We won't hit this. The ExitThread above will kill the caller at this point. + assert(false); + return 0; +} +#pragma warning(pop) diff --git a/src/tools/vtpipeterm/res.rc b/src/tools/vtpipeterm/res.rc new file mode 100644 index 000000000..510705c8c --- /dev/null +++ b/src/tools/vtpipeterm/res.rc @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include + +#define VER_FILETYPE VFT_APP +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "Acts as a terminal for connecting to vtio conhosts and render to the current console window." +#define VER_INTERNALNAME_STR "vtpipeterm" +#define VER_ORIGINALFILENAME_STR "vtpipeterm.EXE" + + +#include + + diff --git a/src/tools/vtpipeterm/sources b/src/tools/vtpipeterm/sources new file mode 100644 index 000000000..068ccf421 --- /dev/null +++ b/src/tools/vtpipeterm/sources @@ -0,0 +1,34 @@ +MSC_WARNING_LEVEL=/W4 /WX + +USER_C_FLAGS = $(USER_C_FLAGS) /std:c++17 + +TARGETNAME=vtpipeterm +TARGETTYPE=PROGRAM + +UMTYPE=console +UMENTRY=wmain + +TEST_CODE=1 +USE_UNICRT = 1 +USE_MSVCRT = 1 + +USE_STL = 1 +STL_VER = STL_VER_CURRENT +USE_NATIVE_EH = 1 + +C_DEFINES=-DUNICODE -D__INSIDE_WINDOWS + +TARGETLIBS=\ + $(MINWIN_SDK_LIB_PATH)\ntdll.lib \ + $(ONECORE_SDK_LIB_VPATH)\onecore.lib + +SOURCES=main.cpp \ + VtConsole.cpp \ + res.rc \ + +TARGET_DESTINATION=retail + +INCLUDES= \ + $(INCLUDES); \ + $(OBJ_PATH)\$(O); \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L) diff --git a/src/tools/vttests/burrito.py b/src/tools/vttests/burrito.py new file mode 100644 index 000000000..e9e1f6eb3 --- /dev/null +++ b/src/tools/vttests/burrito.py @@ -0,0 +1,36 @@ +# coding=utf-8 +################################################################################ +# # +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# # +################################################################################ +# MAKE SURE YOU SAVE THIS FILE AS UTF-8!!! + +""" +This is a dead simple script for testing some emoji in the console. +It's written in python, so the same script can easily be used in both a linux +terminal and the console without a lot of work. +This script has both some "simple" emoji - burrito, cheese, etc. and some more +complex ones - WOMAN COOK is actually two emoji with a zero width joiner. +""" +import sys +import time # time.sleep is in seconds +from common import * + +# Run this file with: +# python burrito.py +if __name__ == '__main__': + clear_all() + print('We are going to make a burrito:') + print(u"\U0001F32F") # Burrito + write('Here we have some components of a burrito:') + # POULTRY LEG, CHEESE WEDGE, HOT PEPPER, TOMATO + print(u"\U0001F357\U0001F9C0\U0001F336\U0001F345") + # woman cook and man cook emoji + print(u"👩‍🍳 packing them up ‍👨‍🍳") + sgr('92') + print(u'✔ Complete!') + sgr('0') + write('\n') + print(u'🌯🌯🌯🌯🌯🌯🌯🌯🌯🌯🌯') diff --git a/src/tools/vttests/common.py b/src/tools/vttests/common.py new file mode 100644 index 000000000..2ecd85414 --- /dev/null +++ b/src/tools/vttests/common.py @@ -0,0 +1,98 @@ +################################################################################ +# # +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# # +################################################################################ + +import sys, os +import time # time.sleep is in seconds + +if os.name == 'nt': + import ctypes + hOut = ctypes.windll.kernel32.GetStdHandle(-11) + out_modes = ctypes.c_uint32() + ENABLE_VT_PROCESSING = ctypes.c_uint32(0x0004) + ctypes.windll.kernel32.GetConsoleMode(hOut, ctypes.byref(out_modes)) + out_modes = ctypes.c_uint32(out_modes.value | 0x0004) + ctypes.windll.kernel32.SetConsoleMode(hOut, out_modes) + setcp_result = ctypes.windll.kernel32.SetConsoleOutputCP(65001) + if not setcp_result: + gle = ctypes.windll.kernel32.GetLastError() + print('SetConsoleOutputCP failed with error {}'.format(gle)) + exit(0) + setcp_result = ctypes.windll.kernel32.SetConsoleCP(65001) + if not setcp_result: + gle = ctypes.windll.kernel32.GetLastError() + print('SetConsoleCP failed with error {}'.format(gle)) + exit(0) + import codecs + codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + sys.stderr = codecs.getwriter('utf8')(sys.stderr) + +def write(s): + sys.stdout.write(s) + +def esc(seq): + write('\x1b{}'.format(seq)) + +def csi(seq): + sys.stdout.write('\x1b[{}'.format(seq)) + +def osc(seq): + sys.stdout.write('\x1b]{}\x07'.format(seq)) + +def cup(r=0, c=0): + csi('H') if (r==0 and c==0) else csi('{};{}H'.format(r, c)) + +def cupxy(x=0, y=0): + cup(y+1, x+1) + +def margins(top=0, bottom=0): + csi('{};{}r'.format(top, bottom)) + +def clear_all(): + cupxy(0,0) + csi('2J') + +def sgr(code=0): + csi('{}m'.format(code)) + +def sgr_n(seq=[]): + csi('{}m'.format(';'.join(str(code) for code in seq))) + +def tbc(): + """ + Clear all tabstops from the terminal. + Tab will take the cursor to the last column, then the first column. + """ + csi('3g') + +def ht(): + write('\t') + +def cbt(): + csi('Z') + +def hts(column=-1): + if column > 0: + csi(';{}H'.format(column)) + esc('H') + +def alt_buffer(): + csi('?1049h') + +def main_buffer(): + csi('?1049l') + +def flush(timeout=0): + sys.stdout.flush() + time.sleep(timeout) + +def set_color(index, r, g, b): + osc('4;{};rgb:{:02X}/{:02X}/{:02X}'.format(index, r, g, b)) + +if __name__ == '__main__': + clear_all() + print('This is the VT Test template.') diff --git a/src/tools/vttests/template.py b/src/tools/vttests/template.py new file mode 100644 index 000000000..93c2536b3 --- /dev/null +++ b/src/tools/vttests/template.py @@ -0,0 +1,17 @@ +################################################################################ +# # +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# # +################################################################################ + +import sys +import time # time.sleep is in seconds +from common import * + +# Run this file with: +# python name-of-file.py +if __name__ == '__main__': + clear_all() + print('This is the VT Test template.') + diff --git a/src/tools/vttests/test-unicode.py b/src/tools/vttests/test-unicode.py new file mode 100644 index 000000000..6b04683d1 --- /dev/null +++ b/src/tools/vttests/test-unicode.py @@ -0,0 +1,92 @@ +# coding=utf-8 +################################################################################ +# # +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# # +################################################################################ +# MAKE SURE YOU SAVE THIS FILE AS UTF-8!!! + +""" +This is a longer sctipt for testing various unicode characters that someone +might want. There are a bunch of chars that are more typical, "normal" +characters, and then there's a decent number of emoji. +""" + +import sys +import time # time.sleep is in seconds +from common import * + +# Run this file with: +# python test-unicode.py +if __name__ == '__main__': + clear_all() + print('Here\'s A bunch of chars that should work:') + write(u'tick: ✔ ') + write(u'cross: ✖ ') + print(u'star: ★ ') + write(u'square: ▇ ') + write(u'squareSmall: ◻ ') + print(u'squareSmallFilled: ◼ ') + print(u'play: ▶ ') + write(u'circle: ◯ ') + write(u'circleFilled: ◉ ') + write(u'circleDotted: ◌ ') + print(u'circleDouble: ◎ ') + write(u'circleCircle: ⓞ ') + write(u'circleCross: ⓧ ') + write(u'circlePipe: Ⓘ ') + write(u'circleQuestionMark: ?⃝ ') + print(u'bullet: ● ') + write(u'dot: ․ ') + write(u'line: ─ ') + print(u'ellipsis: … ') + print(u'pointer: ❯ ') + print(u'pointerSmall: › ') + write(u'info: ℹ ') + write(u'warning: ⚠ ') + print(u'hamburger: ☰ ') + print(u'smiley: ㋡ ') + print(u'mustache: ෴ ') + print(u'heart: ♥ ') + write(u'arrowUp: ↑ ') + write(u'arrowDown: ↓ ') + write(u'arrowLeft: ← ') + print(u'arrowRight: → ') + write(u'radioOn: ◉ ') + print(u'radioOff: ◯ ') + write(u'checkboxOn: ☒ ') + print(u'checkboxOff: ☐ ') + write(u'oneHalf: ½ ') + write(u'oneThird: ⅓ ') + write(u'oneQuarter: ¼ ') + print(u'oneFifth: ⅕ ') + write(u'oneSixth: ⅙ ') + write(u'oneSeventh: ⅐ ') + write(u'oneEighth: ⅛ ') + print(u'oneNinth: ⅑ ') + write(u'oneTenth: ⅒ ') + write(u'twoThirds: ⅔ ') + write(u'twoFifths: ⅖ ') + print(u'threeQuarters: ¾ ') + write(u'threeFifths: ⅗ ') + write(u'threeEighths: ⅜ ') + write(u'fourFifths: ⅘ ') + print(u'fiveSixths: ⅚ ') + write(u'fiveEighths: ⅝ ') + print(u'sevenEighths: ⅞ ') + + print('Emoji:') + write(u'beer: 🍺 ') + print(u'burrito: 🌯 ') + write(u'Red Heart: ❤ ') + print(u'Fire: 🔥 ') + write(u'Face With Tears of Joy: 😂 ') + print(u'Smiling Face With Heart-Eyes: 😍 ') + write(u'Thinking Face: 🤔 ') + print(u'Smiling Face With Smiling Eyes: 😊 ') + print(u'Smiling Face With Hearts: 🥰 ') + write(u'Thumbs Up: 👍 ') + write(u'Heavy Check Mark: ✔ ') + write('\n') + diff --git a/src/tsf/ConsoleTSF.cpp b/src/tsf/ConsoleTSF.cpp new file mode 100644 index 000000000..6b8ba9dd3 --- /dev/null +++ b/src/tsf/ConsoleTSF.cpp @@ -0,0 +1,529 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "TfConvArea.h" +#include "TfEditSes.h" + +/* 626761ad-78d2-44d2-be8b-752cf122acec */ +const GUID GUID_APPLICATION = { 0x626761ad, 0x78d2, 0x44d2, {0xbe, 0x8b, 0x75, 0x2c, 0xf1, 0x22, 0xac, 0xec} }; + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::Initialize +// +//---------------------------------------------------------------------------- + +#define Init_CheckResult() if (FAILED(hr)) { Uninitialize(); return hr; } + +[[nodiscard]] +HRESULT CConsoleTSF::Initialize() +{ + HRESULT hr; + + if (_spITfThreadMgr) + { + return S_FALSE; + } + + // Activate per-thread Cicero in custom UI mode (TF_TMAE_UIELEMENTENABLEDONLY). + + hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + Init_CheckResult(); + _fCoInitialized = TRUE; + + hr = _spITfThreadMgr.CoCreateInstance(CLSID_TF_ThreadMgr); + Init_CheckResult(); + + hr = _spITfThreadMgr->Activate(&_tid); + Init_CheckResult(); + + // Create Cicero document manager and input context. + + hr = _spITfThreadMgr->CreateDocumentMgr(&_spITfDocumentMgr); + Init_CheckResult(); + + TfEditCookie ecTmp; + hr = _spITfDocumentMgr->CreateContext(_tid, + 0, + static_cast(this), + &_spITfInputContext, + &ecTmp); + Init_CheckResult(); + + // Set the context owner before attaching the context to the doc. + CComQIPtr spSrcIC(_spITfInputContext); + hr = spSrcIC->AdviseSink(IID_ITfContextOwner, static_cast(this), &_dwContextOwnerCookie); + Init_CheckResult(); + + hr = _spITfDocumentMgr->Push(_spITfInputContext); + Init_CheckResult(); + + // Collect the active keyboard layout info. + + CComPtr spITfProfilesMgr; + hr = spITfProfilesMgr.CoCreateInstance(CLSID_TF_InputProcessorProfiles); + if (SUCCEEDED(hr)) + { + TF_INPUTPROCESSORPROFILE ipp; + hr = spITfProfilesMgr->GetActiveProfile(GUID_TFCAT_TIP_KEYBOARD, &ipp); + if (SUCCEEDED(hr)) + { + OnActivated(ipp.dwProfileType, ipp.langid, ipp.clsid, ipp.catid, + ipp.guidProfile, ipp.hkl, ipp.dwFlags); + } + } + Init_CheckResult(); + + // Setup some useful Cicero event sinks and callbacks. + + CComQIPtr spSrcTIM(_spITfThreadMgr); + CComQIPtr spSrcICS(_spITfInputContext); + + hr = (spSrcTIM && spSrcIC && spSrcICS) ? S_OK : E_FAIL; + Init_CheckResult(); + + hr = spSrcTIM->AdviseSink(IID_ITfInputProcessorProfileActivationSink, + static_cast(this), + &_dwActivationSinkCookie); + Init_CheckResult(); + + hr = spSrcTIM->AdviseSink(IID_ITfUIElementSink, static_cast(this), &_dwUIElementSinkCookie); + Init_CheckResult(); + + hr = spSrcIC->AdviseSink(IID_ITfTextEditSink, static_cast(this), &_dwTextEditSinkCookie); + Init_CheckResult(); + + hr = spSrcICS->AdviseSingleSink(_tid, IID_ITfCleanupContextSink, static_cast(this)); + Init_CheckResult(); + + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::Uninitialize() +// +//---------------------------------------------------------------------------- + +void CConsoleTSF::Uninitialize() +{ + // Destroy the current conversion area object + + if (_pConversionArea) + { + delete _pConversionArea; + _pConversionArea = NULL; + } + + // Detach Cicero event sinks. + CComQIPtr spSrcICS(_spITfInputContext); + if (spSrcICS) + { + spSrcICS->UnadviseSingleSink(_tid, IID_ITfCleanupContextSink); + } + + // Associate the document\context with the console window. + + CComQIPtr spSrcTIM(_spITfThreadMgr); + if (spSrcTIM) + { + if (_dwUIElementSinkCookie) + { + spSrcTIM->UnadviseSink(_dwUIElementSinkCookie); + } + if (_dwActivationSinkCookie) + { + spSrcTIM->UnadviseSink(_dwActivationSinkCookie); + } + } + _dwUIElementSinkCookie = 0; + _dwActivationSinkCookie = 0; + + CComQIPtr spSrcIC(_spITfInputContext); + if (spSrcIC) + { + if (_dwContextOwnerCookie) + { + spSrcIC->UnadviseSink(_dwContextOwnerCookie); + } + if (_dwTextEditSinkCookie) + { + spSrcIC->UnadviseSink(_dwTextEditSinkCookie); + } + } + _dwContextOwnerCookie = 0; + _dwTextEditSinkCookie = 0; + + // Clear the Cicero reference to our document manager. + + if (_spITfThreadMgr && _spITfDocumentMgr) + { + CComPtr spDocMgr; + _spITfThreadMgr->AssociateFocus(_hwndConsole, NULL, &spDocMgr); + } + + // Dismiss the input context and document manager. + + if (_spITfDocumentMgr) + { + _spITfDocumentMgr->Pop(TF_POPF_ALL); + } + + _spITfInputContext.Release(); + _spITfDocumentMgr.Release(); + + // Deactivate per-thread Cicero and uninitialize COM. + + if (_spITfThreadMgr) + { + _spITfThreadMgr->Deactivate(); + _spITfThreadMgr.Release(); + } + if (_fCoInitialized) + { + ::CoUninitialize(); + _fCoInitialized = FALSE; + } +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::IUnknown::QueryInterface +// CConsoleTSF::IUnknown::AddRef +// CConsoleTSF::IUnknown::Release +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::QueryInterface(REFIID riid, void** ppvObj) +{ + if (!ppvObj) + { + return E_FAIL; + } + *ppvObj = NULL; + + if (IsEqualIID(riid, IID_ITfCleanupContextSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_ITfContextOwnerCompositionSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualIID(riid, IID_ITfUIElementSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualIID(riid, IID_ITfContextOwner)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualIID(riid, IID_ITfInputProcessorProfileActivationSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualIID(riid, IID_ITfTextEditSink)) + { + *ppvObj = static_cast(this); + } + else if (IsEqualGUID(riid, IID_IUnknown)) + { + *ppvObj = this; + } + if (*ppvObj) + { + AddRef(); + } + return (*ppvObj) ? S_OK : E_NOINTERFACE; +} + +STDAPI_(ULONG) CConsoleTSF::AddRef() +{ + return InterlockedIncrement(&_cRef); +} + +STDAPI_(ULONG) CConsoleTSF::Release() +{ + ULONG cr = InterlockedDecrement(&_cRef); + if (cr == 0) + { + if (g_pConsoleTSF == this) + { + g_pConsoleTSF = NULL; + } + delete this; + } + return cr; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfCleanupContextSink::OnCleanupContext +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::OnCleanupContext(TfEditCookie ecWrite, ITfContext* pic) +{ + // + // Remove GUID_PROP_COMPOSING + // + CComPtr prop; + if (SUCCEEDED(pic->GetProperty(GUID_PROP_COMPOSING, &prop))) + { + CComPtr enumranges; + CComPtr rangeFull; + if (SUCCEEDED(prop->EnumRanges(ecWrite, &enumranges, rangeFull))) + { + CComPtr rangeTmp; + while (enumranges->Next(1, &rangeTmp, NULL) == S_OK) + { + VARIANT var; + VariantInit(&var); + prop->GetValue(ecWrite, rangeTmp, &var); + if ((var.vt == VT_I4) && (var.lVal != 0)) + { + prop->Clear(ecWrite, rangeTmp); + } + rangeTmp.Release(); + } + } + } + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfContextOwnerCompositionSink::OnStartComposition +// CConsoleTSF::ITfContextOwnerCompositionSink::OnUpdateComposition +// CConsoleTSF::ITfContextOwnerCompositionSink::OnEndComposition +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::OnStartComposition(ITfCompositionView* pCompView, BOOL* pfOk) +{ + if (!_pConversionArea || (_cCompositions > 0 && (!_fModifyingDoc))) + { + *pfOk = FALSE; + } + else + { + *pfOk = TRUE; + // Ignore compositions triggered by our own edit sessions + // (i.e. when the application is the composition owner) + CLSID clsidCompositionOwner = GUID_APPLICATION; + pCompView->GetOwnerClsid(&clsidCompositionOwner); + if (!IsEqualGUID(clsidCompositionOwner, GUID_APPLICATION)) + { + _cCompositions++; + if (_cCompositions == 1) + { + LOG_IF_FAILED(ImeStartComposition()); + } + } + } + return S_OK; +} + +STDMETHODIMP CConsoleTSF::OnUpdateComposition(ITfCompositionView* /*pComp*/, ITfRange*) +{ + return S_OK; +} + +STDMETHODIMP CConsoleTSF::OnEndComposition(ITfCompositionView* pCompView) +{ + if (!_cCompositions || !_pConversionArea) + { + return E_FAIL; + } + // Ignore compositions triggered by our own edit sessions + // (i.e. when the application is the composition owner) + CLSID clsidCompositionOwner = GUID_APPLICATION; + pCompView->GetOwnerClsid(&clsidCompositionOwner); + if (!IsEqualGUID(clsidCompositionOwner, GUID_APPLICATION)) + { + _cCompositions--; + if (!_cCompositions) + { + LOG_IF_FAILED(_OnCompleteComposition()); + LOG_IF_FAILED(ImeEndComposition()); + } + } + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfTextEditSink::OnEndEdit +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::OnEndEdit(ITfContext *pInputContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord) +{ + if (_cCompositions && _pConversionArea && _HasCompositionChanged(pInputContext, ecReadOnly, pEditRecord)) + { + LOG_IF_FAILED(_OnUpdateComposition()); + } + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfInputProcessorProfileActivationSink::OnActivated +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::OnActivated(DWORD /*dwProfileType*/, LANGID /*langid*/, REFCLSID /*clsid*/, + REFGUID catid, REFGUID /*guidProfile*/, HKL /*hkl*/, DWORD dwFlags) +{ + if (!(dwFlags & TF_IPSINK_FLAG_ACTIVE)) + { + return S_OK; + } + if (!IsEqualGUID(catid, GUID_TFCAT_TIP_KEYBOARD)) + { + // Don't care for non-keyboard profiles. + return S_OK; + } + + try + { + CreateConversionArea(); + } + CATCH_RETURN(); + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfUIElementSink::BeginUIElement +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::BeginUIElement(DWORD /*dwUIElementId*/, BOOL *pbShow) +{ + *pbShow = TRUE; + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfUIElementSink::UpdateUIElement +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::UpdateUIElement(DWORD /*dwUIElementId*/) +{ + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::ITfUIElementSink::EndUIElement +// +//---------------------------------------------------------------------------- + +STDMETHODIMP CConsoleTSF::EndUIElement(DWORD /*dwUIElementId*/) +{ + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::CreateConversionAreaService +// +//---------------------------------------------------------------------------- + + CConversionArea* CConsoleTSF::CreateConversionArea() +{ + BOOL fHadConvArea = (_pConversionArea != NULL); + + if (!_pConversionArea) + { + _pConversionArea = new CConversionArea(); + } + + // Associate the document\context with the console window. + if (!fHadConvArea) + { + CComPtr spPrevDocMgr; + _spITfThreadMgr->AssociateFocus(_hwndConsole, _pConversionArea ? _spITfDocumentMgr : NULL, &spPrevDocMgr); + } + + return _pConversionArea; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::OnUpdateComposition() +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CConsoleTSF::_OnUpdateComposition() +{ + if (_fEditSessionRequested) + { + return S_FALSE; + } + + HRESULT hr = E_OUTOFMEMORY; + CEditSessionUpdateCompositionString* pEditSession = new(std::nothrow) CEditSessionUpdateCompositionString(); + if (pEditSession) + { + // Can't use TF_ES_SYNC because called from OnEndEdit. + _fEditSessionRequested = TRUE; + _spITfInputContext->RequestEditSession(_tid, pEditSession, TF_ES_READWRITE, &hr); + if (FAILED(hr)) + { + pEditSession->Release(); + _fEditSessionRequested = FALSE; + } + } + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::OnCompleteComposition() +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CConsoleTSF::_OnCompleteComposition() +{ + // Update the composition area. + + HRESULT hr = E_OUTOFMEMORY; + CEditSessionCompositionComplete* pEditSession = new(std::nothrow) CEditSessionCompositionComplete(); + if (pEditSession) + { + // The composition could have been finalized because of a caret move, therefore it must be + // inserted synchronously while at the orignal caret position.(TF_ES_SYNC is ok for a nested RO session). + _spITfInputContext->RequestEditSession(_tid, pEditSession, TF_ES_READ | TF_ES_SYNC, &hr); + if (FAILED(hr)) + { + pEditSession->Release(); + } + } + + // Cleanup (empty the context range) after the last composition, unless a new one has started. + if (!_fCleanupSessionRequested) + { + _fCleanupSessionRequested = TRUE; + CEditSessionCompositionCleanup* pEditSessionCleanup = new(std::nothrow) CEditSessionCompositionCleanup(); + if (pEditSessionCleanup) + { + // Can't use TF_ES_SYNC because requesting RW while called within another session. + // For the same reason, must use explicit TF_ES_ASYNC, or the request will be rejected otherwise. + _spITfInputContext->RequestEditSession(_tid, pEditSessionCleanup, TF_ES_READWRITE | TF_ES_ASYNC, &hr); + if (FAILED(hr)) + { + pEditSessionCleanup->Release(); + _fCleanupSessionRequested = FALSE; + } + } + } + return hr; +} diff --git a/src/tsf/ConsoleTSF.h b/src/tsf/ConsoleTSF.h new file mode 100644 index 000000000..825541d74 --- /dev/null +++ b/src/tsf/ConsoleTSF.h @@ -0,0 +1,211 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfContext.h + +Abstract: + + This file defines the CConsoleTSF Interface Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +class CConversionArea; + +class CConsoleTSF final: + public ITfContextOwner, + public ITfContextOwnerCompositionSink, + public ITfInputProcessorProfileActivationSink, + public ITfUIElementSink, + public ITfCleanupContextSink, + public ITfTextEditSink +{ +public: + CConsoleTSF(HWND hwndConsole, + GetSuggestionWindowPos pfnPosition) : + _hwndConsole(hwndConsole), + _pfnPosition(pfnPosition), + _cRef(1), + _tid() + { + } + + virtual ~CConsoleTSF() + { + } + [[nodiscard]] + HRESULT Initialize(); + void Uninitialize(); + +public: + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP_(ULONG) Release(void); + + // ITfContextOwner + STDMETHODIMP GetACPFromPoint(const POINT*, DWORD, LONG *pCP) + { + if (pCP) + { + *pCP = 0; + } + + return S_OK; + } + + + STDMETHODIMP GetScreenExt(RECT *pRect) + { + if (pRect) + { + *pRect = _pfnPosition(); + } + + return S_OK; + } + + STDMETHODIMP GetTextExt(LONG, LONG, RECT *pRect, BOOL *pbClipped) + { + if (pRect) + { + GetScreenExt(pRect); + } + + if (pbClipped) + { + *pbClipped = FALSE; + } + + return S_OK; + } + + STDMETHODIMP GetStatus(TF_STATUS *pTfStatus) + { + if (pTfStatus) + { + pTfStatus->dwDynamicFlags = 0; + pTfStatus->dwStaticFlags = TF_SS_TRANSITORY; + } + return pTfStatus ? S_OK : E_INVALIDARG; + } + STDMETHODIMP GetWnd(HWND* phwnd) + { + *phwnd = _hwndConsole; + return S_OK; + } + STDMETHODIMP GetAttribute(REFGUID, VARIANT*) + { return E_NOTIMPL; } + + // ITfContextOwnerCompositionSink methods + STDMETHODIMP OnStartComposition(ITfCompositionView *pComposition, BOOL *pfOk); + STDMETHODIMP OnUpdateComposition(ITfCompositionView *pComposition, ITfRange *pRangeNew); + STDMETHODIMP OnEndComposition(ITfCompositionView* pComposition); + + // ITfInputProcessorProfileActivationSink + STDMETHODIMP OnActivated(DWORD dwProfileType, LANGID langid, REFCLSID clsid, + REFGUID catid, REFGUID guidProfile, HKL hkl, DWORD dwFlags); + + // ITfUIElementSink methods + STDMETHODIMP BeginUIElement(DWORD dwUIELementId, BOOL *pbShow); + STDMETHODIMP UpdateUIElement(DWORD dwUIELementId); + STDMETHODIMP EndUIElement(DWORD dwUIELementId); + + // ITfCleanupContextSink methods + STDMETHODIMP OnCleanupContext(TfEditCookie ecWrite, ITfContext *pic); + + // ITfTextEditSink methods + STDMETHODIMP OnEndEdit(ITfContext *pInputContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord); + +public: + CConversionArea* CreateConversionArea(); + CConversionArea* GetConversionArea() { return _pConversionArea; } + ITfContext* GetInputContext() { return _spITfInputContext; } + HWND GetConsoleHwnd() { return _hwndConsole; } + TfClientId GetTfClientId() { return _tid; } + BOOL IsInComposition() { return (_cCompositions > 0); } + void OnEditSession() { _fEditSessionRequested = FALSE; } + BOOL IsPendingCompositionCleanup() { return _fCleanupSessionRequested || _fCompositionCleanupSkipped; } + void OnCompositionCleanup(BOOL bSucceeded) + { + _fCleanupSessionRequested = FALSE; + _fCompositionCleanupSkipped = !bSucceeded; + } + void SetModifyingDocFlag(BOOL fSet) { _fModifyingDoc = fSet; } + void SetFocus(BOOL fSet) + { + if (!fSet && _cCompositions) + { + // Close (terminate) any open compositions when losing the input focus. + CComQIPtr spCompositionServices(_spITfInputContext); + if (spCompositionServices) + { + spCompositionServices->TerminateComposition(NULL); + } + } + } + + // A workaround for a MS Korean IME scenario where the IME appends a whitespace + // composition programmatically right after completing a keyboard input composition. + // Since post-composition clean-up is an async operation, the programmatic whitespace + // composition gets completed before the previous composition cleanup happened, + // and this results in a double insertion of the first composition. To avoid that, we'll + // store the length of the last completed composition here until it's cleaned up. + // (for simplicity, this patch doesn't provide a generic solution for all possible + // scenarios with subsequent synchronous compositions, only for the known 'append'). + long GetCompletedRangeLength() const { return _cchCompleted; } + void SetCompletedRangeLength(long cch) { _cchCompleted = cch; } + +private: + [[nodiscard]] + HRESULT _OnUpdateComposition(); + [[nodiscard]] + HRESULT _OnCompleteComposition(); + BOOL _HasCompositionChanged(ITfContext *pInputContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord); + +private: + // ref count. + DWORD _cRef; + + // Cicero stuff. + TfClientId _tid; + CComPtr _spITfThreadMgr; + CComPtr _spITfDocumentMgr; + CComPtr _spITfInputContext; + + // Event sink cookies. + DWORD _dwContextOwnerCookie = 0; + DWORD _dwUIElementSinkCookie = 0; + DWORD _dwTextEditSinkCookie = 0; + DWORD _dwActivationSinkCookie = 0; + + // Conversion area object for the languages. + CConversionArea* _pConversionArea = nullptr; + + // Console info. + HWND _hwndConsole; + GetSuggestionWindowPos _pfnPosition; + + // Miscellaneous flags + BOOL _fModifyingDoc = FALSE; // Set TRUE, when calls ITfRange::SetText + BOOL _fCoInitialized = FALSE; + BOOL _fEditSessionRequested = FALSE; + BOOL _fCleanupSessionRequested = FALSE; + BOOL _fCompositionCleanupSkipped = FALSE; + + int _cCompositions = 0; + long _cchCompleted = 0; // length of completed composition waiting for cleanup +}; + +extern CConsoleTSF* g_pConsoleTSF; diff --git a/src/tsf/StructureArray.h b/src/tsf/StructureArray.h new file mode 100644 index 000000000..2c20f7bdb --- /dev/null +++ b/src/tsf/StructureArray.h @@ -0,0 +1,230 @@ +////////////////////////////////////////////////////////////////////// +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// StructureArray.h +// +// CVoidStructureArray declaration. +// CStructureArray declaration. +// +////////////////////////////////////////////////////////////////////// + +#pragma once + +class CVoidStructureArray +{ +public: + CVoidStructureArray(int iElementSize, int iInitSize = 0) + { + ULONG ulInitSize; + ULONG ulElementSize; + ULONG ulBufferSize; + + _cElements = 0; + _pData = NULL; + _iAllocatedSize = 0; + + _iElementSize = iElementSize; + + if (iInitSize) + { + if (IntToULong(iInitSize, &ulInitSize) != S_OK) { + return; + } + if (IntToULong(_iElementSize, &ulElementSize) != S_OK) { + return; + } + if (ULongMult(ulInitSize, ulElementSize, &ulBufferSize) != S_OK) { + return; + } + + _pData = (BYTE *)LocalAlloc(LPTR, ulBufferSize); + if (_pData) + { + _iAllocatedSize = iInitSize; + } + } + } + virtual ~CVoidStructureArray() { LocalFree(_pData); } + + // + // CVoidStructureArray::GetAt() won't return invalid address or NULL address. + // This function should used to the same idea of element reference as i = a[...] + // + // Note that, + // CVoidStructureArray::GetAt() function should not _pData NULL check and iIndex validation. + // So caller should check empty element and out of index. + // + inline void* GetAt(int iIndex) const + { + FAIL_FAST_IF(!(iIndex >= 0)); + FAIL_FAST_IF(!(iIndex <= _cElements)); // there's code that uses the first invalid offset for loop termination + FAIL_FAST_IF(!(_pData != NULL)); + + return _pData + (iIndex * _iElementSize); + } + + BOOL InsertAt(int iIndex, int cElements = 1) + { + BYTE *pb; + int iSizeNew; + + // check integer overflow. + if ((iIndex < 0) || + (cElements < 0) || + (iIndex > _cElements) || + (_cElements > _cElements + cElements)) + { + return FALSE; + } + + // allocate space if necessary + if (_iAllocatedSize < _cElements + cElements) + { + // allocate 1.5x what we need to avoid future allocs + iSizeNew = max(_cElements + cElements, _cElements + _cElements / 2); + + UINT uiAllocSize; + if (FAILED(UIntMult(iSizeNew, _iElementSize, &uiAllocSize)) || ((int)uiAllocSize) < 0) + { + return FALSE; + } + + if ((pb = (_pData == NULL) ? (BYTE *)LocalAlloc(LPTR, uiAllocSize) : + (BYTE *)LocalReAlloc(_pData, uiAllocSize, LMEM_MOVEABLE | LMEM_ZEROINIT)) + == NULL) + { + return FALSE; + } + + _pData = pb; + _iAllocatedSize = iSizeNew; + } + + if (iIndex < _cElements) + { + // make room for the new addition + memmove(ElementPointer(iIndex + cElements), + ElementPointer(iIndex), + (_cElements - iIndex)*_iElementSize); + } + + _cElements += cElements; + FAIL_FAST_IF(!(_iAllocatedSize >= _cElements)); + + return TRUE; + } + + void RemoveAt(int iIndex, int cElements) + { + int iSizeNew; + + // check integer overflow. + if ((iIndex < 0) || + (cElements < 0) || + (iIndex > _cElements) || + (_cElements > _cElements + cElements)) + { + return; + } + + if (iIndex + cElements < _cElements) + { + // shift following eles left + memmove(ElementPointer(iIndex), + ElementPointer(iIndex + cElements), + (_cElements - iIndex - cElements)*_iElementSize); + } + + _cElements -= cElements; + + // free mem when array contents uses less than half alloc'd mem + iSizeNew = _iAllocatedSize / 2; + if (iSizeNew > _cElements) + { + CompactSize(iSizeNew); + } + } + + int Count() const { return _cElements; } + + void *Append(int cElements = 1) + { + return InsertAt(Count(), cElements) ? GetAt(Count()-cElements) : NULL; + } + + void Clear() + { + LocalFree(_pData); + _pData = NULL; + _cElements = _iAllocatedSize = 0; + } + +private: + void CompactSize(int iSizeNew) + { + BYTE *pb; + ULONG ulSizeNew; + ULONG ulElementSize; + ULONG ulBufferSize; + + FAIL_FAST_IF(!(iSizeNew <= _iAllocatedSize)); + FAIL_FAST_IF(!(_cElements <= iSizeNew)); + + if (iSizeNew == _iAllocatedSize) // LocalReAlloc will actually re-alloc! Don't let it. + return; + + if (IntToULong(iSizeNew, &ulSizeNew) != S_OK) { + return; + } + if (IntToULong(_iElementSize, &ulElementSize) != S_OK) { + return; + } + if (ULongMult(ulSizeNew, ulElementSize, &ulBufferSize) != S_OK) { + return; + } + if ((pb = (BYTE *)LocalReAlloc(_pData, ulBufferSize, LMEM_MOVEABLE | LMEM_ZEROINIT)) != NULL) + { + _pData = pb; + _iAllocatedSize = iSizeNew; + } + } + + BYTE *ElementPointer(int iIndex) const + { + return _pData + (iIndex * _iElementSize); + } + +private: + BYTE *_pData; // the actual array of data + int _cElements; // number of elements in the array + int _iAllocatedSize; // maximum allocated size (in void *'s) of the array + + int _iElementSize; // size of one element +}; + +////////////////////////////////////////////////////////////////////// +// +// CStructureArray declaration. +// +////////////////////////////////////////////////////////////////////// + +// +// typesafe version +// +template +class CStructureArray : public CVoidStructureArray +{ +public: + CStructureArray(int nInitSize = 0):CVoidStructureArray(sizeof(T), nInitSize) {} + + T *GetAt(int iIndex) { return (T *)CVoidStructureArray::GetAt(iIndex); } + + T *Append(int cElements = 1) + { + T *ret; + ret = (T *)CVoidStructureArray::Append(cElements); + return ret; + } +}; diff --git a/src/tsf/TfCatUtil.cpp b/src/tsf/TfCatUtil.cpp new file mode 100644 index 000000000..5f5db9a4d --- /dev/null +++ b/src/tsf/TfCatUtil.cpp @@ -0,0 +1,69 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfCatUtil.cpp + +Abstract: + + This file implements the CicCategoryMgr Class. + +Author: + +Revision History: + +Notes: + +--*/ + + +#include "precomp.h" +#include "TfCatUtil.h" + +//+--------------------------------------------------------------------------- +// +// CicCategoryMgr::ctor +// CicCategoryMgr::dtor +// +//---------------------------------------------------------------------------- + +CicCategoryMgr::CicCategoryMgr() +{ +} + +CicCategoryMgr::~CicCategoryMgr() +{ + if (m_pcat) { + m_pcat.Release(); + } +} + +//+--------------------------------------------------------------------------- +// +// CicCategoryMgr::GetGUIDFromGUIDATOM +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CicCategoryMgr::GetGUIDFromGUIDATOM(TfGuidAtom guidatom, GUID *pguid) +{ + return m_pcat->GetGUID(guidatom, pguid); +} + +//+--------------------------------------------------------------------------- +// +// CicCategoryMgr::InitCategoryInstance +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CicCategoryMgr::InitCategoryInstance( ) +{ + // + // Create ITfCategoryMgr instance. + // + return m_pcat.CoCreateInstance(CLSID_TF_CategoryMgr); +} diff --git a/src/tsf/TfCatUtil.h b/src/tsf/TfCatUtil.h new file mode 100644 index 000000000..349d46b26 --- /dev/null +++ b/src/tsf/TfCatUtil.h @@ -0,0 +1,40 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfCatUtil.h + +Abstract: + + This file defines the CicCategoryMgr Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +class CicCategoryMgr +{ +public: + CicCategoryMgr(); + virtual ~CicCategoryMgr(); + +public: + [[nodiscard]] + HRESULT GetGUIDFromGUIDATOM(TfGuidAtom guidatom, GUID *pguid); + [[nodiscard]] + HRESULT InitCategoryInstance(); + + inline ITfCategoryMgr* GetCategoryMgr() { return m_pcat; } + +private: + CComQIPtr m_pcat; +}; diff --git a/src/tsf/TfConvArea.cpp b/src/tsf/TfConvArea.cpp new file mode 100644 index 000000000..d42bc6969 --- /dev/null +++ b/src/tsf/TfConvArea.cpp @@ -0,0 +1,112 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfConvArea.cpp + +Abstract: + + This file implements the CConversionArea Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#include "precomp.h" +#include "ConsoleTSF.h" +#include "TfCtxtComp.h" +#include "TfConvArea.h" + +//+--------------------------------------------------------------------------- +// CConversionArea +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CConversionArea::DrawComposition(const CComBSTR& CompStr, + const std::vector& DisplayAttributes, + const DWORD CompCursorPos) +{ + // Set up colors. + static const std::array colors{ DEFAULT_COMP_ENTERED, + DEFAULT_COMP_ALREADY_CONVERTED, + DEFAULT_COMP_CONVERSION, + DEFAULT_COMP_YET_CONVERTED, + DEFAULT_COMP_INPUT_ERROR, + DEFAULT_COMP_INPUT_ERROR, + DEFAULT_COMP_INPUT_ERROR, + DEFAULT_COMP_INPUT_ERROR + }; + + std::wstring_view text(CompStr, CompStr.Length()); + + const auto encodedAttributes = _DisplayAttributesToEncodedAttributes(DisplayAttributes, + CompCursorPos); + + std::basic_string_view attributes(encodedAttributes.data(), encodedAttributes.size()); + std::basic_string_view colorArray(colors.data(), colors.size()); + + return ImeComposeData(text, attributes, colorArray); +} + +[[nodiscard]] +HRESULT CConversionArea::ClearComposition() +{ + return ImeClearComposeData(); +} + +[[nodiscard]] +HRESULT CConversionArea::DrawResult(const CComBSTR& ResultStr) +{ + std::wstring_view text(ResultStr, ResultStr.Length()); + + return ImeComposeResult(text); +} + +[[nodiscard]] +std::vector CConversionArea::_DisplayAttributesToEncodedAttributes(const std::vector& DisplayAttributes, + const DWORD CompCursorPos) +{ + std::vector encodedAttrs; + for (const auto& da : DisplayAttributes) + { + BYTE bAttr; + + if (da.bAttr == TF_ATTR_OTHER || da.bAttr > TF_ATTR_FIXEDCONVERTED) + { + bAttr = ATTR_TARGET_CONVERTED; + } + else + { + if (da.bAttr == TF_ATTR_INPUT_ERROR) + { + bAttr = ATTR_CONVERTED; + } + else + { + bAttr = (BYTE)da.bAttr; + } + } + encodedAttrs.emplace_back(bAttr); + } + + if (CompCursorPos != -1) + { + if (CompCursorPos == 0) + { + encodedAttrs[CompCursorPos] |= (BYTE)CONIME_CURSOR_LEFT; // special handling for ConSrv... 0x20 = COMMON_LVB_GRID_SINGLEFLAG + COMMON_LVB_GRID_LVERTICAL + } + else if (CompCursorPos - 1 < DisplayAttributes.size()) + { + encodedAttrs[CompCursorPos - 1] |= (BYTE)CONIME_CURSOR_RIGHT; // special handling for ConSrv... 0x10 = COMMON_LVB_GRID_SINGLEFLAG + COMMON_LVB_GRID_RVERTICAL + } + } + + return encodedAttrs; +} diff --git a/src/tsf/TfConvArea.h b/src/tsf/TfConvArea.h new file mode 100644 index 000000000..b883482c6 --- /dev/null +++ b/src/tsf/TfConvArea.h @@ -0,0 +1,49 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfConvArea.h + +Abstract: + + This file defines the CConversionAreaJapanese Interface Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +//+--------------------------------------------------------------------------- +// +// CConversionArea::Pure virtual class +// +//---------------------------------------------------------------------------- + +class CConversionArea +{ +public: + [[nodiscard]] + HRESULT DrawComposition(const CComBSTR& CompStr, + const std::vector& DisplayAttributes, + const DWORD CompCursorPos = -1); + + [[nodiscard]] + HRESULT ClearComposition(); + + [[nodiscard]] + HRESULT DrawResult(const CComBSTR& ResultStr); + +private: + [[nodiscard]] + std::vector _DisplayAttributesToEncodedAttributes(const std::vector& DisplayAttributes, + const DWORD CompCursorPos); + +}; diff --git a/src/tsf/TfCtxtComp.h b/src/tsf/TfCtxtComp.h new file mode 100644 index 000000000..a5537c006 --- /dev/null +++ b/src/tsf/TfCtxtComp.h @@ -0,0 +1,106 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfCtxtComp.h + +Abstract: + + This file defines the Context of Composition Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +#include "StructureArray.h" + +///////////////////////////////////////////////////////////////////////////// +// CCompString + +class CCompString : public CComBSTR +{ +public: + CCompString() : CComBSTR() { }; + CCompString(__in_ecount(dwLen) LPWSTR lpsz, DWORD dwLen) : CComBSTR(dwLen, lpsz) { }; + virtual ~CCompString() { }; +}; + +///////////////////////////////////////////////////////////////////////////// +// CCompAttribute + +class CCompAttribute : public CStructureArray +{ +public: + CCompAttribute(BYTE* lpAttr=NULL, DWORD dwLen=0) : CStructureArray(dwLen) + { + if (! InsertAt(0, dwLen)) { + return; + } + + BYTE* pb = GetAt(0); + if (pb == NULL) { + return; + } + + memcpy(pb, lpAttr, dwLen * sizeof(BYTE)); + } + virtual ~CCompAttribute() { }; +}; + +///////////////////////////////////////////////////////////////////////////// +// CCompCursorPos + +class CCompCursorPos +{ +public: + CCompCursorPos() + { + m_CursorPosition = 0; + } + + void SetCursorPosition(DWORD CursorPosition) + { + m_CursorPosition = CursorPosition; + } + + DWORD GetCursorPosition() { return m_CursorPosition; } + +private: + DWORD m_CursorPosition; +}; + +///////////////////////////////////////////////////////////////////////////// +// CCompTfGuidAtom + +class CCompTfGuidAtom : public CStructureArray +{ +public: + CCompTfGuidAtom() { }; + virtual ~CCompTfGuidAtom() { }; + + operator TfGuidAtom* () { return GetAt(0); } + + DWORD FillData(const TfGuidAtom& data, DWORD dwLen) + { + TfGuidAtom *psTemp = Append(dwLen); + if (psTemp == NULL) { + return 0; + } + + DWORD index = dwLen; + while (index--) { + *psTemp++ = data; + } + + return dwLen; + } +}; diff --git a/src/tsf/TfDispAttr.cpp b/src/tsf/TfDispAttr.cpp new file mode 100644 index 000000000..3fe720648 --- /dev/null +++ b/src/tsf/TfDispAttr.cpp @@ -0,0 +1,200 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfDispAttr.cpp + +Abstract: + + This file implements the CicDisplayAttributeMgr Class. + +Author: + +Revision History: + +Notes: + +--*/ + + +#include "precomp.h" +#include "TfDispAttr.h" + +//+--------------------------------------------------------------------------- +// +// CicDisplayAttributeMgr::ctor +// CicDisplayAttributeMgr::dtor +// +//---------------------------------------------------------------------------- + +CicDisplayAttributeMgr::CicDisplayAttributeMgr() +{ +} + +CicDisplayAttributeMgr::~CicDisplayAttributeMgr() +{ + if (m_pDAM) { + m_pDAM.Release(); + } +} + +//+--------------------------------------------------------------------------- +// +// CicDisplayAttributeMgr::GetDisplayAttributeTrackPropertyRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CicDisplayAttributeMgr::GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, ITfContext *pic, ITfRange *pRange, + ITfReadOnlyProperty **ppProp, IEnumTfRanges **ppEnum, ULONG *pulNumProp) +{ + HRESULT hr = E_FAIL; + + ULONG ulNumProp; + ulNumProp = (ULONG) m_DispAttrProp.Count(); + if (ulNumProp) { + const GUID **ppguidProp; + // + // TrackProperties wants an array of GUID *'s + // + ppguidProp = (const GUID **) new(std::nothrow) GUID* [ulNumProp]; + if (ppguidProp == NULL) { + hr = E_OUTOFMEMORY; + } + else { + for (ULONG i=0; i pProp; + if (SUCCEEDED(hr = pic->TrackProperties(ppguidProp, ulNumProp, 0, NULL, &pProp))) { + hr = pProp->EnumRanges(ec, ppEnum, pRange); + if (SUCCEEDED(hr)) { + *ppProp = pProp; + (*ppProp)->AddRef(); + } + pProp.Release(); + } + + delete [] ppguidProp; + + if (SUCCEEDED(hr)) { + *pulNumProp = ulNumProp; + } + } + } + + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CicDisplayAttributeMgr::GetDisplayAttributeData +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CicDisplayAttributeMgr::GetDisplayAttributeData(ITfCategoryMgr *pcat, TfEditCookie ec, ITfReadOnlyProperty *pProp, ITfRange *pRange, TF_DISPLAYATTRIBUTE *pda, TfGuidAtom *pguid, ULONG /*ulNumProp*/) +{ + VARIANT var; + + HRESULT hr = E_FAIL; + + if (SUCCEEDED(pProp->GetValue(ec, pRange, &var))) { + FAIL_FAST_IF(!(var.vt == VT_UNKNOWN)); + + CComQIPtr pEnumPropertyVal(var.punkVal); + if (pEnumPropertyVal) { + TF_PROPERTYVAL tfPropVal; + while (pEnumPropertyVal->Next(1, &tfPropVal, NULL) == S_OK) { + if (tfPropVal.varValue.vt == VT_EMPTY) { + continue; // prop has no value over this span + } + + FAIL_FAST_IF(!(tfPropVal.varValue.vt == VT_I4)); // expecting GUIDATOMs + + TfGuidAtom gaVal = (TfGuidAtom)tfPropVal.varValue.lVal; + + GUID guid; + pcat->GetGUID(gaVal, &guid); + + CComPtr pDAI; + if (SUCCEEDED(m_pDAM->GetDisplayAttributeInfo(guid, &pDAI, NULL))) { + // + // Issue: for simple apps. + // + // Small apps can not show multi underline. So + // this helper function returns only one + // DISPLAYATTRIBUTE structure. + // + if (pda) { + pDAI->GetAttributeInfo(pda); + } + + if (pguid) { + *pguid = gaVal; + } + + pDAI.Release(); + hr = S_OK; + break; + } + } + } + VariantClear(&var); + } + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CicDisplayAttributeMgr::InitCategoryInstance +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CicDisplayAttributeMgr::InitDisplayAttributeInstance(ITfCategoryMgr* pcat) +{ + HRESULT hr; + + // + // Create ITfDisplayAttributeMgr instance. + // + if (FAILED(hr = m_pDAM.CoCreateInstance(CLSID_TF_DisplayAttributeMgr))) { + return hr; + } + + CComPtr pEnumProp; + pcat->EnumItemsInCategory(GUID_TFCAT_DISPLAYATTRIBUTEPROPERTY, &pEnumProp); + + // + // make a database for Display Attribute Properties. + // + if (pEnumProp) { + GUID guidProp; + + // + // add System Display Attribute first. + // so no other Display Attribute property overwrite it. + // + GUID *pguid; + + pguid = m_DispAttrProp.Append(); + if (pguid != NULL) { + *pguid = GUID_PROP_ATTRIBUTE; + } + + while(pEnumProp->Next(1, &guidProp, NULL) == S_OK) { + if (!IsEqualGUID(guidProp, GUID_PROP_ATTRIBUTE)) { + pguid = m_DispAttrProp.Append(); + if (pguid != NULL) { + *pguid = guidProp; + } + } + } + } + return hr; +} diff --git a/src/tsf/TfDispAttr.h b/src/tsf/TfDispAttr.h new file mode 100644 index 000000000..b2b08d1ba --- /dev/null +++ b/src/tsf/TfDispAttr.h @@ -0,0 +1,56 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfDispAttr.h + +Abstract: + + This file defines the CicDisplayAttributeMgr Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +#include "StructureArray.h" + +class CicDisplayAttributeMgr +{ +public: + CicDisplayAttributeMgr(); + virtual ~CicDisplayAttributeMgr(); + +public: + [[nodiscard]] + HRESULT GetDisplayAttributeTrackPropertyRange(TfEditCookie ec, + ITfContext *pic, + ITfRange *pRange, + ITfReadOnlyProperty **ppProp, + IEnumTfRanges **ppEnum, + ULONG *pulNumProp); + [[nodiscard]] + HRESULT GetDisplayAttributeData(ITfCategoryMgr *pcat, + TfEditCookie ec, + ITfReadOnlyProperty *pProp, + ITfRange *pRange, + TF_DISPLAYATTRIBUTE *pda, + TfGuidAtom *pguid, + ULONG ulNumProp); + [[nodiscard]] + HRESULT InitDisplayAttributeInstance(ITfCategoryMgr* pcat); + + inline ITfDisplayAttributeMgr* GetDisplayAttributeMgr() { return m_pDAM; } + +private: + CComQIPtr m_pDAM; + CStructureArray m_DispAttrProp; +}; diff --git a/src/tsf/TfEditses.cpp b/src/tsf/TfEditses.cpp new file mode 100644 index 000000000..700c270d7 --- /dev/null +++ b/src/tsf/TfEditses.cpp @@ -0,0 +1,1070 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfEditses.cpp + +Abstract: + + This file implements the CEditSessionObject Class. + +Author: + +Revision History: + +Notes: + +--*/ + + +#include "precomp.h" +#include "TfConvArea.h" +#include "TfCatUtil.h" +#include "TfDispAttr.h" +#include "TfEditses.h" + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::IUnknown::QueryInterface +// CEditSessionObject::IUnknown::AddRef +// CEditSessionObject::IUnknown::Release +// +//---------------------------------------------------------------------------- + +STDAPI CEditSessionObject::QueryInterface(REFIID riid, void** ppvObj) +{ + *ppvObj = NULL; + + if (IsEqualIID(riid, IID_ITfEditSession)) { + *ppvObj = static_cast(this); + } + else if (IsEqualIID(riid, IID_IUnknown)) { + *ppvObj = static_cast(this); + } + + if (*ppvObj) { + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +STDAPI_(ULONG) CEditSessionObject::AddRef() +{ + return ++m_cRef; +} + +STDAPI_(ULONG) CEditSessionObject::Release() +{ + long cr; + + cr = --m_cRef; + FAIL_FAST_IF(!(cr >= 0)); + + if (cr == 0) { + delete this; + } + + return cr; +} + + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::GetAllTextRange +// +//---------------------------------------------------------------------------- + +// static +[[nodiscard]] +HRESULT CEditSessionObject::GetAllTextRange(TfEditCookie ec, ITfContext* ic, ITfRange** range, LONG* lpTextLength, TF_HALTCOND* lpHaltCond) +{ + HRESULT hr; + + // + // init lpTextLength first. + // + *lpTextLength = 0; + + // + // Create the range that covers all the text. + // + CComPtr rangeFull; + if (FAILED(hr = ic->GetStart(ec, &rangeFull))) { + return hr; + } + + LONG cch = 0; + if (FAILED(hr = rangeFull->ShiftEnd(ec, LONG_MAX, &cch, lpHaltCond))) { + return hr; + } + + if (FAILED(hr = rangeFull->Clone(range))) { + return hr; + } + + *lpTextLength = cch; + + rangeFull.Release(); + + return S_OK; +} + + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::SetTextInRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::SetTextInRange(TfEditCookie ec, ITfRange* range, __in_ecount_opt(len) LPWSTR psz, DWORD len) +{ + HRESULT hr = E_FAIL; + if (g_pConsoleTSF) + { + g_pConsoleTSF->SetModifyingDocFlag(TRUE); + hr = range->SetText(ec, 0, psz, len); + g_pConsoleTSF->SetModifyingDocFlag(FALSE); + } + return hr; +} + + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::ClearTextInRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::ClearTextInRange(TfEditCookie ec, ITfRange* range) +{ + // + // Clear the text in Cicero TOM + // + return SetTextInRange(ec, range, NULL, 0); +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::_GetCursorPosition +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::_GetCursorPosition(TfEditCookie ec, CCompCursorPos& CompCursorPos) +{ + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + if (pic == NULL) { + return E_FAIL; + } + + HRESULT hr; + ULONG cFetched; + + TF_SELECTION sel; + sel.range = NULL; + + if (SUCCEEDED(hr = pic->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &cFetched))) { + CComPtr start; + LONG ich; + TF_HALTCOND hc; + + hc.pHaltRange = sel.range; + hc.aHaltPos = (sel.style.ase == TF_AE_START) ? TF_ANCHOR_START : TF_ANCHOR_END; + hc.dwFlags = 0; + + if (SUCCEEDED(hr = GetAllTextRange(ec, pic, &start, &ich, &hc))) { + CompCursorPos.SetCursorPosition(ich); + } + + SafeReleaseClear(sel.range); + } + + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::_GetTextAndAttribute +// +//---------------------------------------------------------------------------- + +// +// Get text and attribute in given range +// +// ITfRange::range +// TF_ANCHOR_START +// |======================================================================| +// +--------------------+ #+----------+ +// |ITfRange::pPropRange| #|pPropRange| +// +--------------------+ #+----------+ +// | GUID_ATOM | # +// +--------------------+ # +// ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^# +// ITfRange::gap_range gap_range # +// # +// V +// ITfRange::no_display_attribute_range +// result_comp +// +1 <- 0 -> -1 +// + +[[nodiscard]] +HRESULT CEditSessionObject::_GetTextAndAttribute(TfEditCookie ec, ITfRange* rangeIn, + CCompString& CompStr, CCompTfGuidAtom& CompGuid, CCompString& ResultStr, + BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr) +{ + HRESULT hr; + + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + if (pic == NULL) { + return E_FAIL; + } + + // + // Get no display attribute range if there exist. + // Otherwise, result range is the same to input range. + // + LONG result_comp; + CComPtr no_display_attribute_range; + if (FAILED(hr = rangeIn->Clone(&no_display_attribute_range))) { + return hr; + } + + const GUID* guids[] = { &GUID_PROP_COMPOSING }; + const int guid_size = sizeof(guids) / sizeof(GUID*); + + if (FAILED(hr = _GetNoDisplayAttributeRange(ec, rangeIn, + guids, guid_size, + no_display_attribute_range))) { + return hr; + } + + + + CComPtr propComp; + if (FAILED(hr = pic->TrackProperties(guids, guid_size, // system property + NULL, 0, // application property + &propComp))) { + return hr; + } + + + CComPtr enumComp; + if (FAILED(hr = propComp->EnumRanges(ec, &enumComp, rangeIn))) { + return hr; + } + + CComPtr range; + while (enumComp->Next(1, &range, NULL) == S_OK) { + VARIANT var; + BOOL fCompExist = FALSE; + + hr = propComp->GetValue(ec, range, &var); + if (S_OK == hr) { + + CComQIPtr EnumPropVal(var.punkVal); + if (EnumPropVal) { + TF_PROPERTYVAL tfPropertyVal; + + while (EnumPropVal->Next(1, &tfPropertyVal, NULL) == S_OK) { + for (int i = 0; i < guid_size; i++) { + if (IsEqualGUID(tfPropertyVal.guidId, *guids[i])) { + if ((V_VT(&tfPropertyVal.varValue) == VT_I4 && V_I4(&tfPropertyVal.varValue) != 0)) { + fCompExist = TRUE; + break; + } + } + } + + VariantClear(&tfPropertyVal.varValue); + + if (fCompExist) { + break; + } + } + } + } + + VariantClear(&var); + + ULONG ulNumProp; + + CComPtr enumProp; + CComPtr prop; + if (FAILED(hr = pCicDispAttr->GetDisplayAttributeTrackPropertyRange(ec, pic, range, &prop, &enumProp, &ulNumProp))) { + return hr; + } + + // use text range for get text + CComPtr textRange; + if (FAILED(hr = range->Clone(&textRange))) { + return hr; + } + + // use text range for gap text (no property range). + CComPtr gap_range; + if (FAILED(hr = range->Clone(&gap_range))) { + return hr; + } + + CComPtr pPropRange; + while (enumProp->Next(1, &pPropRange, NULL) == S_OK) { + + // pick up the gap up to the next property + gap_range->ShiftEndToRange(ec, pPropRange, TF_ANCHOR_START); + + // + // GAP range + // + no_display_attribute_range->CompareStart(ec, gap_range, TF_ANCHOR_START, &result_comp); + LOG_IF_FAILED(_GetTextAndAttributeGapRange(ec, + gap_range, + result_comp, + CompStr, CompGuid, + ResultStr)); + + // + // Get display attribute data if some GUID_ATOM exist. + // + TF_DISPLAYATTRIBUTE da; + TfGuidAtom guidatom = TF_INVALID_GUIDATOM; + + LOG_IF_FAILED(pCicDispAttr->GetDisplayAttributeData(pCicCatMgr->GetCategoryMgr(), + ec, + prop, + pPropRange, + &da, + &guidatom, + ulNumProp)); + + // + // Property range + // + no_display_attribute_range->CompareStart(ec, pPropRange, TF_ANCHOR_START, &result_comp); + + // Adjust GAP range's start anchor to the end of proprty range. + gap_range->ShiftStartToRange(ec, pPropRange, TF_ANCHOR_END); + + // + // Get property text + // + LOG_IF_FAILED(_GetTextAndAttributePropertyRange(ec, + pPropRange, + fCompExist, + result_comp, + bInWriteSession, + da, + guidatom, + CompStr, + CompGuid, + ResultStr)); + + pPropRange.Release(); + + } // while + + // the last non-attr + textRange->ShiftStartToRange(ec, gap_range, TF_ANCHOR_START); + textRange->ShiftEndToRange(ec, range, TF_ANCHOR_END); + + BOOL fEmpty; + while (textRange->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) { + WCHAR wstr0[256 + 1]; + ULONG ulcch0 = ARRAYSIZE(wstr0) - 1; + textRange->GetText(ec, TF_TF_MOVESTART, wstr0, ulcch0, &ulcch0); + + TfGuidAtom guidatom; + guidatom = TF_INVALID_GUIDATOM; + + TF_DISPLAYATTRIBUTE da; + da.bAttr = TF_ATTR_INPUT; + + CompGuid.FillData(guidatom, ulcch0); + CompStr.Append(wstr0, ulcch0); + } + + textRange->Collapse(ec, TF_ANCHOR_END); + + range.Release(); + + } // out-most while for GUID_PROP_COMPOSING + + + // + // set GUID_PROP_CONIME_TRACKCOMPOSITION + // + CComPtr PropertyTrackComposition; + if (SUCCEEDED(hr = pic->GetProperty(GUID_PROP_CONIME_TRACKCOMPOSITION, &PropertyTrackComposition))) { + VARIANT var; + var.vt = VT_I4; + var.lVal = 1; + PropertyTrackComposition->SetValue(ec, rangeIn, &var); + } + + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::_GetTextAndAttributeGapRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::_GetTextAndAttributeGapRange(TfEditCookie ec, ITfRange* gap_range, LONG result_comp, + CCompString& CompStr, CCompTfGuidAtom& CompGuid, CCompString& ResultStr) +{ + TfGuidAtom guidatom; + guidatom = TF_INVALID_GUIDATOM; + + TF_DISPLAYATTRIBUTE da; + da.bAttr = TF_ATTR_INPUT; + + BOOL fEmpty; + WCHAR wstr0[256 + 1]; + ULONG ulcch0; + + while (gap_range->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) { + CComPtr backup_range; + if (FAILED(gap_range->Clone(&backup_range))) { + return E_FAIL; + } + + // + // Retrieve gap text if there exist. + // + ulcch0 = ARRAYSIZE(wstr0) - 1; + if (FAILED(gap_range->GetText(ec, + TF_TF_MOVESTART, // Move range to next after get text. + wstr0, + ulcch0, &ulcch0))) { + return E_FAIL; + } + + if (result_comp <= 0) { + CompGuid.FillData(guidatom, ulcch0); + CompStr.Append(wstr0, ulcch0); + } + else { + ResultStr.Append(wstr0, ulcch0); + LOG_IF_FAILED(ClearTextInRange(ec, backup_range)); + } + } + + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::_GetTextAndAttributePropertyRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::_GetTextAndAttributePropertyRange(TfEditCookie ec, + ITfRange* pPropRange, + BOOL fCompExist, + LONG result_comp, + BOOL bInWriteSession, + TF_DISPLAYATTRIBUTE da, + TfGuidAtom guidatom, + CCompString& CompStr, + CCompTfGuidAtom& CompGuid, + CCompString& ResultStr) +{ + BOOL fEmpty; + WCHAR wstr0[256 + 1]; + ULONG ulcch0; + + while (pPropRange->IsEmpty(ec, &fEmpty) == S_OK && !fEmpty) { + CComPtr backup_range; + if (FAILED(pPropRange->Clone(&backup_range))) { + return E_FAIL; + } + + // + // Retrieve property text if there exist. + // + ulcch0 = ARRAYSIZE(wstr0) - 1; + if (FAILED(pPropRange->GetText(ec, + TF_TF_MOVESTART, // Move range to next after get text. + wstr0, + ulcch0, &ulcch0))) { + return E_FAIL; + } + + // see if there is a valid disp attribute + if (fCompExist == TRUE && result_comp <= 0) { + if (guidatom == TF_INVALID_GUIDATOM) { + da.bAttr = TF_ATTR_INPUT; + } + CompGuid.FillData(guidatom, ulcch0); + CompStr.Append(wstr0, ulcch0); + } + else if (bInWriteSession) { + // if there's no disp attribute attached, it probably means + // the part of string is finalized. + // + ResultStr.Append(wstr0, ulcch0); + + // it was a 'determined' string + // so the doc has to shrink + // + LOG_IF_FAILED(ClearTextInRange(ec, backup_range)); + } + else { + // + // Prevent infinite loop + // + break; + } + } + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject::_GetNoDisplayAttributeRange +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionObject::_GetNoDisplayAttributeRange(TfEditCookie ec, ITfRange* rangeIn, const GUID** guids, const int guid_size, ITfRange* no_display_attribute_range) +{ + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + if (pic == NULL) { + return E_FAIL; + } + + CComPtr propComp; + HRESULT hr = pic->TrackProperties(guids, guid_size, // system property + NULL, 0, // application property + &propComp); + if (FAILED(hr)) { + return hr; + } + + CComPtr enumComp; + hr = propComp->EnumRanges(ec, &enumComp, rangeIn); + if (FAILED(hr)) { + return hr; + } + + CComPtr pRange; + + while (enumComp->Next(1, &pRange, NULL) == S_OK) { + VARIANT var; + BOOL fCompExist = FALSE; + + hr = propComp->GetValue(ec, pRange, &var); + if (S_OK == hr) { + + CComQIPtr EnumPropVal(var.punkVal); + if (EnumPropVal) { + TF_PROPERTYVAL tfPropertyVal; + + while (EnumPropVal->Next(1, &tfPropertyVal, NULL) == S_OK) { + for (int i = 0; i < guid_size; i++) { + if (IsEqualGUID(tfPropertyVal.guidId, *guids[i])) { + if ((V_VT(&tfPropertyVal.varValue) == VT_I4 && V_I4(&tfPropertyVal.varValue) != 0)) { + fCompExist = TRUE; + break; + } + } + } + + VariantClear(&tfPropertyVal.varValue); + + if (fCompExist) { + break; + } + } + } + } + + if (!fCompExist) { + + // Adjust GAP range's start anchor to the end of proprty range. + no_display_attribute_range->ShiftStartToRange(ec, pRange, TF_ANCHOR_START); + } + + VariantClear(&var); + + pRange.Release(); + } + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionCompositionComplete::CompComplete +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionCompositionComplete::CompComplete(TfEditCookie ec) +{ + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + RETURN_HR_IF_NULL(E_FAIL, pic); + + // Get the whole text, finalize it, and set empty string in TOM + CComPtr spRange; + LONG cch; + + RETURN_IF_FAILED(GetAllTextRange(ec, pic, &spRange, &cch)); + + // Check if a part of the range has already been finalized but not removed yet. + // Adjust the range appropriately to avoid inserting the same text twice. + long cchCompleted = g_pConsoleTSF->GetCompletedRangeLength(); + if ((cchCompleted > 0) && + (cchCompleted < cch) && + SUCCEEDED(spRange->ShiftStart(ec, cchCompleted, &cchCompleted, NULL))) + { + FAIL_FAST_IF(!((cchCompleted > 0) && (cchCompleted < cch))); + cch -= cchCompleted; + } + else + { + cchCompleted = 0; + } + + // Get conversion area service. + CConversionArea* conv_area = g_pConsoleTSF->GetConversionArea(); + RETURN_HR_IF_NULL(E_FAIL, conv_area); + + // If there is no string in TextStore we don't have to do anything. + if (!cch) { + // Clear composition + LOG_IF_FAILED(conv_area->ClearComposition()); + return S_OK; + } + + HRESULT hr = S_OK; + try + { + auto wstr = std::make_unique(cch + 1); + + // Get the whole text, finalize it, and erase the whole text. + if (SUCCEEDED(spRange->GetText(ec, TF_TF_IGNOREEND, wstr.get(), (ULONG)cch, (ULONG*)&cch))) + { + // Make Result String. + CCompString ResultStr(wstr.get(), cch); + hr = conv_area->DrawResult(ResultStr); + } + } + CATCH_RETURN(); + + // Update the stored length of the completed fragment. + g_pConsoleTSF->SetCompletedRangeLength(cchCompleted + cch); + + return hr; +} + + +//+--------------------------------------------------------------------------- +// +// CEditSessionCompositionCleanup::EmptyCompositionRange() +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionCompositionCleanup::EmptyCompositionRange(TfEditCookie ec) +{ + if (!g_pConsoleTSF) + { + return E_FAIL; + } + if (!g_pConsoleTSF->IsPendingCompositionCleanup()) + { + return S_OK; + } + + HRESULT hr = E_FAIL; + ITfContext* pic = g_pConsoleTSF->GetInputContext(); + if (pic != NULL) + { + // Cleanup (empty the context range) after the last composition. + + hr = S_OK; + long cchCompleted = g_pConsoleTSF->GetCompletedRangeLength(); + if (cchCompleted != 0) + { + CComPtr spRange; + LONG cch; + hr = GetAllTextRange(ec, pic, &spRange, &cch); + if (SUCCEEDED(hr)) + { + // Clean up only the completed part (which start is expected to coincide with the start of the full range). + if (cchCompleted < cch) + { + spRange->ShiftEnd(ec, (cchCompleted - cch), &cch, NULL); + } + hr = ClearTextInRange(ec, spRange); + g_pConsoleTSF->SetCompletedRangeLength(0); // cleaned up all completed text + } + } + } + g_pConsoleTSF->OnCompositionCleanup(SUCCEEDED(hr)); + return hr; +} + + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString::UpdateCompositionString +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionUpdateCompositionString::UpdateCompositionString(TfEditCookie ec) +{ + HRESULT hr; + + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + if (pic == NULL) { + return E_FAIL; + } + + // Reset the 'edit session requested' flag. + g_pConsoleTSF->OnEditSession(); + + // If the composition has been cancelled\finalized, no update necessary. + if (!g_pConsoleTSF->IsInComposition()) + { + return S_OK; + } + + BOOL bInWriteSession; + if (FAILED(hr = pic->InWriteSession(g_pConsoleTSF->GetTfClientId(), &bInWriteSession))) { + return hr; + } + + CComPtr FullTextRange; + LONG lTextLength; + if (FAILED(hr = GetAllTextRange(ec, pic, &FullTextRange, &lTextLength))) { + return hr; + } + + CComPtr InterimRange; + BOOL fInterim = FALSE; + if (FAILED(hr = _IsInterimSelection(ec, &InterimRange, &fInterim))) { + return hr; + } + + CicCategoryMgr* pCicCat = NULL; + CicDisplayAttributeMgr* pDispAttr = NULL; + + // + // Create Cicero Category Manager and Display Attribute Manager + // + hr = _CreateCategoryAndDisplayAttributeManager(&pCicCat, &pDispAttr); + if (SUCCEEDED(hr)) { + if (fInterim) { + hr = _MakeInterimString(ec, FullTextRange, InterimRange, lTextLength, bInWriteSession, pCicCat, pDispAttr); + } + else { + hr = _MakeCompositionString(ec, FullTextRange, bInWriteSession, pCicCat, pDispAttr); + } + } + + if (pCicCat) { + delete pCicCat; + } + if (pDispAttr) { + delete pDispAttr; + } + + return hr; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString::_IsInterimSelection +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionUpdateCompositionString::_IsInterimSelection(TfEditCookie ec, ITfRange** pInterimRange, BOOL *pfInterim) +{ + ITfContext* pic = g_pConsoleTSF ? g_pConsoleTSF->GetInputContext() : NULL; + if (pic == NULL) { + return E_FAIL; + } + + ULONG cFetched; + + TF_SELECTION sel; + sel.range = NULL; + + *pfInterim = FALSE; + if (pic->GetSelection(ec, TF_DEFAULT_SELECTION, 1, &sel, &cFetched) != S_OK) { + // no selection. we can return S_OK. + return S_OK; + } + + if (sel.style.fInterimChar && sel.range) { + HRESULT hr; + if (FAILED(hr = sel.range->Clone(pInterimRange))) { + SafeReleaseClear(sel.range); + return hr; + } + + *pfInterim = TRUE; + } + + SafeReleaseClear(sel.range); + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString::_MakeCompositionString +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionUpdateCompositionString::_MakeCompositionString(TfEditCookie ec, ITfRange* FullTextRange, BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr) +{ + CCompString CompStr; + CCompTfGuidAtom CompGuid; + CCompCursorPos CompCursorPos; + CCompString ResultStr; + BOOL fIgnorePreviousCompositionResult = FALSE; + + RETURN_IF_FAILED(_GetTextAndAttribute(ec, FullTextRange, + CompStr, CompGuid, ResultStr, + bInWriteSession, + pCicCatMgr, pCicDispAttr)); + + if (g_pConsoleTSF && g_pConsoleTSF->IsPendingCompositionCleanup()) + { + // Don't draw the previous composition result if there was a cleanup session requested for it. + fIgnorePreviousCompositionResult = TRUE; + // Cancel pending cleanup, since the ResultStr was cleared from the composition in _GetTextAndAttribute. + g_pConsoleTSF->OnCompositionCleanup(TRUE); + } + + RETURN_IF_FAILED(_GetCursorPosition(ec, CompCursorPos)); + + // Get display attribute manager + ITfDisplayAttributeMgr* dam = pCicDispAttr->GetDisplayAttributeMgr(); + RETURN_HR_IF_NULL(E_FAIL, dam); + + // Get category manager + ITfCategoryMgr* cat = pCicCatMgr->GetCategoryMgr(); + RETURN_HR_IF_NULL(E_FAIL, cat); + + // Allocate and fill TF_DISPLAYATTRIBUTE + try + { + // Get conversion area service. + CConversionArea* conv_area = g_pConsoleTSF ? g_pConsoleTSF->GetConversionArea() : NULL; + RETURN_HR_IF_NULL(E_FAIL, conv_area); + + if (ResultStr && !fIgnorePreviousCompositionResult) { + return conv_area->DrawResult(ResultStr); + } + if (CompStr) { + ULONG cchDisplayAttribute = (ULONG)CompGuid.Count(); + std::vector DisplayAttributes; + DisplayAttributes.reserve(cchDisplayAttribute); + + for (DWORD i = 0; i < cchDisplayAttribute; i++) { + TF_DISPLAYATTRIBUTE da; + ZeroMemory(&da, sizeof(da)); + da.bAttr = TF_ATTR_OTHER; + + GUID guid; + if (SUCCEEDED(cat->GetGUID(*CompGuid.GetAt(i), &guid))) { + CLSID clsid; + CComPtr dai; + if (SUCCEEDED(dam->GetDisplayAttributeInfo(guid, &dai, &clsid))) { + dai->GetAttributeInfo(&da); + } + } + + DisplayAttributes.emplace_back(da); + } + + return conv_area->DrawComposition(CompStr, // composition string + DisplayAttributes, // display attributes + CompCursorPos.GetCursorPosition()); // cursor position + } + } + CATCH_RETURN(); + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString::_MakeInterimString +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionUpdateCompositionString::_MakeInterimString(TfEditCookie ec, + ITfRange* FullTextRange, + ITfRange* InterimRange, + LONG lTextLength, + BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, + CicDisplayAttributeMgr* pCicDispAttr) +{ + LONG lStartResult; + LONG lEndResult; + + FullTextRange->CompareStart(ec, InterimRange, TF_ANCHOR_START, &lStartResult); + RETURN_HR_IF(E_FAIL, lStartResult > 0); + + FullTextRange->CompareEnd(ec, InterimRange, TF_ANCHOR_END, &lEndResult); + RETURN_HR_IF(E_FAIL, lEndResult != 1); + + CCompString ResultStr; + + if (lStartResult < 0) { + // Make result string. + RETURN_IF_FAILED(FullTextRange->ShiftEndToRange(ec, InterimRange, TF_ANCHOR_START)); + + // Interim char assume 1 char length. + // Full text length - 1 means result string length. + lTextLength--; + + FAIL_FAST_IF(!(lTextLength > 0)); + + if (lTextLength > 0) { + try + { + auto wstr = std::make_unique(lTextLength + 1); + + // Get the result text, finalize it, and erase the result text. + if (SUCCEEDED(FullTextRange->GetText(ec, TF_TF_IGNOREEND, wstr.get(), (ULONG)lTextLength, (ULONG*)&lTextLength))) { + // Clear the TOM + LOG_IF_FAILED(ClearTextInRange(ec, FullTextRange)); + } + } + CATCH_RETURN(); + } + } + + // Make interim character + CCompString CompStr; + CCompTfGuidAtom CompGuid; + CCompString _tempResultStr; + + RETURN_IF_FAILED(_GetTextAndAttribute(ec, InterimRange, + CompStr, CompGuid, _tempResultStr, + bInWriteSession, + pCicCatMgr, pCicDispAttr)); + + + // Get display attribute manager + ITfDisplayAttributeMgr* dam = pCicDispAttr->GetDisplayAttributeMgr(); + RETURN_HR_IF_NULL(E_FAIL, dam); + + // Get category manager + ITfCategoryMgr* cat = pCicCatMgr->GetCategoryMgr(); + RETURN_HR_IF_NULL(E_FAIL, cat); + + // Allocate and fill TF_DISPLAYATTRIBUTE + try + { + // Get conversion area service. + CConversionArea* conv_area = g_pConsoleTSF ? g_pConsoleTSF->GetConversionArea() : NULL; + RETURN_HR_IF_NULL(E_FAIL, conv_area); + + if (ResultStr) { + return conv_area->DrawResult(ResultStr); + } + if (CompStr) { + ULONG cchDisplayAttribute = (ULONG)CompGuid.Count(); + std::vector DisplayAttributes; + DisplayAttributes.reserve(cchDisplayAttribute); + + for (DWORD i = 0; i < cchDisplayAttribute; i++) { + TF_DISPLAYATTRIBUTE da; + ZeroMemory(&da, sizeof(da)); + da.bAttr = TF_ATTR_OTHER; + GUID guid; + if (SUCCEEDED(cat->GetGUID(*CompGuid.GetAt(i), &guid))) { + CLSID clsid; + CComPtr dai; + if (SUCCEEDED(dam->GetDisplayAttributeInfo(guid, &dai, &clsid))) { + dai->GetAttributeInfo(&da); + } + } + + DisplayAttributes.emplace_back(da); + } + + return conv_area->DrawComposition(CompStr, // composition string (Interim string) + DisplayAttributes); // display attributes + } + } + CATCH_RETURN(); + + + return S_OK; +} + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString::_CreateCategoryAndDisplayAttributeManager +// +//---------------------------------------------------------------------------- + +[[nodiscard]] +HRESULT CEditSessionUpdateCompositionString::_CreateCategoryAndDisplayAttributeManager(CicCategoryMgr** pCicCatMgr, CicDisplayAttributeMgr** pCicDispAttr) +{ + HRESULT hr = E_OUTOFMEMORY; + + CicCategoryMgr* pTmpCat = NULL; + CicDisplayAttributeMgr* pTmpDispAttr = NULL; + + // + // Create Cicero Category Manager + // + pTmpCat = new(std::nothrow) CicCategoryMgr; + if (pTmpCat) { + if (SUCCEEDED(hr = pTmpCat->InitCategoryInstance())) { + + ITfCategoryMgr* pcat = pTmpCat->GetCategoryMgr(); + if (pcat) { + // + // Create Cicero Display Attribute Manager + // + pTmpDispAttr = new(std::nothrow) CicDisplayAttributeMgr; + if (pTmpDispAttr) { + if (SUCCEEDED(hr = pTmpDispAttr->InitDisplayAttributeInstance(pcat))) { + *pCicCatMgr = pTmpCat; + *pCicDispAttr = pTmpDispAttr; + } + } + } + } + } + + if (FAILED(hr)) { + if (pTmpCat) { + delete pTmpCat; + } + if (pTmpDispAttr) { + delete pTmpDispAttr; + } + } + + return hr; +} diff --git a/src/tsf/TfEditses.h b/src/tsf/TfEditses.h new file mode 100644 index 000000000..7bab271ba --- /dev/null +++ b/src/tsf/TfEditses.h @@ -0,0 +1,210 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfEditses.h + +Abstract: + + This file defines the CEditSessionObject Class. + +Author: + +Revision History: + +Notes: + +--*/ + +#pragma once + +class CicCategoryMgr; +class CicDisplayAttributeMgr; + +/* 183C627A-B46C-44ad-B797-82F6BEC82131 */ +const GUID GUID_PROP_CONIME_TRACKCOMPOSITION = { + 0x183c627a, + 0xb46c, + 0x44ad, + {0xb7, 0x97, 0x82, 0xf6, 0xbe, 0xc8, 0x21, 0x31} +}; + +//+--------------------------------------------------------------------------- +// +// CEditSessionObject +// +//---------------------------------------------------------------------------- + +class CEditSessionObject : public ITfEditSession +{ +public: + CEditSessionObject() : m_cRef(1) {} + virtual ~CEditSessionObject() { }; + +public: + // + // IUnknown methods + // + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); + STDMETHODIMP_(ULONG) AddRef(void); + STDMETHODIMP_(ULONG) Release(void); + + // + // ITfEditSession method + // + STDMETHODIMP DoEditSession(TfEditCookie ec) + { + HRESULT hr = _DoEditSession(ec); + Release(); // Release reference count for asynchronous edit session. + return hr; + } + + // + // ImmIfSessionObject methods + // +protected: + [[nodiscard]] + virtual HRESULT _DoEditSession(TfEditCookie ec) = 0; + + // + // EditSession methods. + // +public: + [[nodiscard]] + static HRESULT GetAllTextRange(TfEditCookie ec, ITfContext* ic, ITfRange** range, LONG* lpTextLength, TF_HALTCOND* lpHaltCond=NULL); + +protected: + [[nodiscard]] + HRESULT SetTextInRange(TfEditCookie ec, ITfRange* range, __in_ecount_opt(len) LPWSTR psz, DWORD len); + [[nodiscard]] + HRESULT ClearTextInRange(TfEditCookie ec, ITfRange* range); + + [[nodiscard]] + HRESULT _GetTextAndAttribute(TfEditCookie ec, ITfRange* range, + CCompString& CompStr, CCompTfGuidAtom CompGuid, + BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr) + { + CCompString ResultStr; + return _GetTextAndAttribute(ec, range, + CompStr, CompGuid, ResultStr, + bInWriteSession, + pCicCatMgr, pCicDispAttr); + } + + [[nodiscard]] + HRESULT _GetTextAndAttribute(TfEditCookie ec, ITfRange* range, + CCompString& CompStr, CCompTfGuidAtom& CompGuid, CCompString& ResultStr, + BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr); + + + [[nodiscard]] + HRESULT _GetTextAndAttributeGapRange(TfEditCookie ec, ITfRange* gap_range, LONG result_comp, + CCompString& CompStr, CCompTfGuidAtom& CompGuid, CCompString& ResultStr); + + [[nodiscard]] + HRESULT _GetTextAndAttributePropertyRange(TfEditCookie ec, ITfRange* pPropRange, + BOOL fDispAttribute, + LONG result_comp, + BOOL bInWriteSession, + TF_DISPLAYATTRIBUTE da, + TfGuidAtom guidatom, + CCompString& CompStr, CCompTfGuidAtom& CompGuid, CCompString& ResultStr); + + [[nodiscard]] + HRESULT _GetNoDisplayAttributeRange(TfEditCookie ec, ITfRange* range, + const GUID** guids, + const int guid_size, + ITfRange* no_display_attribute_range); + + [[nodiscard]] + HRESULT _GetCursorPosition(TfEditCookie ec, + CCompCursorPos& CompCursorPos); + +private: + int m_cRef; +}; + +//+--------------------------------------------------------------------------- +// +// CEditSessionCompositionComplete +// +//---------------------------------------------------------------------------- + +class CEditSessionCompositionComplete : public CEditSessionObject +{ +public: + CEditSessionCompositionComplete() { } + + [[nodiscard]] + HRESULT _DoEditSession(TfEditCookie ec) + { + return CompComplete(ec); + } + + [[nodiscard]] + HRESULT CompComplete(TfEditCookie ec); +}; + + +//+--------------------------------------------------------------------------- +// +// CEditSessionCompositionCleanup +// +//---------------------------------------------------------------------------- + +class CEditSessionCompositionCleanup : public CEditSessionObject +{ +public: + CEditSessionCompositionCleanup() { } + + [[nodiscard]] + HRESULT _DoEditSession(TfEditCookie ec) + { + return EmptyCompositionRange(ec); + } + + [[nodiscard]] + HRESULT EmptyCompositionRange(TfEditCookie ec); +}; + + +//+--------------------------------------------------------------------------- +// +// CEditSessionUpdateCompositionString +// +//---------------------------------------------------------------------------- + +class CEditSessionUpdateCompositionString : public CEditSessionObject +{ +public: + CEditSessionUpdateCompositionString() {} + + [[nodiscard]] + HRESULT _DoEditSession(TfEditCookie ec) + { + return UpdateCompositionString(ec); + } + + [[nodiscard]] + HRESULT UpdateCompositionString(TfEditCookie ec); + +private: + [[nodiscard]] + HRESULT _IsInterimSelection(TfEditCookie ec, ITfRange** pInterimRange, BOOL *pfInterim); + + [[nodiscard]] + HRESULT _MakeCompositionString(TfEditCookie ec, ITfRange* FullTextRange, BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr); + + [[nodiscard]] + HRESULT _MakeInterimString(TfEditCookie ec, ITfRange* FullTextRange, ITfRange* InterimRange, LONG lTextLength, BOOL bInWriteSession, + CicCategoryMgr* pCicCatMgr, CicDisplayAttributeMgr* pCicDispAttr); + + [[nodiscard]] + HRESULT _CreateCategoryAndDisplayAttributeManager(CicCategoryMgr** pCicCatMgr, CicDisplayAttributeMgr** pCicDispAttr); +}; diff --git a/src/tsf/TfTxtevCb.cpp b/src/tsf/TfTxtevCb.cpp new file mode 100644 index 000000000..6e56bdb76 --- /dev/null +++ b/src/tsf/TfTxtevCb.cpp @@ -0,0 +1,166 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + TfTxtevCb.cpp + +Abstract: + + This file implements the CTextEventSinkCallBack Class. + +Author: + +Revision History: + +Notes: + +--*/ + + +#include "precomp.h" +#include "ConsoleTSF.h" +#include "TfEditses.h" + +//+--------------------------------------------------------------------------- +// +// CConsoleTSF::HasCompositionChanged +// +//---------------------------------------------------------------------------- + +BOOL CConsoleTSF::_HasCompositionChanged(ITfContext *pInputContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord) +{ + BOOL fChanged; + if (SUCCEEDED(pEditRecord->GetSelectionStatus(&fChanged))) + { + if (fChanged) { + return TRUE; + } + } + + // + // Find GUID_PROP_CONIME_TRACKCOMPOSITION property. + // + + CComPtr Property; + CComPtr FoundRange; + CComPtr PropertyTrackComposition; + + BOOL bFound = FALSE; + + if (SUCCEEDED(pInputContext->GetProperty(GUID_PROP_CONIME_TRACKCOMPOSITION, &Property))) { + + CComPtr EnumFindFirstTrackCompRange; + + if (SUCCEEDED(Property->EnumRanges(ecReadOnly, &EnumFindFirstTrackCompRange, NULL))) { + + HRESULT hr; + CComPtr range; + + while ((hr = EnumFindFirstTrackCompRange->Next(1, &range, NULL)) == S_OK) { + + VARIANT var; + VariantInit(&var); + + hr = Property->GetValue(ecReadOnly, range, &var); + if (SUCCEEDED(hr)) { + if ((V_VT(&var) == VT_I4 && V_I4(&var) != 0)) { + range->Clone(&FoundRange); + bFound = TRUE; // FOUND!! + break; + } + } + + VariantClear(&var); + + range.Release(); + + if (bFound) { + break; // FOUND!! + } + } + } + } + + // + // if there is no track composition property, + // the composition has been changed since we put it. + // + if (! bFound) { + return TRUE; + } + + if (FoundRange == NULL) { + return FALSE; + } + + + bFound = FALSE; // RESET bFound flag... + + CComPtr rangeTrackComposition; + if (SUCCEEDED(FoundRange->Clone(&rangeTrackComposition))) { + + // + // get the text range that does not include read only area for + // reconversion. + // + CComPtr rangeAllText; + LONG cch; + if (SUCCEEDED(CEditSessionObject::GetAllTextRange(ecReadOnly, pInputContext, &rangeAllText, &cch))) { + + LONG lResult; + if (SUCCEEDED(rangeTrackComposition->CompareStart(ecReadOnly, rangeAllText, TF_ANCHOR_START, &lResult))) { + + // + // if the start position of the track composition range is not + // the beggining of IC, + // the composition has been changed since we put it. + // + if (lResult != 0) { + bFound = TRUE; // FOUND!! + } + else if (SUCCEEDED(rangeTrackComposition->CompareEnd(ecReadOnly, rangeAllText, TF_ANCHOR_END, &lResult))) { + + // + // if the start position of the track composition range is not + // the beggining of IC, + // the composition has been changed since we put it. + // + // + // If we find the changes in these property, we need to update hIMC. + // + const GUID *guids[] = {&GUID_PROP_COMPOSING, + &GUID_PROP_ATTRIBUTE}; + const int guid_size = sizeof(guids) / sizeof(GUID*); + + CComPtr EnumPropertyChanged; + + if (lResult != 0) { + bFound = TRUE; // FOUND!! + } + else if (SUCCEEDED(pEditRecord->GetTextAndPropertyUpdates(TF_GTP_INCL_TEXT, guids, guid_size, &EnumPropertyChanged))) { + + HRESULT hr; + CComPtr range; + + while ((hr = EnumPropertyChanged->Next(1, &range, NULL)) == S_OK) { + BOOL empty; + if (range->IsEmpty(ecReadOnly, &empty) == S_OK && empty) { + range.Release(); + continue; + } + + range.Release(); + + bFound = TRUE; // FOUND!! + break; + } + } + } + } + } + } + return bFound; +} diff --git a/src/tsf/contsf.cpp b/src/tsf/contsf.cpp new file mode 100644 index 000000000..b738e0b24 --- /dev/null +++ b/src/tsf/contsf.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +CConsoleTSF* g_pConsoleTSF = NULL; + +extern "C" BOOL ActivateTextServices(HWND hwndConsole, GetSuggestionWindowPos pfnPosition) +{ + if (!g_pConsoleTSF && hwndConsole) + { + g_pConsoleTSF = new(std::nothrow) CConsoleTSF(hwndConsole, pfnPosition); + if (g_pConsoleTSF && SUCCEEDED(g_pConsoleTSF->Initialize())) + { + // Conhost calls this function only when the console window has focus. + g_pConsoleTSF->SetFocus(TRUE); + } + else + { + SafeReleaseClear(g_pConsoleTSF); + } + } + return g_pConsoleTSF ? TRUE : FALSE; +} + +extern "C" void DeactivateTextServices() +{ + if (g_pConsoleTSF) + { + g_pConsoleTSF->Uninitialize(); + SafeReleaseClear(g_pConsoleTSF); + } +} diff --git a/src/tsf/globals.h b/src/tsf/globals.h new file mode 100644 index 000000000..55494e4b9 --- /dev/null +++ b/src/tsf/globals.h @@ -0,0 +1,58 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + globals.h + +Abstract: + + Contains declarations for all globally scoped names in the program. + This file defines the CBoolean Interface Class. + +Author: + +Revision History: + +Notes: + +--*/ + + +#pragma once + +// +// SAFECAST(obj, type) +// +// This macro is extremely useful for enforcing strong typechecking on other +// macros. It generates no code. +// +// Simply insert this macro at the beginning of an expression list for +// each parameter that must be typechecked. For example, for the +// definition of MYMAX(x, y), where x and y absolutely must be integers, +// use: +// +// #define MYMAX(x, y) (SAFECAST(x, int), SAFECAST(y, int), ((x) > (y) ? (x) : (y))) +// +// +#define SAFECAST(_obj, _type) (((_type)(_obj)==(_obj)?0:0), (_type)(_obj)) + +// +// Bitfields don't get along too well with bools, +// so here's an easy way to convert them: +// +#define BOOLIFY(expr) (!!(expr)) + +// +// generic COM stuff +// +#define SafeReleaseClear(punk) \ +{ \ + if ((punk) != NULL) \ + { \ + (punk)->Release(); \ + (punk) = NULL; \ + } \ +} diff --git a/src/tsf/precomp.cpp b/src/tsf/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/tsf/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/tsf/precomp.h b/src/tsf/precomp.h new file mode 100644 index 000000000..bba98b768 --- /dev/null +++ b/src/tsf/precomp.h @@ -0,0 +1,55 @@ +/*++ + +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. + +Module Name: + + precomp.h + +Abstract: + + This file precompiled header file. + +Author: + +Revision History: + +Notes: + +--*/ + +#define _OLEAUT32_ +#include +#include + +#include // ATL base + +extern "C" +{ + #include + + #include + #include + #include + #include + + #include + #include + #include +} + +#include // Cicero header +#include // ITextStore standard attributes + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#include "..\inc\contsf.h" + +#include "globals.h" + +#include "TfCtxtComp.h" +#include "ConsoleTSF.h" + + diff --git a/src/tsf/sources b/src/tsf/sources new file mode 100644 index 000000000..98df12138 --- /dev/null +++ b/src/tsf/sources @@ -0,0 +1,58 @@ +!include ..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Text Services Framework +# ------------------------------------- + +# This module allows the Windows Console to interact with +# the "Text Services Framework" which provides Input Method Editors (IMEs). +# This is leveraged to allow the console to interact appropriately with +# Chinese, Japanese, and Korean languages which pop-up overlays to help +# insert the appropriate characters based on a series of keystrokes. + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTSF +TARGETTYPE = LIBRARY + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DWIN32 -DNT + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# Defines IME and Codepage support +W32_SB = 1 + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = precomp.h +PRECOMPILED_PCH = precomp.pch +PRECOMPILED_OBJ = precomp.obj + +SOURCES = \ + contsf.cpp \ + ConsoleTSF.cpp \ + TfConvArea.cpp \ + TfCatUtil.cpp \ + TfDispAttr.cpp \ + TfEditses.cpp \ + TfTxtevCb.cpp \ + +INCLUDES = \ + ..\inc; \ + $(ONECORE_PRIV_SDK_INC_PATH); \ + $(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \ + $(SDK_INC_PATH)\atl30; \ diff --git a/src/tsf/tsf.vcxproj b/src/tsf/tsf.vcxproj new file mode 100644 index 000000000..f28814458 --- /dev/null +++ b/src/tsf/tsf.vcxproj @@ -0,0 +1,38 @@ + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + {2FD12FBB-1DDB-46D8-B818-1023C624CACA} + Win32Proj + tsf + TextServicesFramework + ConTSF + + + + + \ No newline at end of file diff --git a/src/tsf/tsf.vcxproj.filters b/src/tsf/tsf.vcxproj.filters new file mode 100644 index 000000000..897f34ae2 --- /dev/null +++ b/src/tsf/tsf.vcxproj.filters @@ -0,0 +1,75 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + diff --git a/src/types/CodepointWidthDetector.cpp b/src/types/CodepointWidthDetector.cpp new file mode 100644 index 000000000..61cb9a6e0 --- /dev/null +++ b/src/types/CodepointWidthDetector.cpp @@ -0,0 +1,1240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/CodepointWidthDetector.hpp" + +// Routine Description: +// - returns the width type of codepoint by searching the map generated from the unicode spec +// Arguments: +// - glyph - the utf16 encoded codepoint to search for +// Return Value: +// - the width type of the codepoint +CodepointWidth CodepointWidthDetector::GetWidth(const std::wstring_view glyph) const noexcept +{ + if (glyph.empty()) + { + return CodepointWidth::Invalid; + } + + if (_map.empty()) + { + const_cast(this)->_populateUnicodeSearchMap(); + } + + const auto codepoint = _extractCodepoint(glyph); + UnicodeRange search{ codepoint }; + auto it = _map.find(search); + if (it == _map.end()) + { + return CodepointWidth::Invalid; + } + else + { + return it->second; + } +} + +// Routine Description: +// - checks if wch is wide. will attempt to fallback as much possible until an answer is determined +// Arguments: +// - wch - the wchar to check width of +// Return Value: +// - true if wch is wide +bool CodepointWidthDetector::IsWide(const wchar_t wch) const noexcept +{ + const auto width = GetCharWidth(wch); + if (width == CodepointWidth::Invalid) + { + return _lookupIsWide({ &wch, 1 }); + } + else + { + return width == CodepointWidth::Wide; + } +} + +// Routine Description: +// - checks if codepoint is wide. will attempt to fallback as much possible until an answer is determined +// Arguments: +// - glyph - the utf16 encoded codepoint to check width of +// Return Value: +// - true if codepoint is wide +bool CodepointWidthDetector::IsWide(const std::wstring_view glyph) const +{ + THROW_HR_IF(E_INVALIDARG, glyph.empty()); + if (glyph.size() == 1) + { + const auto width = GetCharWidth(glyph.front()); + if (width == CodepointWidth::Invalid) + { + return _lookupIsWide(glyph); + } + else + { + return width == CodepointWidth::Wide; + } + } + else + { + return _lookupIsWide(glyph); + } +} + +// Routine Description: +// - checks if codepoint is wide using fallback methods. +// Arguments: +// - glyph - the utf16 encoded codepoint to check width of +// Return Value: +// - true if codepoint is wide or if it can't be confirmed to be narrow +bool CodepointWidthDetector::_lookupIsWide(const std::wstring_view glyph) const noexcept +{ + const CodepointWidth width = GetWidth(glyph); + if (width == CodepointWidth::Ambiguous) + { + if (_hasFallback) + { + return _pfnFallbackMethod(glyph); + } + } + else + { + return width == CodepointWidth::Wide; + } + // better to be too wide than too narrow + return true; +} + +// Routine Description: +// - extract unicode codepoint from utf16 encoding +// Arguments: +// - glyph - the utf16 encoded codepoint convert +// Return Value: +// - the codepoint being stored +unsigned int CodepointWidthDetector::_extractCodepoint(const std::wstring_view glyph) const noexcept +{ + if (glyph.size() == 1) + { + return static_cast(glyph.front()); + } + else + { + const unsigned int mask = 0x3FF; + // leading bits, shifted over to make space for trailing bits + unsigned int codepoint = (glyph.at(0) & mask) << 10; + // trailing bits + codepoint |= (glyph.at(1) & mask); + // 0x10000 is subtracted from the codepoint to encode a surrogate pair, add it back + codepoint += 0x10000; + return codepoint; + } +} + +// Method Description: +// - Sets a function that should be used as the fallback mechanism for +// determining a particular glyph's width, should the glyph be an ambiguous +// width. +// A Terminal could hook in a Renderer's IsGlyphWideByFont method as the +// fallback to ask the renderer for the glyph's width (for example). +// Arguments: +// - pfnFallback - the function to use as the fallback method. +// Return Value: +// - +void CodepointWidthDetector::SetFallbackMethod(std::function pfnFallback) +{ + _pfnFallbackMethod = pfnFallback; + _hasFallback = true; +} + +void CodepointWidthDetector::_populateUnicodeSearchMap() +{ + // generated from http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt + _map[UnicodeRange(0, 160)] = CodepointWidth::Narrow; + _map[UnicodeRange(161, 161)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(162, 163)] = CodepointWidth::Narrow; + _map[UnicodeRange(164, 164)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(165, 166)] = CodepointWidth::Narrow; + _map[UnicodeRange(167, 168)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(169, 169)] = CodepointWidth::Narrow; + _map[UnicodeRange(170, 170)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(171, 172)] = CodepointWidth::Narrow; + _map[UnicodeRange(173, 174)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(175, 175)] = CodepointWidth::Narrow; + _map[UnicodeRange(176, 180)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(181, 181)] = CodepointWidth::Narrow; + _map[UnicodeRange(182, 186)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(187, 187)] = CodepointWidth::Narrow; + _map[UnicodeRange(188, 191)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(192, 197)] = CodepointWidth::Narrow; + _map[UnicodeRange(198, 198)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(199, 207)] = CodepointWidth::Narrow; + _map[UnicodeRange(208, 208)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(209, 214)] = CodepointWidth::Narrow; + _map[UnicodeRange(215, 216)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(217, 221)] = CodepointWidth::Narrow; + _map[UnicodeRange(222, 225)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(226, 229)] = CodepointWidth::Narrow; + _map[UnicodeRange(230, 230)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(231, 231)] = CodepointWidth::Narrow; + _map[UnicodeRange(232, 234)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(235, 235)] = CodepointWidth::Narrow; + _map[UnicodeRange(236, 237)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(238, 239)] = CodepointWidth::Narrow; + _map[UnicodeRange(240, 240)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(241, 241)] = CodepointWidth::Narrow; + _map[UnicodeRange(242, 243)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(244, 246)] = CodepointWidth::Narrow; + _map[UnicodeRange(247, 250)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(251, 251)] = CodepointWidth::Narrow; + _map[UnicodeRange(252, 252)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(253, 253)] = CodepointWidth::Narrow; + _map[UnicodeRange(254, 254)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(255, 256)] = CodepointWidth::Narrow; + _map[UnicodeRange(257, 257)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(258, 272)] = CodepointWidth::Narrow; + _map[UnicodeRange(273, 273)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(274, 274)] = CodepointWidth::Narrow; + _map[UnicodeRange(275, 275)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(276, 282)] = CodepointWidth::Narrow; + _map[UnicodeRange(283, 283)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(284, 293)] = CodepointWidth::Narrow; + _map[UnicodeRange(294, 295)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(296, 298)] = CodepointWidth::Narrow; + _map[UnicodeRange(299, 299)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(300, 304)] = CodepointWidth::Narrow; + _map[UnicodeRange(305, 307)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(308, 311)] = CodepointWidth::Narrow; + _map[UnicodeRange(312, 312)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(313, 318)] = CodepointWidth::Narrow; + _map[UnicodeRange(319, 322)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(323, 323)] = CodepointWidth::Narrow; + _map[UnicodeRange(324, 324)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(325, 327)] = CodepointWidth::Narrow; + _map[UnicodeRange(328, 331)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(332, 332)] = CodepointWidth::Narrow; + _map[UnicodeRange(333, 333)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(334, 337)] = CodepointWidth::Narrow; + _map[UnicodeRange(338, 339)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(340, 357)] = CodepointWidth::Narrow; + _map[UnicodeRange(358, 359)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(360, 362)] = CodepointWidth::Narrow; + _map[UnicodeRange(363, 363)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(364, 461)] = CodepointWidth::Narrow; + _map[UnicodeRange(462, 462)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(463, 463)] = CodepointWidth::Narrow; + _map[UnicodeRange(464, 464)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(465, 465)] = CodepointWidth::Narrow; + _map[UnicodeRange(466, 466)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(467, 467)] = CodepointWidth::Narrow; + _map[UnicodeRange(468, 468)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(469, 469)] = CodepointWidth::Narrow; + _map[UnicodeRange(470, 470)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(471, 471)] = CodepointWidth::Narrow; + _map[UnicodeRange(472, 472)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(473, 473)] = CodepointWidth::Narrow; + _map[UnicodeRange(474, 474)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(475, 475)] = CodepointWidth::Narrow; + _map[UnicodeRange(476, 476)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(477, 592)] = CodepointWidth::Narrow; + _map[UnicodeRange(593, 593)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(594, 608)] = CodepointWidth::Narrow; + _map[UnicodeRange(609, 609)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(610, 707)] = CodepointWidth::Narrow; + _map[UnicodeRange(708, 708)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(709, 710)] = CodepointWidth::Narrow; + _map[UnicodeRange(711, 711)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(712, 712)] = CodepointWidth::Narrow; + _map[UnicodeRange(713, 715)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(716, 716)] = CodepointWidth::Narrow; + _map[UnicodeRange(717, 717)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(718, 719)] = CodepointWidth::Narrow; + _map[UnicodeRange(720, 720)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(721, 727)] = CodepointWidth::Narrow; + _map[UnicodeRange(728, 731)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(732, 732)] = CodepointWidth::Narrow; + _map[UnicodeRange(733, 733)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(734, 734)] = CodepointWidth::Narrow; + _map[UnicodeRange(735, 735)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(736, 767)] = CodepointWidth::Narrow; + _map[UnicodeRange(768, 879)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(880, 887)] = CodepointWidth::Narrow; + _map[UnicodeRange(890, 895)] = CodepointWidth::Narrow; + _map[UnicodeRange(900, 906)] = CodepointWidth::Narrow; + _map[UnicodeRange(908, 908)] = CodepointWidth::Narrow; + _map[UnicodeRange(910, 912)] = CodepointWidth::Narrow; + _map[UnicodeRange(913, 929)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(931, 937)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(938, 944)] = CodepointWidth::Narrow; + _map[UnicodeRange(945, 961)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(962, 962)] = CodepointWidth::Narrow; + _map[UnicodeRange(963, 969)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(970, 1024)] = CodepointWidth::Narrow; + _map[UnicodeRange(1025, 1025)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(1026, 1039)] = CodepointWidth::Narrow; + _map[UnicodeRange(1040, 1103)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(1104, 1104)] = CodepointWidth::Narrow; + _map[UnicodeRange(1105, 1105)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(1106, 1327)] = CodepointWidth::Narrow; + _map[UnicodeRange(1329, 1366)] = CodepointWidth::Narrow; + _map[UnicodeRange(1369, 1375)] = CodepointWidth::Narrow; + _map[UnicodeRange(1377, 1415)] = CodepointWidth::Narrow; + _map[UnicodeRange(1417, 1418)] = CodepointWidth::Narrow; + _map[UnicodeRange(1421, 1423)] = CodepointWidth::Narrow; + _map[UnicodeRange(1425, 1479)] = CodepointWidth::Narrow; + _map[UnicodeRange(1488, 1514)] = CodepointWidth::Narrow; + _map[UnicodeRange(1520, 1524)] = CodepointWidth::Narrow; + _map[UnicodeRange(1536, 1564)] = CodepointWidth::Narrow; + _map[UnicodeRange(1566, 1805)] = CodepointWidth::Narrow; + _map[UnicodeRange(1807, 1866)] = CodepointWidth::Narrow; + _map[UnicodeRange(1869, 1969)] = CodepointWidth::Narrow; + _map[UnicodeRange(1984, 2042)] = CodepointWidth::Narrow; + _map[UnicodeRange(2048, 2093)] = CodepointWidth::Narrow; + _map[UnicodeRange(2096, 2110)] = CodepointWidth::Narrow; + _map[UnicodeRange(2112, 2139)] = CodepointWidth::Narrow; + _map[UnicodeRange(2142, 2142)] = CodepointWidth::Narrow; + _map[UnicodeRange(2144, 2154)] = CodepointWidth::Narrow; + _map[UnicodeRange(2208, 2228)] = CodepointWidth::Narrow; + _map[UnicodeRange(2230, 2237)] = CodepointWidth::Narrow; + _map[UnicodeRange(2260, 2435)] = CodepointWidth::Narrow; + _map[UnicodeRange(2437, 2444)] = CodepointWidth::Narrow; + _map[UnicodeRange(2447, 2448)] = CodepointWidth::Narrow; + _map[UnicodeRange(2451, 2472)] = CodepointWidth::Narrow; + _map[UnicodeRange(2474, 2480)] = CodepointWidth::Narrow; + _map[UnicodeRange(2482, 2482)] = CodepointWidth::Narrow; + _map[UnicodeRange(2486, 2489)] = CodepointWidth::Narrow; + _map[UnicodeRange(2492, 2500)] = CodepointWidth::Narrow; + _map[UnicodeRange(2503, 2504)] = CodepointWidth::Narrow; + _map[UnicodeRange(2507, 2510)] = CodepointWidth::Narrow; + _map[UnicodeRange(2519, 2519)] = CodepointWidth::Narrow; + _map[UnicodeRange(2524, 2525)] = CodepointWidth::Narrow; + _map[UnicodeRange(2527, 2531)] = CodepointWidth::Narrow; + _map[UnicodeRange(2534, 2557)] = CodepointWidth::Narrow; + _map[UnicodeRange(2561, 2563)] = CodepointWidth::Narrow; + _map[UnicodeRange(2565, 2570)] = CodepointWidth::Narrow; + _map[UnicodeRange(2575, 2576)] = CodepointWidth::Narrow; + _map[UnicodeRange(2579, 2600)] = CodepointWidth::Narrow; + _map[UnicodeRange(2602, 2608)] = CodepointWidth::Narrow; + _map[UnicodeRange(2610, 2611)] = CodepointWidth::Narrow; + _map[UnicodeRange(2613, 2614)] = CodepointWidth::Narrow; + _map[UnicodeRange(2616, 2617)] = CodepointWidth::Narrow; + _map[UnicodeRange(2620, 2620)] = CodepointWidth::Narrow; + _map[UnicodeRange(2622, 2626)] = CodepointWidth::Narrow; + _map[UnicodeRange(2631, 2632)] = CodepointWidth::Narrow; + _map[UnicodeRange(2635, 2637)] = CodepointWidth::Narrow; + _map[UnicodeRange(2641, 2641)] = CodepointWidth::Narrow; + _map[UnicodeRange(2649, 2652)] = CodepointWidth::Narrow; + _map[UnicodeRange(2654, 2654)] = CodepointWidth::Narrow; + _map[UnicodeRange(2662, 2677)] = CodepointWidth::Narrow; + _map[UnicodeRange(2689, 2691)] = CodepointWidth::Narrow; + _map[UnicodeRange(2693, 2701)] = CodepointWidth::Narrow; + _map[UnicodeRange(2703, 2705)] = CodepointWidth::Narrow; + _map[UnicodeRange(2707, 2728)] = CodepointWidth::Narrow; + _map[UnicodeRange(2730, 2736)] = CodepointWidth::Narrow; + _map[UnicodeRange(2738, 2739)] = CodepointWidth::Narrow; + _map[UnicodeRange(2741, 2745)] = CodepointWidth::Narrow; + _map[UnicodeRange(2748, 2757)] = CodepointWidth::Narrow; + _map[UnicodeRange(2759, 2761)] = CodepointWidth::Narrow; + _map[UnicodeRange(2763, 2765)] = CodepointWidth::Narrow; + _map[UnicodeRange(2768, 2768)] = CodepointWidth::Narrow; + _map[UnicodeRange(2784, 2787)] = CodepointWidth::Narrow; + _map[UnicodeRange(2790, 2801)] = CodepointWidth::Narrow; + _map[UnicodeRange(2809, 2815)] = CodepointWidth::Narrow; + _map[UnicodeRange(2817, 2819)] = CodepointWidth::Narrow; + _map[UnicodeRange(2821, 2828)] = CodepointWidth::Narrow; + _map[UnicodeRange(2831, 2832)] = CodepointWidth::Narrow; + _map[UnicodeRange(2835, 2856)] = CodepointWidth::Narrow; + _map[UnicodeRange(2858, 2864)] = CodepointWidth::Narrow; + _map[UnicodeRange(2866, 2867)] = CodepointWidth::Narrow; + _map[UnicodeRange(2869, 2873)] = CodepointWidth::Narrow; + _map[UnicodeRange(2876, 2884)] = CodepointWidth::Narrow; + _map[UnicodeRange(2887, 2888)] = CodepointWidth::Narrow; + _map[UnicodeRange(2891, 2893)] = CodepointWidth::Narrow; + _map[UnicodeRange(2902, 2903)] = CodepointWidth::Narrow; + _map[UnicodeRange(2908, 2909)] = CodepointWidth::Narrow; + _map[UnicodeRange(2911, 2915)] = CodepointWidth::Narrow; + _map[UnicodeRange(2918, 2935)] = CodepointWidth::Narrow; + _map[UnicodeRange(2946, 2947)] = CodepointWidth::Narrow; + _map[UnicodeRange(2949, 2954)] = CodepointWidth::Narrow; + _map[UnicodeRange(2958, 2960)] = CodepointWidth::Narrow; + _map[UnicodeRange(2962, 2965)] = CodepointWidth::Narrow; + _map[UnicodeRange(2969, 2970)] = CodepointWidth::Narrow; + _map[UnicodeRange(2972, 2972)] = CodepointWidth::Narrow; + _map[UnicodeRange(2974, 2975)] = CodepointWidth::Narrow; + _map[UnicodeRange(2979, 2980)] = CodepointWidth::Narrow; + _map[UnicodeRange(2984, 2986)] = CodepointWidth::Narrow; + _map[UnicodeRange(2990, 3001)] = CodepointWidth::Narrow; + _map[UnicodeRange(3006, 3010)] = CodepointWidth::Narrow; + _map[UnicodeRange(3014, 3016)] = CodepointWidth::Narrow; + _map[UnicodeRange(3018, 3021)] = CodepointWidth::Narrow; + _map[UnicodeRange(3024, 3024)] = CodepointWidth::Narrow; + _map[UnicodeRange(3031, 3031)] = CodepointWidth::Narrow; + _map[UnicodeRange(3046, 3066)] = CodepointWidth::Narrow; + _map[UnicodeRange(3072, 3075)] = CodepointWidth::Narrow; + _map[UnicodeRange(3077, 3084)] = CodepointWidth::Narrow; + _map[UnicodeRange(3086, 3088)] = CodepointWidth::Narrow; + _map[UnicodeRange(3090, 3112)] = CodepointWidth::Narrow; + _map[UnicodeRange(3114, 3129)] = CodepointWidth::Narrow; + _map[UnicodeRange(3133, 3140)] = CodepointWidth::Narrow; + _map[UnicodeRange(3142, 3144)] = CodepointWidth::Narrow; + _map[UnicodeRange(3146, 3149)] = CodepointWidth::Narrow; + _map[UnicodeRange(3157, 3158)] = CodepointWidth::Narrow; + _map[UnicodeRange(3160, 3162)] = CodepointWidth::Narrow; + _map[UnicodeRange(3168, 3171)] = CodepointWidth::Narrow; + _map[UnicodeRange(3174, 3183)] = CodepointWidth::Narrow; + _map[UnicodeRange(3192, 3203)] = CodepointWidth::Narrow; + _map[UnicodeRange(3205, 3212)] = CodepointWidth::Narrow; + _map[UnicodeRange(3214, 3216)] = CodepointWidth::Narrow; + _map[UnicodeRange(3218, 3240)] = CodepointWidth::Narrow; + _map[UnicodeRange(3242, 3251)] = CodepointWidth::Narrow; + _map[UnicodeRange(3253, 3257)] = CodepointWidth::Narrow; + _map[UnicodeRange(3260, 3268)] = CodepointWidth::Narrow; + _map[UnicodeRange(3270, 3272)] = CodepointWidth::Narrow; + _map[UnicodeRange(3274, 3277)] = CodepointWidth::Narrow; + _map[UnicodeRange(3285, 3286)] = CodepointWidth::Narrow; + _map[UnicodeRange(3294, 3294)] = CodepointWidth::Narrow; + _map[UnicodeRange(3296, 3299)] = CodepointWidth::Narrow; + _map[UnicodeRange(3302, 3311)] = CodepointWidth::Narrow; + _map[UnicodeRange(3313, 3314)] = CodepointWidth::Narrow; + _map[UnicodeRange(3328, 3331)] = CodepointWidth::Narrow; + _map[UnicodeRange(3333, 3340)] = CodepointWidth::Narrow; + _map[UnicodeRange(3342, 3344)] = CodepointWidth::Narrow; + _map[UnicodeRange(3346, 3396)] = CodepointWidth::Narrow; + _map[UnicodeRange(3398, 3400)] = CodepointWidth::Narrow; + _map[UnicodeRange(3402, 3407)] = CodepointWidth::Narrow; + _map[UnicodeRange(3412, 3427)] = CodepointWidth::Narrow; + _map[UnicodeRange(3430, 3455)] = CodepointWidth::Narrow; + _map[UnicodeRange(3458, 3459)] = CodepointWidth::Narrow; + _map[UnicodeRange(3461, 3478)] = CodepointWidth::Narrow; + _map[UnicodeRange(3482, 3505)] = CodepointWidth::Narrow; + _map[UnicodeRange(3507, 3515)] = CodepointWidth::Narrow; + _map[UnicodeRange(3517, 3517)] = CodepointWidth::Narrow; + _map[UnicodeRange(3520, 3526)] = CodepointWidth::Narrow; + _map[UnicodeRange(3530, 3530)] = CodepointWidth::Narrow; + _map[UnicodeRange(3535, 3540)] = CodepointWidth::Narrow; + _map[UnicodeRange(3542, 3542)] = CodepointWidth::Narrow; + _map[UnicodeRange(3544, 3551)] = CodepointWidth::Narrow; + _map[UnicodeRange(3558, 3567)] = CodepointWidth::Narrow; + _map[UnicodeRange(3570, 3572)] = CodepointWidth::Narrow; + _map[UnicodeRange(3585, 3642)] = CodepointWidth::Narrow; + _map[UnicodeRange(3647, 3675)] = CodepointWidth::Narrow; + _map[UnicodeRange(3713, 3714)] = CodepointWidth::Narrow; + _map[UnicodeRange(3716, 3716)] = CodepointWidth::Narrow; + _map[UnicodeRange(3719, 3720)] = CodepointWidth::Narrow; + _map[UnicodeRange(3722, 3722)] = CodepointWidth::Narrow; + _map[UnicodeRange(3725, 3725)] = CodepointWidth::Narrow; + _map[UnicodeRange(3732, 3735)] = CodepointWidth::Narrow; + _map[UnicodeRange(3737, 3743)] = CodepointWidth::Narrow; + _map[UnicodeRange(3745, 3747)] = CodepointWidth::Narrow; + _map[UnicodeRange(3749, 3749)] = CodepointWidth::Narrow; + _map[UnicodeRange(3751, 3751)] = CodepointWidth::Narrow; + _map[UnicodeRange(3754, 3755)] = CodepointWidth::Narrow; + _map[UnicodeRange(3757, 3769)] = CodepointWidth::Narrow; + _map[UnicodeRange(3771, 3773)] = CodepointWidth::Narrow; + _map[UnicodeRange(3776, 3780)] = CodepointWidth::Narrow; + _map[UnicodeRange(3782, 3782)] = CodepointWidth::Narrow; + _map[UnicodeRange(3784, 3789)] = CodepointWidth::Narrow; + _map[UnicodeRange(3792, 3801)] = CodepointWidth::Narrow; + _map[UnicodeRange(3804, 3807)] = CodepointWidth::Narrow; + _map[UnicodeRange(3840, 3911)] = CodepointWidth::Narrow; + _map[UnicodeRange(3913, 3948)] = CodepointWidth::Narrow; + _map[UnicodeRange(3953, 3991)] = CodepointWidth::Narrow; + _map[UnicodeRange(3993, 4028)] = CodepointWidth::Narrow; + _map[UnicodeRange(4030, 4044)] = CodepointWidth::Narrow; + _map[UnicodeRange(4046, 4058)] = CodepointWidth::Narrow; + _map[UnicodeRange(4096, 4293)] = CodepointWidth::Narrow; + _map[UnicodeRange(4295, 4295)] = CodepointWidth::Narrow; + _map[UnicodeRange(4301, 4301)] = CodepointWidth::Narrow; + _map[UnicodeRange(4304, 4351)] = CodepointWidth::Narrow; + _map[UnicodeRange(4352, 4447)] = CodepointWidth::Wide; + _map[UnicodeRange(4448, 4680)] = CodepointWidth::Narrow; + _map[UnicodeRange(4682, 4685)] = CodepointWidth::Narrow; + _map[UnicodeRange(4688, 4694)] = CodepointWidth::Narrow; + _map[UnicodeRange(4696, 4696)] = CodepointWidth::Narrow; + _map[UnicodeRange(4698, 4701)] = CodepointWidth::Narrow; + _map[UnicodeRange(4704, 4744)] = CodepointWidth::Narrow; + _map[UnicodeRange(4746, 4749)] = CodepointWidth::Narrow; + _map[UnicodeRange(4752, 4784)] = CodepointWidth::Narrow; + _map[UnicodeRange(4786, 4789)] = CodepointWidth::Narrow; + _map[UnicodeRange(4792, 4798)] = CodepointWidth::Narrow; + _map[UnicodeRange(4800, 4800)] = CodepointWidth::Narrow; + _map[UnicodeRange(4802, 4805)] = CodepointWidth::Narrow; + _map[UnicodeRange(4808, 4822)] = CodepointWidth::Narrow; + _map[UnicodeRange(4824, 4880)] = CodepointWidth::Narrow; + _map[UnicodeRange(4882, 4885)] = CodepointWidth::Narrow; + _map[UnicodeRange(4888, 4954)] = CodepointWidth::Narrow; + _map[UnicodeRange(4957, 4988)] = CodepointWidth::Narrow; + _map[UnicodeRange(4992, 5017)] = CodepointWidth::Narrow; + _map[UnicodeRange(5024, 5109)] = CodepointWidth::Narrow; + _map[UnicodeRange(5112, 5117)] = CodepointWidth::Narrow; + _map[UnicodeRange(5120, 5788)] = CodepointWidth::Narrow; + _map[UnicodeRange(5792, 5880)] = CodepointWidth::Narrow; + _map[UnicodeRange(5888, 5900)] = CodepointWidth::Narrow; + _map[UnicodeRange(5902, 5908)] = CodepointWidth::Narrow; + _map[UnicodeRange(5920, 5942)] = CodepointWidth::Narrow; + _map[UnicodeRange(5952, 5971)] = CodepointWidth::Narrow; + _map[UnicodeRange(5984, 5996)] = CodepointWidth::Narrow; + _map[UnicodeRange(5998, 6000)] = CodepointWidth::Narrow; + _map[UnicodeRange(6002, 6003)] = CodepointWidth::Narrow; + _map[UnicodeRange(6016, 6109)] = CodepointWidth::Narrow; + _map[UnicodeRange(6112, 6121)] = CodepointWidth::Narrow; + _map[UnicodeRange(6128, 6137)] = CodepointWidth::Narrow; + _map[UnicodeRange(6144, 6158)] = CodepointWidth::Narrow; + _map[UnicodeRange(6160, 6169)] = CodepointWidth::Narrow; + _map[UnicodeRange(6176, 6263)] = CodepointWidth::Narrow; + _map[UnicodeRange(6272, 6314)] = CodepointWidth::Narrow; + _map[UnicodeRange(6320, 6389)] = CodepointWidth::Narrow; + _map[UnicodeRange(6400, 6430)] = CodepointWidth::Narrow; + _map[UnicodeRange(6432, 6443)] = CodepointWidth::Narrow; + _map[UnicodeRange(6448, 6459)] = CodepointWidth::Narrow; + _map[UnicodeRange(6464, 6464)] = CodepointWidth::Narrow; + _map[UnicodeRange(6468, 6509)] = CodepointWidth::Narrow; + _map[UnicodeRange(6512, 6516)] = CodepointWidth::Narrow; + _map[UnicodeRange(6528, 6571)] = CodepointWidth::Narrow; + _map[UnicodeRange(6576, 6601)] = CodepointWidth::Narrow; + _map[UnicodeRange(6608, 6618)] = CodepointWidth::Narrow; + _map[UnicodeRange(6622, 6683)] = CodepointWidth::Narrow; + _map[UnicodeRange(6686, 6750)] = CodepointWidth::Narrow; + _map[UnicodeRange(6752, 6780)] = CodepointWidth::Narrow; + _map[UnicodeRange(6783, 6793)] = CodepointWidth::Narrow; + _map[UnicodeRange(6800, 6809)] = CodepointWidth::Narrow; + _map[UnicodeRange(6816, 6829)] = CodepointWidth::Narrow; + _map[UnicodeRange(6832, 6846)] = CodepointWidth::Narrow; + _map[UnicodeRange(6912, 6987)] = CodepointWidth::Narrow; + _map[UnicodeRange(6992, 7036)] = CodepointWidth::Narrow; + _map[UnicodeRange(7040, 7155)] = CodepointWidth::Narrow; + _map[UnicodeRange(7164, 7223)] = CodepointWidth::Narrow; + _map[UnicodeRange(7227, 7241)] = CodepointWidth::Narrow; + _map[UnicodeRange(7245, 7304)] = CodepointWidth::Narrow; + _map[UnicodeRange(7360, 7367)] = CodepointWidth::Narrow; + _map[UnicodeRange(7376, 7417)] = CodepointWidth::Narrow; + _map[UnicodeRange(7424, 7673)] = CodepointWidth::Narrow; + _map[UnicodeRange(7675, 7957)] = CodepointWidth::Narrow; + _map[UnicodeRange(7960, 7965)] = CodepointWidth::Narrow; + _map[UnicodeRange(7968, 8005)] = CodepointWidth::Narrow; + _map[UnicodeRange(8008, 8013)] = CodepointWidth::Narrow; + _map[UnicodeRange(8016, 8023)] = CodepointWidth::Narrow; + _map[UnicodeRange(8025, 8025)] = CodepointWidth::Narrow; + _map[UnicodeRange(8027, 8027)] = CodepointWidth::Narrow; + _map[UnicodeRange(8029, 8029)] = CodepointWidth::Narrow; + _map[UnicodeRange(8031, 8061)] = CodepointWidth::Narrow; + _map[UnicodeRange(8064, 8116)] = CodepointWidth::Narrow; + _map[UnicodeRange(8118, 8132)] = CodepointWidth::Narrow; + _map[UnicodeRange(8134, 8147)] = CodepointWidth::Narrow; + _map[UnicodeRange(8150, 8155)] = CodepointWidth::Narrow; + _map[UnicodeRange(8157, 8175)] = CodepointWidth::Narrow; + _map[UnicodeRange(8178, 8180)] = CodepointWidth::Narrow; + _map[UnicodeRange(8182, 8190)] = CodepointWidth::Narrow; + _map[UnicodeRange(8192, 8207)] = CodepointWidth::Narrow; + _map[UnicodeRange(8208, 8208)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8209, 8210)] = CodepointWidth::Narrow; + _map[UnicodeRange(8211, 8214)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8215, 8215)] = CodepointWidth::Narrow; + _map[UnicodeRange(8216, 8217)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8218, 8219)] = CodepointWidth::Narrow; + _map[UnicodeRange(8220, 8221)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8222, 8223)] = CodepointWidth::Narrow; + _map[UnicodeRange(8224, 8226)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8227, 8227)] = CodepointWidth::Narrow; + _map[UnicodeRange(8228, 8231)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8232, 8239)] = CodepointWidth::Narrow; + _map[UnicodeRange(8240, 8240)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8241, 8241)] = CodepointWidth::Narrow; + _map[UnicodeRange(8242, 8243)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8244, 8244)] = CodepointWidth::Narrow; + _map[UnicodeRange(8245, 8245)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8246, 8250)] = CodepointWidth::Narrow; + _map[UnicodeRange(8251, 8251)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8252, 8253)] = CodepointWidth::Narrow; + _map[UnicodeRange(8254, 8254)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8255, 8292)] = CodepointWidth::Narrow; + _map[UnicodeRange(8294, 8305)] = CodepointWidth::Narrow; + _map[UnicodeRange(8308, 8308)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8309, 8318)] = CodepointWidth::Narrow; + _map[UnicodeRange(8319, 8319)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8320, 8320)] = CodepointWidth::Narrow; + _map[UnicodeRange(8321, 8324)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8325, 8334)] = CodepointWidth::Narrow; + _map[UnicodeRange(8336, 8348)] = CodepointWidth::Narrow; + _map[UnicodeRange(8352, 8363)] = CodepointWidth::Narrow; + _map[UnicodeRange(8364, 8364)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8365, 8383)] = CodepointWidth::Narrow; + _map[UnicodeRange(8400, 8432)] = CodepointWidth::Narrow; + _map[UnicodeRange(8448, 8450)] = CodepointWidth::Narrow; + _map[UnicodeRange(8451, 8451)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8452, 8452)] = CodepointWidth::Narrow; + _map[UnicodeRange(8453, 8453)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8454, 8456)] = CodepointWidth::Narrow; + _map[UnicodeRange(8457, 8457)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8458, 8466)] = CodepointWidth::Narrow; + _map[UnicodeRange(8467, 8467)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8468, 8469)] = CodepointWidth::Narrow; + _map[UnicodeRange(8470, 8470)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8471, 8480)] = CodepointWidth::Narrow; + _map[UnicodeRange(8481, 8482)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8483, 8485)] = CodepointWidth::Narrow; + _map[UnicodeRange(8486, 8486)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8487, 8490)] = CodepointWidth::Narrow; + _map[UnicodeRange(8491, 8491)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8492, 8530)] = CodepointWidth::Narrow; + _map[UnicodeRange(8531, 8532)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8533, 8538)] = CodepointWidth::Narrow; + _map[UnicodeRange(8539, 8542)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8543, 8543)] = CodepointWidth::Narrow; + _map[UnicodeRange(8544, 8555)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8556, 8559)] = CodepointWidth::Narrow; + _map[UnicodeRange(8560, 8569)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8570, 8584)] = CodepointWidth::Narrow; + _map[UnicodeRange(8585, 8585)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8586, 8587)] = CodepointWidth::Narrow; + _map[UnicodeRange(8592, 8601)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8602, 8631)] = CodepointWidth::Narrow; + _map[UnicodeRange(8632, 8633)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8634, 8657)] = CodepointWidth::Narrow; + _map[UnicodeRange(8658, 8658)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8659, 8659)] = CodepointWidth::Narrow; + _map[UnicodeRange(8660, 8660)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8661, 8678)] = CodepointWidth::Narrow; + _map[UnicodeRange(8679, 8679)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8680, 8703)] = CodepointWidth::Narrow; + _map[UnicodeRange(8704, 8704)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8705, 8705)] = CodepointWidth::Narrow; + _map[UnicodeRange(8706, 8707)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8708, 8710)] = CodepointWidth::Narrow; + _map[UnicodeRange(8711, 8712)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8713, 8714)] = CodepointWidth::Narrow; + _map[UnicodeRange(8715, 8715)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8716, 8718)] = CodepointWidth::Narrow; + _map[UnicodeRange(8719, 8719)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8720, 8720)] = CodepointWidth::Narrow; + _map[UnicodeRange(8721, 8721)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8722, 8724)] = CodepointWidth::Narrow; + _map[UnicodeRange(8725, 8725)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8726, 8729)] = CodepointWidth::Narrow; + _map[UnicodeRange(8730, 8730)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8731, 8732)] = CodepointWidth::Narrow; + _map[UnicodeRange(8733, 8736)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8737, 8738)] = CodepointWidth::Narrow; + _map[UnicodeRange(8739, 8739)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8740, 8740)] = CodepointWidth::Narrow; + _map[UnicodeRange(8741, 8741)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8742, 8742)] = CodepointWidth::Narrow; + _map[UnicodeRange(8743, 8748)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8749, 8749)] = CodepointWidth::Narrow; + _map[UnicodeRange(8750, 8750)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8751, 8755)] = CodepointWidth::Narrow; + _map[UnicodeRange(8756, 8759)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8760, 8763)] = CodepointWidth::Narrow; + _map[UnicodeRange(8764, 8765)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8766, 8775)] = CodepointWidth::Narrow; + _map[UnicodeRange(8776, 8776)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8777, 8779)] = CodepointWidth::Narrow; + _map[UnicodeRange(8780, 8780)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8781, 8785)] = CodepointWidth::Narrow; + _map[UnicodeRange(8786, 8786)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8787, 8799)] = CodepointWidth::Narrow; + _map[UnicodeRange(8800, 8801)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8802, 8803)] = CodepointWidth::Narrow; + _map[UnicodeRange(8804, 8807)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8808, 8809)] = CodepointWidth::Narrow; + _map[UnicodeRange(8810, 8811)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8812, 8813)] = CodepointWidth::Narrow; + _map[UnicodeRange(8814, 8815)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8816, 8833)] = CodepointWidth::Narrow; + _map[UnicodeRange(8834, 8835)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8836, 8837)] = CodepointWidth::Narrow; + _map[UnicodeRange(8838, 8839)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8840, 8852)] = CodepointWidth::Narrow; + _map[UnicodeRange(8853, 8853)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8854, 8856)] = CodepointWidth::Narrow; + _map[UnicodeRange(8857, 8857)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8858, 8868)] = CodepointWidth::Narrow; + _map[UnicodeRange(8869, 8869)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8870, 8894)] = CodepointWidth::Narrow; + _map[UnicodeRange(8895, 8895)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8896, 8977)] = CodepointWidth::Narrow; + _map[UnicodeRange(8978, 8978)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(8979, 8985)] = CodepointWidth::Narrow; + _map[UnicodeRange(8986, 8987)] = CodepointWidth::Wide; + _map[UnicodeRange(8988, 9000)] = CodepointWidth::Narrow; + _map[UnicodeRange(9001, 9002)] = CodepointWidth::Wide; + _map[UnicodeRange(9003, 9192)] = CodepointWidth::Narrow; + _map[UnicodeRange(9193, 9196)] = CodepointWidth::Wide; + _map[UnicodeRange(9197, 9199)] = CodepointWidth::Narrow; + _map[UnicodeRange(9200, 9200)] = CodepointWidth::Wide; + _map[UnicodeRange(9201, 9202)] = CodepointWidth::Narrow; + _map[UnicodeRange(9203, 9203)] = CodepointWidth::Wide; + _map[UnicodeRange(9204, 9254)] = CodepointWidth::Narrow; + _map[UnicodeRange(9280, 9290)] = CodepointWidth::Narrow; + _map[UnicodeRange(9312, 9449)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9450, 9450)] = CodepointWidth::Narrow; + _map[UnicodeRange(9451, 9547)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9548, 9551)] = CodepointWidth::Narrow; + _map[UnicodeRange(9552, 9587)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9588, 9599)] = CodepointWidth::Narrow; + _map[UnicodeRange(9600, 9615)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9616, 9617)] = CodepointWidth::Narrow; + _map[UnicodeRange(9618, 9621)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9622, 9631)] = CodepointWidth::Narrow; + _map[UnicodeRange(9632, 9633)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9634, 9634)] = CodepointWidth::Narrow; + _map[UnicodeRange(9635, 9641)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9642, 9649)] = CodepointWidth::Narrow; + _map[UnicodeRange(9650, 9651)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9652, 9653)] = CodepointWidth::Narrow; + _map[UnicodeRange(9654, 9655)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9656, 9659)] = CodepointWidth::Narrow; + _map[UnicodeRange(9660, 9661)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9662, 9663)] = CodepointWidth::Narrow; + _map[UnicodeRange(9664, 9665)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9666, 9669)] = CodepointWidth::Narrow; + _map[UnicodeRange(9670, 9672)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9673, 9674)] = CodepointWidth::Narrow; + _map[UnicodeRange(9675, 9675)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9676, 9677)] = CodepointWidth::Narrow; + _map[UnicodeRange(9678, 9681)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9682, 9697)] = CodepointWidth::Narrow; + _map[UnicodeRange(9698, 9701)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9702, 9710)] = CodepointWidth::Narrow; + _map[UnicodeRange(9711, 9711)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9712, 9724)] = CodepointWidth::Narrow; + _map[UnicodeRange(9725, 9726)] = CodepointWidth::Wide; + _map[UnicodeRange(9727, 9732)] = CodepointWidth::Narrow; + _map[UnicodeRange(9733, 9734)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9735, 9736)] = CodepointWidth::Narrow; + _map[UnicodeRange(9737, 9737)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9738, 9741)] = CodepointWidth::Narrow; + _map[UnicodeRange(9742, 9743)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9744, 9747)] = CodepointWidth::Narrow; + _map[UnicodeRange(9748, 9749)] = CodepointWidth::Wide; + _map[UnicodeRange(9750, 9755)] = CodepointWidth::Narrow; + _map[UnicodeRange(9756, 9756)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9757, 9757)] = CodepointWidth::Narrow; + _map[UnicodeRange(9758, 9758)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9759, 9791)] = CodepointWidth::Narrow; + _map[UnicodeRange(9792, 9792)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9793, 9793)] = CodepointWidth::Narrow; + _map[UnicodeRange(9794, 9794)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9795, 9799)] = CodepointWidth::Narrow; + _map[UnicodeRange(9800, 9811)] = CodepointWidth::Wide; + _map[UnicodeRange(9812, 9823)] = CodepointWidth::Narrow; + _map[UnicodeRange(9824, 9825)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9826, 9826)] = CodepointWidth::Narrow; + _map[UnicodeRange(9827, 9829)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9830, 9830)] = CodepointWidth::Narrow; + _map[UnicodeRange(9831, 9834)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9835, 9835)] = CodepointWidth::Narrow; + _map[UnicodeRange(9836, 9837)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9838, 9838)] = CodepointWidth::Narrow; + _map[UnicodeRange(9839, 9839)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9840, 9854)] = CodepointWidth::Narrow; + _map[UnicodeRange(9855, 9855)] = CodepointWidth::Wide; + _map[UnicodeRange(9856, 9874)] = CodepointWidth::Narrow; + _map[UnicodeRange(9875, 9875)] = CodepointWidth::Wide; + _map[UnicodeRange(9876, 9885)] = CodepointWidth::Narrow; + _map[UnicodeRange(9886, 9887)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9888, 9888)] = CodepointWidth::Narrow; + _map[UnicodeRange(9889, 9889)] = CodepointWidth::Wide; + _map[UnicodeRange(9890, 9897)] = CodepointWidth::Narrow; + _map[UnicodeRange(9898, 9899)] = CodepointWidth::Wide; + _map[UnicodeRange(9900, 9916)] = CodepointWidth::Narrow; + _map[UnicodeRange(9917, 9918)] = CodepointWidth::Wide; + _map[UnicodeRange(9919, 9919)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9920, 9923)] = CodepointWidth::Narrow; + _map[UnicodeRange(9924, 9925)] = CodepointWidth::Wide; + _map[UnicodeRange(9926, 9933)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9934, 9934)] = CodepointWidth::Wide; + _map[UnicodeRange(9935, 9939)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9940, 9940)] = CodepointWidth::Wide; + _map[UnicodeRange(9941, 9953)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9954, 9954)] = CodepointWidth::Narrow; + _map[UnicodeRange(9955, 9955)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9956, 9959)] = CodepointWidth::Narrow; + _map[UnicodeRange(9960, 9961)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9962, 9962)] = CodepointWidth::Wide; + _map[UnicodeRange(9963, 9969)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9970, 9971)] = CodepointWidth::Wide; + _map[UnicodeRange(9972, 9972)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9973, 9973)] = CodepointWidth::Wide; + _map[UnicodeRange(9974, 9977)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9978, 9978)] = CodepointWidth::Wide; + _map[UnicodeRange(9979, 9980)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9981, 9981)] = CodepointWidth::Wide; + _map[UnicodeRange(9982, 9983)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(9984, 9988)] = CodepointWidth::Narrow; + _map[UnicodeRange(9989, 9989)] = CodepointWidth::Wide; + _map[UnicodeRange(9990, 9993)] = CodepointWidth::Narrow; + _map[UnicodeRange(9994, 9995)] = CodepointWidth::Wide; + _map[UnicodeRange(9996, 10023)] = CodepointWidth::Narrow; + _map[UnicodeRange(10024, 10024)] = CodepointWidth::Wide; + _map[UnicodeRange(10025, 10044)] = CodepointWidth::Narrow; + _map[UnicodeRange(10045, 10045)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(10046, 10059)] = CodepointWidth::Narrow; + _map[UnicodeRange(10060, 10060)] = CodepointWidth::Wide; + _map[UnicodeRange(10061, 10061)] = CodepointWidth::Narrow; + _map[UnicodeRange(10062, 10062)] = CodepointWidth::Wide; + _map[UnicodeRange(10063, 10066)] = CodepointWidth::Narrow; + _map[UnicodeRange(10067, 10069)] = CodepointWidth::Wide; + _map[UnicodeRange(10070, 10070)] = CodepointWidth::Narrow; + _map[UnicodeRange(10071, 10071)] = CodepointWidth::Wide; + _map[UnicodeRange(10072, 10101)] = CodepointWidth::Narrow; + _map[UnicodeRange(10102, 10111)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(10112, 10132)] = CodepointWidth::Narrow; + _map[UnicodeRange(10133, 10135)] = CodepointWidth::Wide; + _map[UnicodeRange(10136, 10159)] = CodepointWidth::Narrow; + _map[UnicodeRange(10160, 10160)] = CodepointWidth::Wide; + _map[UnicodeRange(10161, 10174)] = CodepointWidth::Narrow; + _map[UnicodeRange(10175, 10175)] = CodepointWidth::Wide; + _map[UnicodeRange(10176, 11034)] = CodepointWidth::Narrow; + _map[UnicodeRange(11035, 11036)] = CodepointWidth::Wide; + _map[UnicodeRange(11037, 11087)] = CodepointWidth::Narrow; + _map[UnicodeRange(11088, 11088)] = CodepointWidth::Wide; + _map[UnicodeRange(11089, 11092)] = CodepointWidth::Narrow; + _map[UnicodeRange(11093, 11093)] = CodepointWidth::Wide; + _map[UnicodeRange(11094, 11097)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(11098, 11123)] = CodepointWidth::Narrow; + _map[UnicodeRange(11126, 11157)] = CodepointWidth::Narrow; + _map[UnicodeRange(11160, 11193)] = CodepointWidth::Narrow; + _map[UnicodeRange(11197, 11208)] = CodepointWidth::Narrow; + _map[UnicodeRange(11210, 11218)] = CodepointWidth::Narrow; + _map[UnicodeRange(11244, 11247)] = CodepointWidth::Narrow; + _map[UnicodeRange(11264, 11310)] = CodepointWidth::Narrow; + _map[UnicodeRange(11312, 11358)] = CodepointWidth::Narrow; + _map[UnicodeRange(11360, 11507)] = CodepointWidth::Narrow; + _map[UnicodeRange(11513, 11557)] = CodepointWidth::Narrow; + _map[UnicodeRange(11559, 11559)] = CodepointWidth::Narrow; + _map[UnicodeRange(11565, 11565)] = CodepointWidth::Narrow; + _map[UnicodeRange(11568, 11623)] = CodepointWidth::Narrow; + _map[UnicodeRange(11631, 11632)] = CodepointWidth::Narrow; + _map[UnicodeRange(11647, 11670)] = CodepointWidth::Narrow; + _map[UnicodeRange(11680, 11686)] = CodepointWidth::Narrow; + _map[UnicodeRange(11688, 11694)] = CodepointWidth::Narrow; + _map[UnicodeRange(11696, 11702)] = CodepointWidth::Narrow; + _map[UnicodeRange(11704, 11710)] = CodepointWidth::Narrow; + _map[UnicodeRange(11712, 11718)] = CodepointWidth::Narrow; + _map[UnicodeRange(11720, 11726)] = CodepointWidth::Narrow; + _map[UnicodeRange(11728, 11734)] = CodepointWidth::Narrow; + _map[UnicodeRange(11736, 11742)] = CodepointWidth::Narrow; + _map[UnicodeRange(11744, 11849)] = CodepointWidth::Narrow; + _map[UnicodeRange(11904, 11929)] = CodepointWidth::Wide; + _map[UnicodeRange(11931, 12019)] = CodepointWidth::Wide; + _map[UnicodeRange(12032, 12245)] = CodepointWidth::Wide; + _map[UnicodeRange(12272, 12283)] = CodepointWidth::Wide; + _map[UnicodeRange(12288, 12350)] = CodepointWidth::Wide; + _map[UnicodeRange(12351, 12351)] = CodepointWidth::Narrow; + _map[UnicodeRange(12353, 12438)] = CodepointWidth::Wide; + _map[UnicodeRange(12441, 12543)] = CodepointWidth::Wide; + _map[UnicodeRange(12549, 12590)] = CodepointWidth::Wide; + _map[UnicodeRange(12593, 12686)] = CodepointWidth::Wide; + _map[UnicodeRange(12688, 12730)] = CodepointWidth::Wide; + _map[UnicodeRange(12736, 12771)] = CodepointWidth::Wide; + _map[UnicodeRange(12784, 12830)] = CodepointWidth::Wide; + _map[UnicodeRange(12832, 12871)] = CodepointWidth::Wide; + _map[UnicodeRange(12872, 12879)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(12880, 13054)] = CodepointWidth::Wide; + _map[UnicodeRange(13056, 19903)] = CodepointWidth::Wide; + _map[UnicodeRange(19904, 19967)] = CodepointWidth::Narrow; + _map[UnicodeRange(19968, 42124)] = CodepointWidth::Wide; + _map[UnicodeRange(42128, 42182)] = CodepointWidth::Wide; + _map[UnicodeRange(42192, 42539)] = CodepointWidth::Narrow; + _map[UnicodeRange(42560, 42743)] = CodepointWidth::Narrow; + _map[UnicodeRange(42752, 42926)] = CodepointWidth::Narrow; + _map[UnicodeRange(42928, 42935)] = CodepointWidth::Narrow; + _map[UnicodeRange(42999, 43051)] = CodepointWidth::Narrow; + _map[UnicodeRange(43056, 43065)] = CodepointWidth::Narrow; + _map[UnicodeRange(43072, 43127)] = CodepointWidth::Narrow; + _map[UnicodeRange(43136, 43205)] = CodepointWidth::Narrow; + _map[UnicodeRange(43214, 43225)] = CodepointWidth::Narrow; + _map[UnicodeRange(43232, 43261)] = CodepointWidth::Narrow; + _map[UnicodeRange(43264, 43347)] = CodepointWidth::Narrow; + _map[UnicodeRange(43359, 43359)] = CodepointWidth::Narrow; + _map[UnicodeRange(43360, 43388)] = CodepointWidth::Wide; + _map[UnicodeRange(43392, 43469)] = CodepointWidth::Narrow; + _map[UnicodeRange(43471, 43481)] = CodepointWidth::Narrow; + _map[UnicodeRange(43486, 43518)] = CodepointWidth::Narrow; + _map[UnicodeRange(43520, 43574)] = CodepointWidth::Narrow; + _map[UnicodeRange(43584, 43597)] = CodepointWidth::Narrow; + _map[UnicodeRange(43600, 43609)] = CodepointWidth::Narrow; + _map[UnicodeRange(43612, 43714)] = CodepointWidth::Narrow; + _map[UnicodeRange(43739, 43766)] = CodepointWidth::Narrow; + _map[UnicodeRange(43777, 43782)] = CodepointWidth::Narrow; + _map[UnicodeRange(43785, 43790)] = CodepointWidth::Narrow; + _map[UnicodeRange(43793, 43798)] = CodepointWidth::Narrow; + _map[UnicodeRange(43808, 43814)] = CodepointWidth::Narrow; + _map[UnicodeRange(43816, 43822)] = CodepointWidth::Narrow; + _map[UnicodeRange(43824, 43877)] = CodepointWidth::Narrow; + _map[UnicodeRange(43888, 44013)] = CodepointWidth::Narrow; + _map[UnicodeRange(44016, 44025)] = CodepointWidth::Narrow; + _map[UnicodeRange(44032, 55203)] = CodepointWidth::Wide; + _map[UnicodeRange(55216, 55238)] = CodepointWidth::Narrow; + _map[UnicodeRange(55243, 55291)] = CodepointWidth::Narrow; + _map[UnicodeRange(55296, 57343)] = CodepointWidth::Narrow; + _map[UnicodeRange(57344, 63743)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(63744, 64255)] = CodepointWidth::Wide; + _map[UnicodeRange(64256, 64262)] = CodepointWidth::Narrow; + _map[UnicodeRange(64275, 64279)] = CodepointWidth::Narrow; + _map[UnicodeRange(64285, 64310)] = CodepointWidth::Narrow; + _map[UnicodeRange(64312, 64316)] = CodepointWidth::Narrow; + _map[UnicodeRange(64318, 64318)] = CodepointWidth::Narrow; + _map[UnicodeRange(64320, 64321)] = CodepointWidth::Narrow; + _map[UnicodeRange(64323, 64324)] = CodepointWidth::Narrow; + _map[UnicodeRange(64326, 64449)] = CodepointWidth::Narrow; + _map[UnicodeRange(64467, 64831)] = CodepointWidth::Narrow; + _map[UnicodeRange(64848, 64911)] = CodepointWidth::Narrow; + _map[UnicodeRange(64914, 64967)] = CodepointWidth::Narrow; + _map[UnicodeRange(65008, 65021)] = CodepointWidth::Narrow; + _map[UnicodeRange(65024, 65039)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(65040, 65049)] = CodepointWidth::Wide; + _map[UnicodeRange(65056, 65071)] = CodepointWidth::Narrow; + _map[UnicodeRange(65072, 65106)] = CodepointWidth::Wide; + _map[UnicodeRange(65108, 65126)] = CodepointWidth::Wide; + _map[UnicodeRange(65128, 65131)] = CodepointWidth::Wide; + _map[UnicodeRange(65136, 65140)] = CodepointWidth::Narrow; + _map[UnicodeRange(65142, 65276)] = CodepointWidth::Narrow; + _map[UnicodeRange(65279, 65279)] = CodepointWidth::Narrow; + _map[UnicodeRange(65281, 65376)] = CodepointWidth::Wide; + _map[UnicodeRange(65377, 65470)] = CodepointWidth::Narrow; + _map[UnicodeRange(65474, 65479)] = CodepointWidth::Narrow; + _map[UnicodeRange(65482, 65487)] = CodepointWidth::Narrow; + _map[UnicodeRange(65490, 65495)] = CodepointWidth::Narrow; + _map[UnicodeRange(65498, 65500)] = CodepointWidth::Narrow; + _map[UnicodeRange(65504, 65510)] = CodepointWidth::Wide; + _map[UnicodeRange(65512, 65518)] = CodepointWidth::Narrow; + _map[UnicodeRange(65529, 65532)] = CodepointWidth::Narrow; + _map[UnicodeRange(65533, 65533)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(65536, 65547)] = CodepointWidth::Narrow; + _map[UnicodeRange(65549, 65574)] = CodepointWidth::Narrow; + _map[UnicodeRange(65576, 65594)] = CodepointWidth::Narrow; + _map[UnicodeRange(65596, 65597)] = CodepointWidth::Narrow; + _map[UnicodeRange(65599, 65613)] = CodepointWidth::Narrow; + _map[UnicodeRange(65616, 65629)] = CodepointWidth::Narrow; + _map[UnicodeRange(65664, 65786)] = CodepointWidth::Narrow; + _map[UnicodeRange(65792, 65794)] = CodepointWidth::Narrow; + _map[UnicodeRange(65799, 65843)] = CodepointWidth::Narrow; + _map[UnicodeRange(65847, 65934)] = CodepointWidth::Narrow; + _map[UnicodeRange(65936, 65947)] = CodepointWidth::Narrow; + _map[UnicodeRange(65952, 65952)] = CodepointWidth::Narrow; + _map[UnicodeRange(66000, 66045)] = CodepointWidth::Narrow; + _map[UnicodeRange(66176, 66204)] = CodepointWidth::Narrow; + _map[UnicodeRange(66208, 66256)] = CodepointWidth::Narrow; + _map[UnicodeRange(66272, 66299)] = CodepointWidth::Narrow; + _map[UnicodeRange(66304, 66339)] = CodepointWidth::Narrow; + _map[UnicodeRange(66349, 66378)] = CodepointWidth::Narrow; + _map[UnicodeRange(66384, 66426)] = CodepointWidth::Narrow; + _map[UnicodeRange(66432, 66461)] = CodepointWidth::Narrow; + _map[UnicodeRange(66463, 66499)] = CodepointWidth::Narrow; + _map[UnicodeRange(66504, 66517)] = CodepointWidth::Narrow; + _map[UnicodeRange(66560, 66717)] = CodepointWidth::Narrow; + _map[UnicodeRange(66720, 66729)] = CodepointWidth::Narrow; + _map[UnicodeRange(66736, 66771)] = CodepointWidth::Narrow; + _map[UnicodeRange(66776, 66811)] = CodepointWidth::Narrow; + _map[UnicodeRange(66816, 66855)] = CodepointWidth::Narrow; + _map[UnicodeRange(66864, 66915)] = CodepointWidth::Narrow; + _map[UnicodeRange(66927, 66927)] = CodepointWidth::Narrow; + _map[UnicodeRange(67072, 67382)] = CodepointWidth::Narrow; + _map[UnicodeRange(67392, 67413)] = CodepointWidth::Narrow; + _map[UnicodeRange(67424, 67431)] = CodepointWidth::Narrow; + _map[UnicodeRange(67584, 67589)] = CodepointWidth::Narrow; + _map[UnicodeRange(67592, 67592)] = CodepointWidth::Narrow; + _map[UnicodeRange(67594, 67637)] = CodepointWidth::Narrow; + _map[UnicodeRange(67639, 67640)] = CodepointWidth::Narrow; + _map[UnicodeRange(67644, 67644)] = CodepointWidth::Narrow; + _map[UnicodeRange(67647, 67669)] = CodepointWidth::Narrow; + _map[UnicodeRange(67671, 67742)] = CodepointWidth::Narrow; + _map[UnicodeRange(67751, 67759)] = CodepointWidth::Narrow; + _map[UnicodeRange(67808, 67826)] = CodepointWidth::Narrow; + _map[UnicodeRange(67828, 67829)] = CodepointWidth::Narrow; + _map[UnicodeRange(67835, 67867)] = CodepointWidth::Narrow; + _map[UnicodeRange(67871, 67897)] = CodepointWidth::Narrow; + _map[UnicodeRange(67903, 67903)] = CodepointWidth::Narrow; + _map[UnicodeRange(67968, 68023)] = CodepointWidth::Narrow; + _map[UnicodeRange(68028, 68047)] = CodepointWidth::Narrow; + _map[UnicodeRange(68050, 68099)] = CodepointWidth::Narrow; + _map[UnicodeRange(68101, 68102)] = CodepointWidth::Narrow; + _map[UnicodeRange(68108, 68115)] = CodepointWidth::Narrow; + _map[UnicodeRange(68117, 68119)] = CodepointWidth::Narrow; + _map[UnicodeRange(68121, 68147)] = CodepointWidth::Narrow; + _map[UnicodeRange(68152, 68154)] = CodepointWidth::Narrow; + _map[UnicodeRange(68159, 68167)] = CodepointWidth::Narrow; + _map[UnicodeRange(68176, 68184)] = CodepointWidth::Narrow; + _map[UnicodeRange(68192, 68255)] = CodepointWidth::Narrow; + _map[UnicodeRange(68288, 68326)] = CodepointWidth::Narrow; + _map[UnicodeRange(68331, 68342)] = CodepointWidth::Narrow; + _map[UnicodeRange(68352, 68405)] = CodepointWidth::Narrow; + _map[UnicodeRange(68409, 68437)] = CodepointWidth::Narrow; + _map[UnicodeRange(68440, 68466)] = CodepointWidth::Narrow; + _map[UnicodeRange(68472, 68497)] = CodepointWidth::Narrow; + _map[UnicodeRange(68505, 68508)] = CodepointWidth::Narrow; + _map[UnicodeRange(68521, 68527)] = CodepointWidth::Narrow; + _map[UnicodeRange(68608, 68680)] = CodepointWidth::Narrow; + _map[UnicodeRange(68736, 68786)] = CodepointWidth::Narrow; + _map[UnicodeRange(68800, 68850)] = CodepointWidth::Narrow; + _map[UnicodeRange(68858, 68863)] = CodepointWidth::Narrow; + _map[UnicodeRange(69216, 69246)] = CodepointWidth::Narrow; + _map[UnicodeRange(69632, 69709)] = CodepointWidth::Narrow; + _map[UnicodeRange(69714, 69743)] = CodepointWidth::Narrow; + _map[UnicodeRange(69759, 69825)] = CodepointWidth::Narrow; + _map[UnicodeRange(69840, 69864)] = CodepointWidth::Narrow; + _map[UnicodeRange(69872, 69881)] = CodepointWidth::Narrow; + _map[UnicodeRange(69888, 69940)] = CodepointWidth::Narrow; + _map[UnicodeRange(69942, 69955)] = CodepointWidth::Narrow; + _map[UnicodeRange(69968, 70006)] = CodepointWidth::Narrow; + _map[UnicodeRange(70016, 70093)] = CodepointWidth::Narrow; + _map[UnicodeRange(70096, 70111)] = CodepointWidth::Narrow; + _map[UnicodeRange(70113, 70132)] = CodepointWidth::Narrow; + _map[UnicodeRange(70144, 70161)] = CodepointWidth::Narrow; + _map[UnicodeRange(70163, 70206)] = CodepointWidth::Narrow; + _map[UnicodeRange(70272, 70278)] = CodepointWidth::Narrow; + _map[UnicodeRange(70280, 70280)] = CodepointWidth::Narrow; + _map[UnicodeRange(70282, 70285)] = CodepointWidth::Narrow; + _map[UnicodeRange(70287, 70301)] = CodepointWidth::Narrow; + _map[UnicodeRange(70303, 70313)] = CodepointWidth::Narrow; + _map[UnicodeRange(70320, 70378)] = CodepointWidth::Narrow; + _map[UnicodeRange(70384, 70393)] = CodepointWidth::Narrow; + _map[UnicodeRange(70400, 70403)] = CodepointWidth::Narrow; + _map[UnicodeRange(70405, 70412)] = CodepointWidth::Narrow; + _map[UnicodeRange(70415, 70416)] = CodepointWidth::Narrow; + _map[UnicodeRange(70419, 70440)] = CodepointWidth::Narrow; + _map[UnicodeRange(70442, 70448)] = CodepointWidth::Narrow; + _map[UnicodeRange(70450, 70451)] = CodepointWidth::Narrow; + _map[UnicodeRange(70453, 70457)] = CodepointWidth::Narrow; + _map[UnicodeRange(70460, 70468)] = CodepointWidth::Narrow; + _map[UnicodeRange(70471, 70472)] = CodepointWidth::Narrow; + _map[UnicodeRange(70475, 70477)] = CodepointWidth::Narrow; + _map[UnicodeRange(70480, 70480)] = CodepointWidth::Narrow; + _map[UnicodeRange(70487, 70487)] = CodepointWidth::Narrow; + _map[UnicodeRange(70493, 70499)] = CodepointWidth::Narrow; + _map[UnicodeRange(70502, 70508)] = CodepointWidth::Narrow; + _map[UnicodeRange(70512, 70516)] = CodepointWidth::Narrow; + _map[UnicodeRange(70656, 70745)] = CodepointWidth::Narrow; + _map[UnicodeRange(70747, 70747)] = CodepointWidth::Narrow; + _map[UnicodeRange(70749, 70749)] = CodepointWidth::Narrow; + _map[UnicodeRange(70784, 70855)] = CodepointWidth::Narrow; + _map[UnicodeRange(70864, 70873)] = CodepointWidth::Narrow; + _map[UnicodeRange(71040, 71093)] = CodepointWidth::Narrow; + _map[UnicodeRange(71096, 71133)] = CodepointWidth::Narrow; + _map[UnicodeRange(71168, 71236)] = CodepointWidth::Narrow; + _map[UnicodeRange(71248, 71257)] = CodepointWidth::Narrow; + _map[UnicodeRange(71264, 71276)] = CodepointWidth::Narrow; + _map[UnicodeRange(71296, 71351)] = CodepointWidth::Narrow; + _map[UnicodeRange(71360, 71369)] = CodepointWidth::Narrow; + _map[UnicodeRange(71424, 71449)] = CodepointWidth::Narrow; + _map[UnicodeRange(71453, 71467)] = CodepointWidth::Narrow; + _map[UnicodeRange(71472, 71487)] = CodepointWidth::Narrow; + _map[UnicodeRange(71840, 71922)] = CodepointWidth::Narrow; + _map[UnicodeRange(71935, 71935)] = CodepointWidth::Narrow; + _map[UnicodeRange(72192, 72263)] = CodepointWidth::Narrow; + _map[UnicodeRange(72272, 72323)] = CodepointWidth::Narrow; + _map[UnicodeRange(72326, 72348)] = CodepointWidth::Narrow; + _map[UnicodeRange(72350, 72354)] = CodepointWidth::Narrow; + _map[UnicodeRange(72384, 72440)] = CodepointWidth::Narrow; + _map[UnicodeRange(72704, 72712)] = CodepointWidth::Narrow; + _map[UnicodeRange(72714, 72758)] = CodepointWidth::Narrow; + _map[UnicodeRange(72760, 72773)] = CodepointWidth::Narrow; + _map[UnicodeRange(72784, 72812)] = CodepointWidth::Narrow; + _map[UnicodeRange(72816, 72847)] = CodepointWidth::Narrow; + _map[UnicodeRange(72850, 72871)] = CodepointWidth::Narrow; + _map[UnicodeRange(72873, 72886)] = CodepointWidth::Narrow; + _map[UnicodeRange(72960, 72966)] = CodepointWidth::Narrow; + _map[UnicodeRange(72968, 72969)] = CodepointWidth::Narrow; + _map[UnicodeRange(72971, 73014)] = CodepointWidth::Narrow; + _map[UnicodeRange(73018, 73018)] = CodepointWidth::Narrow; + _map[UnicodeRange(73020, 73021)] = CodepointWidth::Narrow; + _map[UnicodeRange(73023, 73031)] = CodepointWidth::Narrow; + _map[UnicodeRange(73040, 73049)] = CodepointWidth::Narrow; + _map[UnicodeRange(73728, 74649)] = CodepointWidth::Narrow; + _map[UnicodeRange(74752, 74862)] = CodepointWidth::Narrow; + _map[UnicodeRange(74864, 74868)] = CodepointWidth::Narrow; + _map[UnicodeRange(74880, 75075)] = CodepointWidth::Narrow; + _map[UnicodeRange(77824, 78894)] = CodepointWidth::Narrow; + _map[UnicodeRange(82944, 83526)] = CodepointWidth::Narrow; + _map[UnicodeRange(92160, 92728)] = CodepointWidth::Narrow; + _map[UnicodeRange(92736, 92766)] = CodepointWidth::Narrow; + _map[UnicodeRange(92768, 92777)] = CodepointWidth::Narrow; + _map[UnicodeRange(92782, 92783)] = CodepointWidth::Narrow; + _map[UnicodeRange(92880, 92909)] = CodepointWidth::Narrow; + _map[UnicodeRange(92912, 92917)] = CodepointWidth::Narrow; + _map[UnicodeRange(92928, 92997)] = CodepointWidth::Narrow; + _map[UnicodeRange(93008, 93017)] = CodepointWidth::Narrow; + _map[UnicodeRange(93019, 93025)] = CodepointWidth::Narrow; + _map[UnicodeRange(93027, 93047)] = CodepointWidth::Narrow; + _map[UnicodeRange(93053, 93071)] = CodepointWidth::Narrow; + _map[UnicodeRange(93952, 94020)] = CodepointWidth::Narrow; + _map[UnicodeRange(94032, 94078)] = CodepointWidth::Narrow; + _map[UnicodeRange(94095, 94111)] = CodepointWidth::Narrow; + _map[UnicodeRange(94176, 94177)] = CodepointWidth::Wide; + _map[UnicodeRange(94208, 100332)] = CodepointWidth::Wide; + _map[UnicodeRange(100352, 101106)] = CodepointWidth::Wide; + _map[UnicodeRange(110592, 110878)] = CodepointWidth::Wide; + _map[UnicodeRange(110960, 111355)] = CodepointWidth::Wide; + _map[UnicodeRange(113664, 113770)] = CodepointWidth::Narrow; + _map[UnicodeRange(113776, 113788)] = CodepointWidth::Narrow; + _map[UnicodeRange(113792, 113800)] = CodepointWidth::Narrow; + _map[UnicodeRange(113808, 113817)] = CodepointWidth::Narrow; + _map[UnicodeRange(113820, 113827)] = CodepointWidth::Narrow; + _map[UnicodeRange(118784, 119029)] = CodepointWidth::Narrow; + _map[UnicodeRange(119040, 119078)] = CodepointWidth::Narrow; + _map[UnicodeRange(119081, 119272)] = CodepointWidth::Narrow; + _map[UnicodeRange(119296, 119365)] = CodepointWidth::Narrow; + _map[UnicodeRange(119552, 119638)] = CodepointWidth::Narrow; + _map[UnicodeRange(119648, 119665)] = CodepointWidth::Narrow; + _map[UnicodeRange(119808, 119892)] = CodepointWidth::Narrow; + _map[UnicodeRange(119894, 119964)] = CodepointWidth::Narrow; + _map[UnicodeRange(119966, 119967)] = CodepointWidth::Narrow; + _map[UnicodeRange(119970, 119970)] = CodepointWidth::Narrow; + _map[UnicodeRange(119973, 119974)] = CodepointWidth::Narrow; + _map[UnicodeRange(119977, 119980)] = CodepointWidth::Narrow; + _map[UnicodeRange(119982, 119993)] = CodepointWidth::Narrow; + _map[UnicodeRange(119995, 119995)] = CodepointWidth::Narrow; + _map[UnicodeRange(119997, 120003)] = CodepointWidth::Narrow; + _map[UnicodeRange(120005, 120069)] = CodepointWidth::Narrow; + _map[UnicodeRange(120071, 120074)] = CodepointWidth::Narrow; + _map[UnicodeRange(120077, 120084)] = CodepointWidth::Narrow; + _map[UnicodeRange(120086, 120092)] = CodepointWidth::Narrow; + _map[UnicodeRange(120094, 120121)] = CodepointWidth::Narrow; + _map[UnicodeRange(120123, 120126)] = CodepointWidth::Narrow; + _map[UnicodeRange(120128, 120132)] = CodepointWidth::Narrow; + _map[UnicodeRange(120134, 120134)] = CodepointWidth::Narrow; + _map[UnicodeRange(120138, 120144)] = CodepointWidth::Narrow; + _map[UnicodeRange(120146, 120485)] = CodepointWidth::Narrow; + _map[UnicodeRange(120488, 120779)] = CodepointWidth::Narrow; + _map[UnicodeRange(120782, 121483)] = CodepointWidth::Narrow; + _map[UnicodeRange(121499, 121503)] = CodepointWidth::Narrow; + _map[UnicodeRange(121505, 121519)] = CodepointWidth::Narrow; + _map[UnicodeRange(122880, 122886)] = CodepointWidth::Narrow; + _map[UnicodeRange(122888, 122904)] = CodepointWidth::Narrow; + _map[UnicodeRange(122907, 122913)] = CodepointWidth::Narrow; + _map[UnicodeRange(122915, 122916)] = CodepointWidth::Narrow; + _map[UnicodeRange(122918, 122922)] = CodepointWidth::Narrow; + _map[UnicodeRange(124928, 125124)] = CodepointWidth::Narrow; + _map[UnicodeRange(125127, 125142)] = CodepointWidth::Narrow; + _map[UnicodeRange(125184, 125258)] = CodepointWidth::Narrow; + _map[UnicodeRange(125264, 125273)] = CodepointWidth::Narrow; + _map[UnicodeRange(125278, 125279)] = CodepointWidth::Narrow; + _map[UnicodeRange(126464, 126467)] = CodepointWidth::Narrow; + _map[UnicodeRange(126469, 126495)] = CodepointWidth::Narrow; + _map[UnicodeRange(126497, 126498)] = CodepointWidth::Narrow; + _map[UnicodeRange(126500, 126500)] = CodepointWidth::Narrow; + _map[UnicodeRange(126503, 126503)] = CodepointWidth::Narrow; + _map[UnicodeRange(126505, 126514)] = CodepointWidth::Narrow; + _map[UnicodeRange(126516, 126519)] = CodepointWidth::Narrow; + _map[UnicodeRange(126521, 126521)] = CodepointWidth::Narrow; + _map[UnicodeRange(126523, 126523)] = CodepointWidth::Narrow; + _map[UnicodeRange(126530, 126530)] = CodepointWidth::Narrow; + _map[UnicodeRange(126535, 126535)] = CodepointWidth::Narrow; + _map[UnicodeRange(126537, 126537)] = CodepointWidth::Narrow; + _map[UnicodeRange(126539, 126539)] = CodepointWidth::Narrow; + _map[UnicodeRange(126541, 126543)] = CodepointWidth::Narrow; + _map[UnicodeRange(126545, 126546)] = CodepointWidth::Narrow; + _map[UnicodeRange(126548, 126548)] = CodepointWidth::Narrow; + _map[UnicodeRange(126551, 126551)] = CodepointWidth::Narrow; + _map[UnicodeRange(126553, 126553)] = CodepointWidth::Narrow; + _map[UnicodeRange(126555, 126555)] = CodepointWidth::Narrow; + _map[UnicodeRange(126557, 126557)] = CodepointWidth::Narrow; + _map[UnicodeRange(126559, 126559)] = CodepointWidth::Narrow; + _map[UnicodeRange(126561, 126562)] = CodepointWidth::Narrow; + _map[UnicodeRange(126564, 126564)] = CodepointWidth::Narrow; + _map[UnicodeRange(126567, 126570)] = CodepointWidth::Narrow; + _map[UnicodeRange(126572, 126578)] = CodepointWidth::Narrow; + _map[UnicodeRange(126580, 126583)] = CodepointWidth::Narrow; + _map[UnicodeRange(126585, 126588)] = CodepointWidth::Narrow; + _map[UnicodeRange(126590, 126590)] = CodepointWidth::Narrow; + _map[UnicodeRange(126592, 126601)] = CodepointWidth::Narrow; + _map[UnicodeRange(126603, 126619)] = CodepointWidth::Narrow; + _map[UnicodeRange(126625, 126627)] = CodepointWidth::Narrow; + _map[UnicodeRange(126629, 126633)] = CodepointWidth::Narrow; + _map[UnicodeRange(126635, 126651)] = CodepointWidth::Narrow; + _map[UnicodeRange(126704, 126705)] = CodepointWidth::Narrow; + _map[UnicodeRange(126976, 126979)] = CodepointWidth::Narrow; + _map[UnicodeRange(126980, 126980)] = CodepointWidth::Wide; + _map[UnicodeRange(126981, 127019)] = CodepointWidth::Narrow; + _map[UnicodeRange(127024, 127123)] = CodepointWidth::Narrow; + _map[UnicodeRange(127136, 127150)] = CodepointWidth::Narrow; + _map[UnicodeRange(127153, 127167)] = CodepointWidth::Narrow; + _map[UnicodeRange(127169, 127182)] = CodepointWidth::Narrow; + _map[UnicodeRange(127183, 127183)] = CodepointWidth::Wide; + _map[UnicodeRange(127185, 127221)] = CodepointWidth::Narrow; + _map[UnicodeRange(127232, 127242)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127243, 127244)] = CodepointWidth::Narrow; + _map[UnicodeRange(127248, 127277)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127278, 127278)] = CodepointWidth::Narrow; + _map[UnicodeRange(127280, 127337)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127338, 127339)] = CodepointWidth::Narrow; + _map[UnicodeRange(127344, 127373)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127374, 127374)] = CodepointWidth::Wide; + _map[UnicodeRange(127375, 127376)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127377, 127386)] = CodepointWidth::Wide; + _map[UnicodeRange(127387, 127404)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(127462, 127487)] = CodepointWidth::Narrow; + _map[UnicodeRange(127488, 127490)] = CodepointWidth::Wide; + _map[UnicodeRange(127504, 127547)] = CodepointWidth::Wide; + _map[UnicodeRange(127552, 127560)] = CodepointWidth::Wide; + _map[UnicodeRange(127568, 127569)] = CodepointWidth::Wide; + _map[UnicodeRange(127584, 127589)] = CodepointWidth::Wide; + _map[UnicodeRange(127744, 127776)] = CodepointWidth::Wide; + _map[UnicodeRange(127777, 127788)] = CodepointWidth::Narrow; + _map[UnicodeRange(127789, 127797)] = CodepointWidth::Wide; + _map[UnicodeRange(127798, 127798)] = CodepointWidth::Narrow; + _map[UnicodeRange(127799, 127868)] = CodepointWidth::Wide; + _map[UnicodeRange(127869, 127869)] = CodepointWidth::Narrow; + _map[UnicodeRange(127870, 127891)] = CodepointWidth::Wide; + _map[UnicodeRange(127892, 127903)] = CodepointWidth::Narrow; + _map[UnicodeRange(127904, 127946)] = CodepointWidth::Wide; + _map[UnicodeRange(127947, 127950)] = CodepointWidth::Narrow; + _map[UnicodeRange(127951, 127955)] = CodepointWidth::Wide; + _map[UnicodeRange(127956, 127967)] = CodepointWidth::Narrow; + _map[UnicodeRange(127968, 127984)] = CodepointWidth::Wide; + _map[UnicodeRange(127985, 127987)] = CodepointWidth::Narrow; + _map[UnicodeRange(127988, 127988)] = CodepointWidth::Wide; + _map[UnicodeRange(127989, 127991)] = CodepointWidth::Narrow; + _map[UnicodeRange(127992, 128062)] = CodepointWidth::Wide; + _map[UnicodeRange(128063, 128063)] = CodepointWidth::Narrow; + _map[UnicodeRange(128064, 128064)] = CodepointWidth::Wide; + _map[UnicodeRange(128065, 128065)] = CodepointWidth::Narrow; + _map[UnicodeRange(128066, 128252)] = CodepointWidth::Wide; + _map[UnicodeRange(128253, 128254)] = CodepointWidth::Narrow; + _map[UnicodeRange(128255, 128317)] = CodepointWidth::Wide; + _map[UnicodeRange(128318, 128330)] = CodepointWidth::Narrow; + _map[UnicodeRange(128331, 128334)] = CodepointWidth::Wide; + _map[UnicodeRange(128335, 128335)] = CodepointWidth::Narrow; + _map[UnicodeRange(128336, 128359)] = CodepointWidth::Wide; + _map[UnicodeRange(128360, 128377)] = CodepointWidth::Narrow; + _map[UnicodeRange(128378, 128378)] = CodepointWidth::Wide; + _map[UnicodeRange(128379, 128404)] = CodepointWidth::Narrow; + _map[UnicodeRange(128405, 128406)] = CodepointWidth::Wide; + _map[UnicodeRange(128407, 128419)] = CodepointWidth::Narrow; + _map[UnicodeRange(128420, 128420)] = CodepointWidth::Wide; + _map[UnicodeRange(128421, 128506)] = CodepointWidth::Narrow; + _map[UnicodeRange(128507, 128591)] = CodepointWidth::Wide; + _map[UnicodeRange(128592, 128639)] = CodepointWidth::Narrow; + _map[UnicodeRange(128640, 128709)] = CodepointWidth::Wide; + _map[UnicodeRange(128710, 128715)] = CodepointWidth::Narrow; + _map[UnicodeRange(128716, 128716)] = CodepointWidth::Wide; + _map[UnicodeRange(128717, 128719)] = CodepointWidth::Narrow; + _map[UnicodeRange(128720, 128722)] = CodepointWidth::Wide; + _map[UnicodeRange(128723, 128724)] = CodepointWidth::Narrow; + _map[UnicodeRange(128736, 128746)] = CodepointWidth::Narrow; + _map[UnicodeRange(128747, 128748)] = CodepointWidth::Wide; + _map[UnicodeRange(128752, 128755)] = CodepointWidth::Narrow; + _map[UnicodeRange(128756, 128760)] = CodepointWidth::Wide; + _map[UnicodeRange(128768, 128883)] = CodepointWidth::Narrow; + _map[UnicodeRange(128896, 128980)] = CodepointWidth::Narrow; + _map[UnicodeRange(129024, 129035)] = CodepointWidth::Narrow; + _map[UnicodeRange(129040, 129095)] = CodepointWidth::Narrow; + _map[UnicodeRange(129104, 129113)] = CodepointWidth::Narrow; + _map[UnicodeRange(129120, 129159)] = CodepointWidth::Narrow; + _map[UnicodeRange(129168, 129197)] = CodepointWidth::Narrow; + _map[UnicodeRange(129280, 129291)] = CodepointWidth::Narrow; + _map[UnicodeRange(129296, 129342)] = CodepointWidth::Wide; + _map[UnicodeRange(129344, 129356)] = CodepointWidth::Wide; + _map[UnicodeRange(129360, 129387)] = CodepointWidth::Wide; + _map[UnicodeRange(129408, 129431)] = CodepointWidth::Wide; + _map[UnicodeRange(129472, 129472)] = CodepointWidth::Wide; + _map[UnicodeRange(129488, 129510)] = CodepointWidth::Wide; + _map[UnicodeRange(131072, 196605)] = CodepointWidth::Wide; + _map[UnicodeRange(196608, 262141)] = CodepointWidth::Wide; + _map[UnicodeRange(917505, 917505)] = CodepointWidth::Narrow; + _map[UnicodeRange(917536, 917631)] = CodepointWidth::Narrow; + _map[UnicodeRange(917760, 917999)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(983040, 1048573)] = CodepointWidth::Ambiguous; + _map[UnicodeRange(1048576, 1114109)] = CodepointWidth::Ambiguous; +} diff --git a/src/types/FocusEvent.cpp b/src/types/FocusEvent.cpp new file mode 100644 index 000000000..d850fff50 --- /dev/null +++ b/src/types/FocusEvent.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +FocusEvent::~FocusEvent() +{ +} + +INPUT_RECORD FocusEvent::ToInputRecord() const noexcept +{ + INPUT_RECORD record{ 0 }; + record.EventType = FOCUS_EVENT; + record.Event.FocusEvent.bSetFocus = !!_focus; + return record; +} + +InputEventType FocusEvent::EventType() const noexcept +{ + return InputEventType::FocusEvent; +} + +void FocusEvent::SetFocus(const bool focus) noexcept +{ + _focus = focus; +} diff --git a/src/types/GlyphWidth.cpp b/src/types/GlyphWidth.cpp new file mode 100644 index 000000000..68f87957e --- /dev/null +++ b/src/types/GlyphWidth.cpp @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/CodepointWidthDetector.hpp" +#include "inc/GlyphWidth.hpp" + +static CodepointWidthDetector widthDetector; + +// Function Description: +// - determines if the glyph represented by the string of characters should be +// wide or not. See CodepointWidthDetector::IsWide +bool IsGlyphFullWidth(const std::wstring_view glyph) +{ + return widthDetector.IsWide(glyph); +} + +// Function Description: +// - determines if the glyph represented by the single character should be +// wide or not. See CodepointWidthDetector::IsWide +bool IsGlyphFullWidth(const wchar_t wch) +{ + return widthDetector.IsWide(wch); +} + +// Function Description: +// - Sets a function that should be used by the global CodepointWidthDetector +// as the fallback mechanism for determining a particular glyph's width, +// should the glyph be an ambiguous width. +// A Terminal could hook in a Renderer's IsGlyphWideByFont method as the +// fallback to ask the renderer for the glyph's width (for example). +// Arguments: +// - pfnFallback - the function to use as the fallback method. +// Return Value: +// - +void SetGlyphWidthFallback(std::function pfnFallback) +{ + widthDetector.SetFallbackMethod(pfnFallback); +} diff --git a/src/types/IInputEvent.cpp b/src/types/IInputEvent.cpp new file mode 100644 index 000000000..93c6795f4 --- /dev/null +++ b/src/types/IInputEvent.cpp @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +#include + +std::unique_ptr IInputEvent::Create(const INPUT_RECORD& record) +{ + switch (record.EventType) + { + case KEY_EVENT: + return std::make_unique(record.Event.KeyEvent); + case MOUSE_EVENT: + return std::make_unique(record.Event.MouseEvent); + case WINDOW_BUFFER_SIZE_EVENT: + return std::make_unique(record.Event.WindowBufferSizeEvent); + case MENU_EVENT: + return std::make_unique(record.Event.MenuEvent); + case FOCUS_EVENT: + return std::make_unique(record.Event.FocusEvent); + default: + THROW_HR(E_INVALIDARG); + } +} + +std::deque> IInputEvent::Create(gsl::span records) +{ + std::deque> outEvents; + + for (auto& record : records) + { + outEvents.push_back(Create(record)); + } + + return outEvents; +} + + +// Routine Description: +// - Converts std::deque to std::deque> +// Arguments: +// - inRecords - records to convert +// Return Value: +// - std::deque of IInputEvents on success. Will throw exception on failure. +std::deque> IInputEvent::Create(const std::deque& records) +{ + std::deque> outEvents; + for (size_t i = 0; i < records.size(); ++i) + { + std::unique_ptr event = IInputEvent::Create(records[i]); + outEvents.push_back(std::move(event)); + } + return outEvents; +} + +std::vector IInputEvent::ToInputRecords(const std::deque>& events) +{ + std::vector records; + records.reserve(events.size()); + + for (auto& evt : events) + { + records.push_back(evt->ToInputRecord()); + } + + return records; +} diff --git a/src/types/IInputEventStreams.cpp b/src/types/IInputEventStreams.cpp new file mode 100644 index 000000000..87020fd9f --- /dev/null +++ b/src/types/IInputEventStreams.cpp @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" +#include + +std::wostream& operator<<(std::wostream& stream, const IInputEvent* const pEvent) +{ + if (pEvent == nullptr) + { + return stream << L"nullptr"; + } + + try + { + switch (pEvent->EventType()) + { + case InputEventType::KeyEvent: + return stream << static_cast(pEvent); + case InputEventType::MouseEvent: + return stream << static_cast(pEvent); + case InputEventType::WindowBufferSizeEvent: + return stream << static_cast(pEvent); + case InputEventType::MenuEvent: + return stream << static_cast(pEvent); + case InputEventType::FocusEvent: + return stream << static_cast(pEvent); + default: + return stream << L"IInputEvent()"; + } + } + catch (...) + { + return stream << L"IInputEvent stream error"; + } +} + +std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent) +{ + if (pKeyEvent == nullptr) + { + return stream << L"nullptr"; + } + + std::wstring keyMotion = pKeyEvent->_keyDown ? L"keyDown" : L"keyUp"; + std::wstring charData = { pKeyEvent->_charData }; + if (pKeyEvent->_charData == L'\0') + { + charData = L"null"; + } + + return stream << L"KeyEvent(" << + keyMotion << L", " << + L"repeat: " << pKeyEvent->_repeatCount << L", " << + L"keyCode: " << pKeyEvent->_virtualKeyCode << L", " << + L"scanCode: " << pKeyEvent->_virtualScanCode << L", " << + L"char: " << charData << L", " << + L"mods: " << pKeyEvent->_activeModifierKeys << L")"; +} + +std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent) +{ + if (pMouseEvent == nullptr) + { + return stream << L"nullptr"; + } + + return stream << L"MouseEvent(" << + L"X: " << pMouseEvent->_position.X << L", " << + L"Y: " << pMouseEvent->_position.Y << L", " << + L"buttons: " << pMouseEvent->_buttonState << L", " << + L"mods: " << pMouseEvent->_activeModifierKeys << L", " << + L"events: " << pMouseEvent->_eventFlags << L")"; +} + +std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent) +{ + if (pEvent == nullptr) + { + return stream << L"nullptr"; + } + + return stream << L"WindowbufferSizeEvent(" << + L"X: " << pEvent->_size.X << L", " << + L"Y: " << pEvent->_size.Y << L")"; +} + +std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent) +{ + if (pMenuEvent == nullptr) + { + return stream << L"nullptr"; + } + + return stream << L"MenuEvent(" << + L"CommandId" << pMenuEvent->_commandId << L")"; +} + +std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent) +{ + if (pFocusEvent == nullptr) + { + return stream << L"nullptr"; + } + + return stream << L"FocusEvent(" << + L"focus" << pFocusEvent->_focus << L")"; +} diff --git a/src/types/KeyEvent.cpp b/src/types/KeyEvent.cpp new file mode 100644 index 000000000..18e48e6d6 --- /dev/null +++ b/src/types/KeyEvent.cpp @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +KeyEvent::~KeyEvent() +{ +} + +INPUT_RECORD KeyEvent::ToInputRecord() const noexcept +{ + INPUT_RECORD record{ 0 }; + record.EventType = KEY_EVENT; + record.Event.KeyEvent.bKeyDown = !!_keyDown; + record.Event.KeyEvent.wRepeatCount = _repeatCount; + record.Event.KeyEvent.wVirtualKeyCode = _virtualKeyCode; + record.Event.KeyEvent.wVirtualScanCode = _virtualScanCode; + record.Event.KeyEvent.uChar.UnicodeChar = _charData; + record.Event.KeyEvent.dwControlKeyState = _activeModifierKeys; + return record; +} + +InputEventType KeyEvent::EventType() const noexcept +{ + return InputEventType::KeyEvent; +} + +void KeyEvent::SetKeyDown(const bool keyDown) noexcept +{ + _keyDown = keyDown; +} + + +void KeyEvent::SetRepeatCount(const WORD repeatCount) noexcept +{ + _repeatCount = repeatCount; +} + +void KeyEvent::SetVirtualKeyCode(const WORD virtualKeyCode) noexcept +{ + _virtualKeyCode = virtualKeyCode; +} + +void KeyEvent::SetVirtualScanCode(const WORD virtualScanCode) noexcept +{ + _virtualScanCode = virtualScanCode; +} + +void KeyEvent::SetCharData(const wchar_t character) noexcept +{ + _charData = character; +} + +void KeyEvent::SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept +{ + _activeModifierKeys = activeModifierKeys; +} + +void KeyEvent::DeactivateModifierKey(const ModifierKeyState modifierKey) noexcept +{ + DWORD const bitFlag = ToConsoleControlKeyFlag(modifierKey); + WI_ClearAllFlags(_activeModifierKeys, bitFlag); +} + +void KeyEvent::ActivateModifierKey(const ModifierKeyState modifierKey) noexcept +{ + DWORD const bitFlag = ToConsoleControlKeyFlag(modifierKey); + WI_SetAllFlags(_activeModifierKeys, bitFlag); +} + +bool KeyEvent::DoActiveModifierKeysMatch(const std::unordered_set& consoleModifiers) const noexcept +{ + DWORD consoleBits = 0; + for (const ModifierKeyState& mod : consoleModifiers) + { + WI_SetAllFlags(consoleBits, ToConsoleControlKeyFlag(mod)); + } + return consoleBits == _activeModifierKeys; +} + +// Routine Description: +// - checks if this key event is a special key for line editing +// Arguments: +// - none +// Return Value: +// - true if this key has special relevance to line editing, false otherwise +bool KeyEvent::IsCommandLineEditingKey() const noexcept +{ + if (!IsAltPressed() && !IsCtrlPressed()) + { + switch (GetVirtualKeyCode()) + { + case VK_ESCAPE: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_INSERT: + case VK_DELETE: + case VK_F1: + case VK_F2: + case VK_F3: + case VK_F4: + case VK_F5: + case VK_F6: + case VK_F7: + case VK_F8: + case VK_F9: + return true; + default: + break; + } + } + if (IsCtrlPressed()) + { + switch (GetVirtualKeyCode()) + { + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_RIGHT: + return true; + default: + break; + } + } + + if (IsAltPressed()) + { + switch (GetVirtualKeyCode()) + { + case VK_F7: + case VK_F10: + return true; + default: + break; + } + } + return false; +} + +// Routine Description: +// - checks if this key event is a special key for popups +// Arguments: +// - None +// Return Value: +// - true if this key has special relevance to popups, false otherwise +bool KeyEvent::IsPopupKey() const noexcept +{ + if (!IsAltPressed() && !IsCtrlPressed()) + { + switch (GetVirtualKeyCode()) + { + case VK_ESCAPE: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_F2: + case VK_F4: + case VK_F7: + case VK_F9: + case VK_DELETE: + return true; + default: + break; + } + } + + return false; +} diff --git a/src/types/MenuEvent.cpp b/src/types/MenuEvent.cpp new file mode 100644 index 000000000..55705118e --- /dev/null +++ b/src/types/MenuEvent.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +MenuEvent::~MenuEvent() +{ +} + +INPUT_RECORD MenuEvent::ToInputRecord() const noexcept +{ + INPUT_RECORD record{ 0 }; + record.EventType = MENU_EVENT; + record.Event.MenuEvent.dwCommandId = _commandId; + return record; +} + +InputEventType MenuEvent::EventType() const noexcept +{ + return InputEventType::MenuEvent; +} + +void MenuEvent::SetCommandId(const UINT commandId) noexcept +{ + _commandId = commandId; +} diff --git a/src/types/ModifierKeyState.cpp b/src/types/ModifierKeyState.cpp new file mode 100644 index 000000000..8f0f5bfbd --- /dev/null +++ b/src/types/ModifierKeyState.cpp @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +#include + +// Routine Description: +// - checks if flag is present in flags +// Arguments: +// - flags - bit battern to check for flag +// - flag - bit pattern to search for +// Return Value: +// - true if flag is present in flags +// Note: +// - The wil version of IsFlagSet is only to operate on values that +// are compile time constants. This will work with runtime calculated +// values. +constexpr bool RuntimeIsFlagSet(const DWORD flags, const DWORD flag) noexcept +{ + return !!(flags & flag); +} + +std::unordered_set FromVkKeyScan(const short vkKeyScanFlags) +{ + std::unordered_set keyState; + + switch (vkKeyScanFlags) + { + case VkKeyScanModState::None: + break; + case VkKeyScanModState::ShiftPressed: + keyState.insert(ModifierKeyState::Shift); + break; + case VkKeyScanModState::CtrlPressed: + keyState.insert(ModifierKeyState::LeftCtrl); + keyState.insert(ModifierKeyState::RightCtrl); + break; + case VkKeyScanModState::ShiftAndCtrlPressed: + keyState.insert(ModifierKeyState::Shift); + keyState.insert(ModifierKeyState::LeftCtrl); + keyState.insert(ModifierKeyState::RightCtrl); + break; + case VkKeyScanModState::AltPressed: + keyState.insert(ModifierKeyState::LeftAlt); + keyState.insert(ModifierKeyState::RightAlt); + break; + case VkKeyScanModState::ShiftAndAltPressed: + keyState.insert(ModifierKeyState::Shift); + keyState.insert(ModifierKeyState::LeftAlt); + keyState.insert(ModifierKeyState::RightAlt); + break; + case VkKeyScanModState::CtrlAndAltPressed: + keyState.insert(ModifierKeyState::LeftCtrl); + keyState.insert(ModifierKeyState::RightCtrl); + keyState.insert(ModifierKeyState::LeftAlt); + keyState.insert(ModifierKeyState::RightAlt); + break; + case VkKeyScanModState::ModPressed: + keyState.insert(ModifierKeyState::Shift); + keyState.insert(ModifierKeyState::LeftCtrl); + keyState.insert(ModifierKeyState::RightCtrl); + keyState.insert(ModifierKeyState::LeftAlt); + keyState.insert(ModifierKeyState::RightAlt); + break; + default: + THROW_HR(E_INVALIDARG); + break; + } + + return keyState; +} + +using ModifierKeyStateMapping = std::pair; + +constexpr static ModifierKeyStateMapping ModifierKeyStateTranslationTable[] = +{ + { ModifierKeyState::RightAlt, RIGHT_ALT_PRESSED }, + { ModifierKeyState::LeftAlt, LEFT_ALT_PRESSED }, + { ModifierKeyState::RightCtrl, RIGHT_CTRL_PRESSED }, + { ModifierKeyState::LeftCtrl, LEFT_CTRL_PRESSED }, + { ModifierKeyState::Shift, SHIFT_PRESSED }, + { ModifierKeyState::NumLock, NUMLOCK_ON }, + { ModifierKeyState::ScrollLock, SCROLLLOCK_ON }, + { ModifierKeyState::CapsLock, CAPSLOCK_ON }, + { ModifierKeyState::EnhancedKey, ENHANCED_KEY }, + { ModifierKeyState::NlsDbcsChar, NLS_DBCSCHAR }, + { ModifierKeyState::NlsAlphanumeric, NLS_ALPHANUMERIC }, + { ModifierKeyState::NlsKatakana, NLS_KATAKANA }, + { ModifierKeyState::NlsHiragana, NLS_HIRAGANA }, + { ModifierKeyState::NlsRoman, NLS_ROMAN }, + { ModifierKeyState::NlsImeConversion, NLS_IME_CONVERSION }, + { ModifierKeyState::AltNumpad, ALTNUMPAD_BIT }, + { ModifierKeyState::NlsImeDisable, NLS_IME_DISABLE } +}; + +static_assert(size(ModifierKeyStateTranslationTable) == static_cast(ModifierKeyState::ENUM_COUNT), + "ModifierKeyStateTranslationTable must have a valid mapping for each modifier value"); + +// Routine Description: +// - Expands legacy control keys bitsets into a stl set +// Arguments: +// - flags - legacy bitset to expand +// Return Value: +// - set of ModifierKeyState values that represent flags +std::unordered_set FromConsoleControlKeyFlags(const DWORD flags) +{ + std::unordered_set keyStates; + + for (const ModifierKeyStateMapping& mapping : ModifierKeyStateTranslationTable) + { + if (RuntimeIsFlagSet(flags, mapping.second)) + { + keyStates.insert(mapping.first); + } + } + + return keyStates; +} + +// Routine Description: +// - Converts ModifierKeyState back to the bizarre console bitflag associated with it. +// Arguments: +// - modifierKey - modifier to convert +// Return Value: +// - console bitflag associated with modifierKey +DWORD ToConsoleControlKeyFlag(const ModifierKeyState modifierKey) noexcept +{ + for (const ModifierKeyStateMapping& mapping : ModifierKeyStateTranslationTable) + { + if (mapping.first == modifierKey) + { + return mapping.second; + } + } + return 0; +} diff --git a/src/types/MouseEvent.cpp b/src/types/MouseEvent.cpp new file mode 100644 index 000000000..d763111a9 --- /dev/null +++ b/src/types/MouseEvent.cpp @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +MouseEvent::~MouseEvent() +{ +} + +INPUT_RECORD MouseEvent::ToInputRecord() const noexcept +{ + INPUT_RECORD record{ 0 }; + record.EventType = MOUSE_EVENT; + record.Event.MouseEvent.dwMousePosition = _position; + record.Event.MouseEvent.dwButtonState = _buttonState; + record.Event.MouseEvent.dwControlKeyState = _activeModifierKeys; + record.Event.MouseEvent.dwEventFlags = _eventFlags; + return record; +} + +InputEventType MouseEvent::EventType() const noexcept +{ + return InputEventType::MouseEvent; +} + +void MouseEvent::SetPosition(const COORD position) noexcept +{ + _position = position; +} + +void MouseEvent::SetButtonState(const DWORD buttonState) noexcept +{ + _buttonState = buttonState; +} + +void MouseEvent::SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept +{ + _activeModifierKeys = activeModifierKeys; +} +void MouseEvent::SetEventFlags(const DWORD eventFlags) noexcept +{ + _eventFlags = eventFlags; +} diff --git a/src/types/Utf16Parser.cpp b/src/types/Utf16Parser.cpp new file mode 100644 index 000000000..7eac6d478 --- /dev/null +++ b/src/types/Utf16Parser.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "inc/Utf16Parser.hpp" + +// Routine Description: +// - Finds the next single collection for the codepoint out of the given UTF-16 string information. +// - In simpler terms, it will group UTF-16 surrogate pairs into a single unit or give you a valid single-item UTF-16 character. +// - Does not validate UTF-16 input beyond proper leading/trailing character sequences. +// Arguments: +// - wstr - The UTF-16 string to parse. +// Return Value: +// - A view into the string given of just the next codepoint unit. +std::wstring_view Utf16Parser::ParseNext(std::wstring_view wstr) +{ + size_t pos = 0; + size_t length = 0; + + for (auto wch : wstr) + { + if (IsLeadingSurrogate(wch)) + { + length++; + } + else if (IsTrailingSurrogate(wch)) + { + if (length != 0) + { + length++; + break; + } + else + { + pos++; + } + } + else + { + length++; + break; + } + } + + return wstr.substr(pos, length); +} + +// Routine Description: +// - formats a utf16 encoded wstring and splits the codepoints into individual collections. +// - will drop badly formatted leading/trailing char sequences. +// - does not validate utf16 input beyond proper leading/trailing char sequences. +// Arguments: +// - wstr - the string to parse +// Return Value: +// - a vector of utf16 codepoints. glyphs that require surrogate pairs will be grouped +// together in a vector and codepoints that use only one wchar will be in a vector by themselves. +std::vector> Utf16Parser::Parse(std::wstring_view wstr) +{ + std::vector> result; + std::vector sequence; + for (const auto wch : wstr) + { + if (IsLeadingSurrogate(wch)) + { + sequence.clear(); + sequence.push_back(wch); + } + else if (IsTrailingSurrogate(wch)) + { + if (!sequence.empty()) + { + sequence.push_back(wch); + result.push_back(sequence); + sequence.clear(); + } + } + else + { + result.push_back({ wch }); + } + } + return result; +} diff --git a/src/types/WindowBufferSizeEvent.cpp b/src/types/WindowBufferSizeEvent.cpp new file mode 100644 index 000000000..550a14356 --- /dev/null +++ b/src/types/WindowBufferSizeEvent.cpp @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/IInputEvent.hpp" + +WindowBufferSizeEvent::~WindowBufferSizeEvent() +{ +} + +INPUT_RECORD WindowBufferSizeEvent::ToInputRecord() const noexcept +{ + INPUT_RECORD record{ 0 }; + record.EventType = WINDOW_BUFFER_SIZE_EVENT; + record.Event.WindowBufferSizeEvent.dwSize = _size; + return record; +} + +InputEventType WindowBufferSizeEvent::EventType() const noexcept +{ + return InputEventType::WindowBufferSizeEvent; +} + +void WindowBufferSizeEvent::SetSize(const COORD size) noexcept +{ + _size = size; +} diff --git a/src/types/convert.cpp b/src/types/convert.cpp new file mode 100644 index 000000000..6ec1ae6c6 --- /dev/null +++ b/src/types/convert.cpp @@ -0,0 +1,525 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/convert.hpp" + +#include "../inc/unicode.hpp" + +#ifdef BUILD_ONECORE_INTERACTIVITY +#include "../../interactivity/inc/VtApiRedirection.hpp" +#endif + +#pragma hdrstop + +// TODO: MSFT 14150722 - can these const values be generated at +// runtime without breaking compatibility? +static const WORD altScanCode = 0x38; +static const WORD leftShiftScanCode = 0x2A; + +// Routine Description: +// - Takes a multibyte string, allocates the appropriate amount of memory for the conversion, performs the conversion, +// and returns the Unicode UTF-16 result in the smart pointer (and the length). +// Arguments: +// - codepage - Windows Code Page representing the multibyte source text +// - source - View of multibyte characters of source text +// Return Value: +// - The UTF-16 wide string. +// - NOTE: Throws suitable HRESULT errors from memory allocation, safe math, or MultiByteToWideChar failures. +[[nodiscard]] +std::wstring ConvertToW(const UINT codePage, const std::string_view source) +{ + // If there's nothing to convert, bail early. + if (source.empty()) + { + return {}; + } + + int iSource; // convert to int because Mb2Wc requires it. + THROW_IF_FAILED(SizeTToInt(source.size(), &iSource)); + + // Ask how much space we will need. + int const iTarget = MultiByteToWideChar(codePage, 0, source.data(), iSource, nullptr, 0); + THROW_LAST_ERROR_IF(0 == iTarget); + + size_t cchNeeded; + THROW_IF_FAILED(IntToSizeT(iTarget, &cchNeeded)); + + // Allocate ourselves space in a smart pointer. + std::unique_ptr pwsOut = std::make_unique(cchNeeded); + THROW_IF_NULL_ALLOC(pwsOut); + + // Attempt conversion for real. + THROW_LAST_ERROR_IF(0 == MultiByteToWideChar(codePage, 0, source.data(), iSource, pwsOut.get(), iTarget)); + + // Return as a string + return std::wstring(pwsOut.get(), cchNeeded); +} + +// Routine Description: +// - Takes a wide string, allocates the appropriate amount of memory for the conversion, performs the conversion, +// and returns the Multibyte result +// Arguments: +// - codepage - Windows Code Page representing the multibyte destination text +// - source - Unicode (UTF-16) characters of source text +// Return Value: +// - The multibyte string encoded in the given codepage +// - NOTE: Throws suitable HRESULT errors from memory allocation, safe math, or MultiByteToWideChar failures. +[[nodiscard]] +std::string ConvertToA(const UINT codepage, const std::wstring_view source) +{ + // If there's nothing to convert, bail early. + if (source.empty()) + { + return {}; + } + + int iSource; // convert to int because Wc2Mb requires it. + THROW_IF_FAILED(SizeTToInt(source.size(), &iSource)); + + // Ask how much space we will need. +#pragma prefast(suppress:__WARNING_W2A_BEST_FIT, "WC_NO_BEST_FIT_CHARS doesn't work in many codepages. Retain old behavior.") + int const iTarget = WideCharToMultiByte(codepage, 0, source.data(), iSource, nullptr, 0, nullptr, nullptr); + THROW_LAST_ERROR_IF(0 == iTarget); + + size_t cchNeeded; + THROW_IF_FAILED(IntToSizeT(iTarget, &cchNeeded)); + + // Allocate ourselves space in a smart pointer + std::unique_ptr psOut = std::make_unique(cchNeeded); + THROW_IF_NULL_ALLOC(psOut.get()); + + // Attempt conversion for real. +#pragma prefast(suppress:__WARNING_W2A_BEST_FIT, "WC_NO_BEST_FIT_CHARS doesn't work in many codepages. Retain old behavior.") + THROW_LAST_ERROR_IF(0 == WideCharToMultiByte(codepage, 0, source.data(), iSource, psOut.get(), iTarget, nullptr, nullptr)); + + // Return as a string + return std::string(psOut.get(), cchNeeded); +} + +// Routine Description: +// - Takes a wide string, and determines how many bytes it would take to store it with the given Multibyte codepage. +// Arguments: +// - codepage - Windows Code Page representing the multibyte destination text +// - source - Array of Unicode characters of source text +// Return Value: +// - Length in characters of multibyte buffer that would be required to hold this text after conversion +// - NOTE: Throws suitable HRESULT errors from memory allocation, safe math, or WideCharToMultiByte failures. +[[nodiscard]] +size_t GetALengthFromW(const UINT codepage, const std::wstring_view source) +{ + // If there's no bytes, bail early. + if (source.empty()) + { + return 0; + } + + int iSource; // convert to int because Wc2Mb requires it + THROW_IF_FAILED(SizeTToInt(source.size(), &iSource)); + + // Ask how many bytes this string consumes in the other codepage +#pragma prefast(suppress:__WARNING_W2A_BEST_FIT, "WC_NO_BEST_FIT_CHARS doesn't work in many codepages. Retain old behavior.") + int const iTarget = WideCharToMultiByte(codepage, 0, source.data(), iSource, nullptr, 0, nullptr, nullptr); + THROW_LAST_ERROR_IF(0 == iTarget); + + // Convert types safely. + size_t cchTarget; + THROW_IF_FAILED(IntToSizeT(iTarget, &cchTarget)); + + return cchTarget; +} + +std::deque> CharToKeyEvents(const wchar_t wch, + const unsigned int codepage) +{ + const short invalidKey = -1; + short keyState = VkKeyScanW(wch); + + if (keyState == invalidKey) + { + // Determine DBCS character because these character does not know by VkKeyScan. + // GetStringTypeW(CT_CTYPE3) & C3_ALPHA can determine all linguistic characters. However, this is + // not include symbolic character for DBCS. + WORD CharType = 0; + GetStringTypeW(CT_CTYPE3, &wch, 1, &CharType); + + if (WI_IsFlagSet(CharType, C3_ALPHA) || GetCharWidth(wch) == CodepointWidth::Wide) + { + keyState = 0; + } + } + + std::deque> convertedEvents; + if (keyState == invalidKey) + { + // if VkKeyScanW fails (char is not in kbd layout), we must + // emulate the key being input through the numpad + convertedEvents = SynthesizeNumpadEvents(wch, codepage); + } + else + { + convertedEvents = SynthesizeKeyboardEvents(wch, keyState); + } + + return convertedEvents; +} + + +// Routine Description: +// - converts a wchar_t into a series of KeyEvents as if it was typed +// using the keyboard +// Arguments: +// - wch - the wchar_t to convert +// Return Value: +// - deque of KeyEvents that represent the wchar_t being typed +// Note: +// - will throw exception on error +std::deque> SynthesizeKeyboardEvents(const wchar_t wch, const short keyState) +{ + const byte modifierState = HIBYTE(keyState); + + bool altGrSet = false; + bool shiftSet = false; + std::deque> keyEvents; + + // add modifier key event if necessary + if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) + { + altGrSet = true; + keyEvents.push_back(std::make_unique(true, + 1ui16, + static_cast(VK_MENU), + altScanCode, + UNICODE_NULL, + (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED))); + } + else if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) + { + shiftSet = true; + keyEvents.push_back(std::make_unique(true, + 1ui16, + static_cast(VK_SHIFT), + leftShiftScanCode, + UNICODE_NULL, + SHIFT_PRESSED)); + } + + const WORD virtualScanCode = gsl::narrow(MapVirtualKeyW(wch, MAPVK_VK_TO_VSC)); + KeyEvent keyEvent{ true, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }; + + // add modifier flags if necessary + if (WI_IsFlagSet(modifierState, VkKeyScanModState::ShiftPressed)) + { + keyEvent.ActivateModifierKey(ModifierKeyState::Shift); + } + if (WI_IsFlagSet(modifierState, VkKeyScanModState::CtrlPressed)) + { + keyEvent.ActivateModifierKey(ModifierKeyState::LeftCtrl); + } + if (WI_AreAllFlagsSet(modifierState, VkKeyScanModState::CtrlAndAltPressed)) + { + keyEvent.ActivateModifierKey(ModifierKeyState::RightAlt); + } + + // add key event down and up + keyEvents.push_back(std::make_unique(keyEvent)); + keyEvent.SetKeyDown(false); + keyEvents.push_back(std::make_unique(keyEvent)); + + // add modifier key up event + if (altGrSet) + { + keyEvents.push_back(std::make_unique(false, + 1ui16, + static_cast(VK_MENU), + altScanCode, + UNICODE_NULL, + ENHANCED_KEY)); + } + else if (shiftSet) + { + keyEvents.push_back(std::make_unique(false, + 1ui16, + static_cast(VK_SHIFT), + leftShiftScanCode, + UNICODE_NULL, + 0)); + } + + return keyEvents; +} + +// Routine Description: +// - converts a wchar_t into a series of KeyEvents as if it was typed +// using Alt + numpad +// Arguments: +// - wch - the wchar_t to convert +// Return Value: +// - deque of KeyEvents that represent the wchar_t being typed using +// alt + numpad +// Note: +// - will throw exception on error +std::deque> SynthesizeNumpadEvents(const wchar_t wch, const unsigned int codepage) +{ + std::deque> keyEvents; + + //alt keydown + keyEvents.push_back(std::make_unique(true, + 1ui16, + static_cast(VK_MENU), + altScanCode, + UNICODE_NULL, + LEFT_ALT_PRESSED)); + + const int radix = 10; + std::wstring wstr{ wch }; + const auto convertedChars = ConvertToA(codepage, wstr); + if (convertedChars.size() == 1) + { + // It is OK if the char is "signed -1", we want to interpret that as "unsigned 255" for the + // "integer to character" conversion below with ::to_string, thus the static_cast. + // Prime example is nonbreaking space U+00A0 will convert to OEM by codepage 437 to 0xFF which is -1 signed. + // But it is absolutely valid as 0xFF or 255 unsigned as the correct CP437 character. + // We need to treat it as unsigned because we're going to pretend it was a keypad entry + // and you don't enter negative numbers on the keypad. + unsigned char const uch = static_cast(convertedChars[0]); + + // unsigned char values are in the range [0, 255] so we need to be + // able to store up to 4 chars from the conversion (including the end of string char) + auto charString = std::to_string(uch); + + for (auto& ch : std::string_view(charString)) + { + if (ch == 0) + { + break; + } + const WORD virtualKey = ch - '0' + VK_NUMPAD0; + const WORD virtualScanCode = gsl::narrow(MapVirtualKeyW(virtualKey, MAPVK_VK_TO_VSC)); + + keyEvents.push_back(std::make_unique(true, + 1ui16, + virtualKey, + virtualScanCode, + UNICODE_NULL, + LEFT_ALT_PRESSED)); + keyEvents.push_back(std::make_unique(false, + 1ui16, + virtualKey, + virtualScanCode, + UNICODE_NULL, + LEFT_ALT_PRESSED)); + } + } + + // alt keyup + keyEvents.push_back(std::make_unique(false, + 1ui16, + static_cast(VK_MENU), + altScanCode, + wch, + 0)); + return keyEvents; +} + +// Routine Description: +// - naively determines the width of a UCS2 encoded wchar +// Arguments: +// - wch - the wchar_t to measure +// Return Value: +// - CodepointWidth indicating width of wch +// Notes: +// 04-08-92 ShunK Created. +// Jul-27-1992 KazuM Added Screen Information and Code Page Information. +// Jan-29-1992 V-Hirots Substruct Screen Information. +// Oct-06-1996 KazuM Not use RtlUnicodeToMultiByteSize and WideCharToMultiByte +// Because 950 (Chinese Traditional) only defined 13500 chars, +// and unicode defined almost 18000 chars. +// So there are almost 4000 chars can not be mapped to big5 code. +// Apr-30-2015 MiNiksa Corrected unknown character code assumption. Max Width in Text Metric +// is not reliable for calculating half/full width. Must use current +// display font data (cached) instead. +// May-23-2017 migrie Forced Box-Drawing Characters (x2500-x257F) to narrow. +// Jan-16-2018 migrie Seperated core lookup from asking the renderer the width +CodepointWidth GetCharWidth(const wchar_t wch) noexcept +{ + // 0x00-0x1F is ambiguous by font + if (0x20 <= wch && wch <= 0x7e) + { + /* ASCII */ + return CodepointWidth::Narrow; + } + // 0x80 - 0x0451 varies from narrow to ambiguous by character and font (Unicode 9.0) + else if (0x0452 <= wch && wch <= 0x10FF) + { + // From Unicode 9.0, this range is narrow (assorted languages) + return CodepointWidth::Narrow; + } + else if (0x1100 <= wch && wch <= 0x115F) + { + // From Unicode 9.0, Hangul Choseong is wide + return CodepointWidth::Wide; + } + else if (0x1160 <= wch && wch <= 0x200F) + { + // From Unicode 9.0, this range is narrow (assorted languages) + return CodepointWidth::Narrow; + } + // 0x2500 - 0x257F is the box drawing character range - + // Technically, these are ambiguous width characters, but applications that + // use them generally assume that they're narrow to ensure proper alignment. + else if (0x2500 <= wch && wch <= 0x257F) + { + return CodepointWidth::Narrow; + } + // 0x2010 - 0x2B59 varies between narrow, ambiguous, and wide by character and font (Unicode 9.0) + else if (0x2B5A <= wch && wch <= 0x2E44) + { + // From Unicode 9.0, this range is narrow (assorted languages) + return CodepointWidth::Narrow; + } + else if (0x2E80 <= wch && wch <= 0x303e) + { + // From Unicode 9.0, this range is wide (assorted languages) + return CodepointWidth::Wide; + } + else if (0x3041 <= wch && wch <= 0x3094) + { + /* Hiragana */ + return CodepointWidth::Wide; + } + else if (0x30a1 <= wch && wch <= 0x30f6) + { + /* Katakana */ + return CodepointWidth::Wide; + } + else if (0x3105 <= wch && wch <= 0x312c) + { + /* Bopomofo */ + return CodepointWidth::Wide; + } + else if (0x3131 <= wch && wch <= 0x318e) + { + /* Hangul Elements */ + return CodepointWidth::Wide; + } + else if (0x3190 <= wch && wch <= 0x3247) + { + // From Unicode 9.0, this range is wide + return CodepointWidth::Wide; + } + else if (0x3251 <= wch && wch <= 0xA4C6) + { + // This exception range is narrow width hexagrams. + if (0x4DC0 <= wch && wch <= 0x4DFF) + { + return CodepointWidth::Narrow; + } + else + { + // From Unicode 9.0, this range is wide + // CJK Unified Ideograph and Yi and Reserved. + // Includes Han Ideographic range. + return CodepointWidth::Wide; + } + } + else if (0xA4D0 <= wch && wch <= 0xABF9) + { + // This exception range is wide Hangul Choseong + if (0xA960 <= wch && wch <= 0xA97C) + { + return CodepointWidth::Wide; + } + else + { + // From Unicode 9.0, this range is narrow (assorted languages) + return CodepointWidth::Narrow; + } + } + else if (0xac00 <= wch && wch <= 0xd7a3) + { + /* Korean Hangul Syllables */ + return CodepointWidth::Wide; + } + else if (0xD7B0 <= wch && wch <= 0xD7FB) + { + // From Unicode 9.0, this range is narrow + // Hangul Jungseong and Hangul Jongseong + return CodepointWidth::Narrow; + } + // 0xD800-0xDFFF is reserved for UTF-16 surrogate pairs. + // 0xE000-0xF8FF is reserved for private use characters and is therefore always ambiguous. + else if (0xF900 <= wch && wch <= 0xFAFF) + { + // From Unicode 9.0, this range is wide + // CJK Compatibility Ideographs + // Includes Han Compatibility Ideographs + return CodepointWidth::Wide; + } + else if (0xFB00 <= wch && wch <= 0xFDFD) + { + // From Unicode 9.0, this range is narrow (assorted languages) + return CodepointWidth::Narrow; + } + else if (0xFE10 <= wch && wch <= 0xFE6B) + { + // This exception range has narrow combining ligatures + if (0xFE20 <= wch && wch <= 0xFE2F) + { + return CodepointWidth::Narrow; + } + else + { + // From Unicode 9.0, this range is wide + // Presentation forms + return CodepointWidth::Wide; + } + } + else if (0xFE70 <= wch && wch <= 0xFEFF) + { + // From Unicode 9.0, this range is narrow + return CodepointWidth::Narrow; + } + else if (0xff01 <= wch && wch <= 0xff5e) + { + /* Fullwidth ASCII variants */ + return CodepointWidth::Wide; + } + else if (0xff61 <= wch && wch <= 0xff9f) + { + /* Halfwidth Katakana variants */ + return CodepointWidth::Narrow; + } + else if ((0xffa0 <= wch && wch <= 0xffbe) || + (0xffc2 <= wch && wch <= 0xffc7) || + (0xffca <= wch && wch <= 0xffcf) || + (0xffd2 <= wch && wch <= 0xffd7) || + (0xffda <= wch && wch <= 0xffdc)) + { + /* Halfwidth Hangule variants */ + return CodepointWidth::Narrow; + } + else if (0xffe0 <= wch && wch <= 0xffe6) + { + /* Fullwidth symbol variants */ + return CodepointWidth::Wide; + } + // Currently we do not support codepoints above 0xffff + else + { + return CodepointWidth::Invalid; + } +} + +wchar_t Utf16ToUcs2(const std::wstring_view charData) +{ + THROW_HR_IF(E_INVALIDARG, charData.empty()); + if (charData.size() > 1) + { + return UNICODE_REPLACEMENT; + } + else + { + return charData.front(); + } +} diff --git a/src/types/dirs b/src/types/dirs new file mode 100644 index 000000000..95a663151 --- /dev/null +++ b/src/types/dirs @@ -0,0 +1,3 @@ +DIRS=lib \ + + diff --git a/src/types/inc/CodepointWidthDetector.hpp b/src/types/inc/CodepointWidthDetector.hpp new file mode 100644 index 000000000..689c52004 --- /dev/null +++ b/src/types/inc/CodepointWidthDetector.hpp @@ -0,0 +1,123 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- CodepointWidthDetector.hpp + +Abstract: +- Object used to measure the width of a codepoint when it's rendered + +Author: +- Austin Diviness (AustDi) 18-May-2018 +--*/ + +#pragma once + +#include "convert.hpp" + +static_assert(sizeof(unsigned int) == sizeof(wchar_t) * 2, + "UnicodeRange expects to be able to store a unicode codepoint in an unsigned int"); + + +// use to measure the width of a codepoint +class CodepointWidthDetector final +{ +protected: + + // used to store range data in CodepointWidthDetector's internal map + class UnicodeRange final + { + public: + UnicodeRange(const unsigned int lowerBound, + const unsigned int upperBound) : + _lowerBound{ lowerBound }, + _upperBound{ upperBound }, + _isBounds{ true } + { + } + + UnicodeRange(const unsigned int searchTerm) : + _lowerBound{ searchTerm }, + _upperBound{ searchTerm }, + _isBounds{ false } + { + } + + bool IsBounds() const noexcept + { + return _isBounds; + } + + unsigned int LowerBound() const + { + FAIL_FAST_IF(!_isBounds); + return _lowerBound; + } + + unsigned int UpperBound() const + { + FAIL_FAST_IF(!_isBounds); + return _upperBound; + } + + unsigned int SearchTerm() const + { + FAIL_FAST_IF(_isBounds); + return _lowerBound; + } + + private: + unsigned int _lowerBound; + unsigned int _upperBound; + bool _isBounds; + }; + + // used for comparing if we've found the range that a searching UnicodeRange falls into + struct UnicodeRangeCompare final + { + bool operator()(const UnicodeRange& a, const UnicodeRange& b) const + { + if (!a.IsBounds() && b.IsBounds()) + { + return a.SearchTerm() < b.LowerBound(); + } + else if (a.IsBounds() && !b.IsBounds()) + { + return a.UpperBound() < b.SearchTerm(); + } + else if (a.IsBounds() && b.IsBounds()) + { + return a.LowerBound() < b.LowerBound(); + } + else + { + return a.SearchTerm() < b.SearchTerm(); + } + } + }; + +public: + CodepointWidthDetector() = default; + CodepointWidthDetector(const CodepointWidthDetector&) = delete; + CodepointWidthDetector(CodepointWidthDetector&&) = delete; + ~CodepointWidthDetector() = default; + CodepointWidthDetector& operator=(const CodepointWidthDetector&) = delete; + + CodepointWidth GetWidth(const std::wstring_view glyph) const noexcept; + bool IsWide(const std::wstring_view glyph) const; + bool IsWide(const wchar_t wch) const noexcept; + void SetFallbackMethod(std::function pfnFallback); + +#ifdef UNIT_TESTING + friend class CodepointWidthDetectorTests; +#endif + +private: + bool _lookupIsWide(const std::wstring_view glyph) const noexcept; + unsigned int _extractCodepoint(const std::wstring_view glyph) const noexcept; + void _populateUnicodeSearchMap(); + + std::map _map; + std::function _pfnFallbackMethod; + bool _hasFallback = false; +}; diff --git a/src/types/inc/GlyphWidth.hpp b/src/types/inc/GlyphWidth.hpp new file mode 100644 index 000000000..45f611ae6 --- /dev/null +++ b/src/types/inc/GlyphWidth.hpp @@ -0,0 +1,14 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- GlyphWidth.hpp + +Abstract: +- Helpers for determining the width of a particular string of chars. + +*/ + +bool IsGlyphFullWidth(const std::wstring_view glyph); +bool IsGlyphFullWidth(const wchar_t wch); +void SetGlyphWidthFallback(std::function pfnFallback); diff --git a/src/types/inc/IInputEvent.hpp b/src/types/inc/IInputEvent.hpp new file mode 100644 index 000000000..25bba2792 --- /dev/null +++ b/src/types/inc/IInputEvent.hpp @@ -0,0 +1,478 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- IInputEvent.hpp + +Abstract: +- Internal representation of public INPUT_RECORD struct. + +Author: +- Austin Diviness (AustDi) 18-Aug-2017 +--*/ + +#pragma once + +#include +#include + +#ifndef ALTNUMPAD_BIT +// from winconp.h +#define ALTNUMPAD_BIT 0x04000000 // AltNumpad OEM char (copied from ntuser\inc\kbd.h) +#endif + +#include + +#include +#include +#include +#include + +enum class InputEventType +{ + KeyEvent, + MouseEvent, + WindowBufferSizeEvent, + MenuEvent, + FocusEvent +}; + +class IInputEvent +{ +public: + static std::unique_ptr Create(const INPUT_RECORD& record); + static std::deque> Create(gsl::span records); + static std::deque> Create(const std::deque& records); + + static std::vector ToInputRecords(const std::deque>& events); + + virtual ~IInputEvent() = 0; + IInputEvent() = default; + IInputEvent(const IInputEvent&) = default; + IInputEvent(IInputEvent&&) = default; + IInputEvent& operator=(const IInputEvent&)& = default; + IInputEvent& operator=(IInputEvent&&)& = default; + + virtual INPUT_RECORD ToInputRecord() const noexcept = 0; + + virtual InputEventType EventType() const noexcept = 0; + +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const IInputEvent* const pEvent); +#endif +}; + +inline IInputEvent::~IInputEvent() +{ + +} + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const IInputEvent* pEvent); +#endif + +#define ALT_PRESSED (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED) +#define CTRL_PRESSED (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED) +#define MOD_PRESSED (SHIFT_PRESSED | ALT_PRESSED | CTRL_PRESSED) + +// Note taken from VkKeyScan docs (https://msdn.microsoft.com/en-us/library/windows/desktop/ms646329(v=vs.85).aspx): +// For keyboard layouts that use the right-hand ALT key as a shift key +// (for example, the French keyboard layout), the shift state is +// represented by the value 6, because the right-hand ALT key is +// converted internally into CTRL+ALT. +struct VkKeyScanModState +{ + static const byte None = 0; + static const byte ShiftPressed = 1; + static const byte CtrlPressed = 2; + static const byte ShiftAndCtrlPressed = ShiftPressed | CtrlPressed; + static const byte AltPressed = 4; + static const byte ShiftAndAltPressed = ShiftPressed | AltPressed; + static const byte CtrlAndAltPressed = CtrlPressed | AltPressed; + static const byte ModPressed = ShiftPressed | CtrlPressed | AltPressed; +}; + +enum class ModifierKeyState +{ + RightAlt, + LeftAlt, + RightCtrl, + LeftCtrl, + Shift, + NumLock, + ScrollLock, + CapsLock, + EnhancedKey, + NlsDbcsChar, + NlsAlphanumeric, + NlsKatakana, + NlsHiragana, + NlsRoman, + NlsImeConversion, + AltNumpad, + NlsImeDisable, + ENUM_COUNT // must be the last element in the enum class +}; + +std::unordered_set FromVkKeyScan(const short vkKeyScanFlags); +std::unordered_set FromConsoleControlKeyFlags(const DWORD flags); +DWORD ToConsoleControlKeyFlag(const ModifierKeyState modifierKey) noexcept; + +class KeyEvent : public IInputEvent +{ +public: + constexpr KeyEvent(const KEY_EVENT_RECORD& record) : + _keyDown{ !!record.bKeyDown }, + _repeatCount{ record.wRepeatCount }, + _virtualKeyCode{ record.wVirtualKeyCode }, + _virtualScanCode{ record.wVirtualScanCode }, + _charData{ record.uChar.UnicodeChar }, + _activeModifierKeys{ record.dwControlKeyState } + { + } + + constexpr KeyEvent(const bool keyDown, + const WORD repeatCount, + const WORD virtualKeyCode, + const WORD virtualScanCode, + const wchar_t charData, + const DWORD activeModifierKeys) : + _keyDown{ keyDown }, + _repeatCount{ repeatCount }, + _virtualKeyCode{ virtualKeyCode }, + _virtualScanCode{ virtualScanCode }, + _charData{ charData }, + _activeModifierKeys{ activeModifierKeys } + { + } + + constexpr KeyEvent() noexcept : + _keyDown{ 0 }, + _repeatCount{ 0 }, + _virtualKeyCode{ 0 }, + _virtualScanCode{ 0 }, + _charData { 0 }, + _activeModifierKeys{ 0 } + { + } + + ~KeyEvent(); + KeyEvent(const KeyEvent&) = default; + KeyEvent(KeyEvent&&) = default; + KeyEvent& operator=(const KeyEvent&)& = default; + KeyEvent& operator=(KeyEvent&&)& = default; + + INPUT_RECORD ToInputRecord() const noexcept override; + InputEventType EventType() const noexcept override; + + constexpr bool IsShiftPressed() const noexcept + { + return WI_IsFlagSet(_activeModifierKeys, SHIFT_PRESSED); + } + + constexpr bool IsAltPressed() const noexcept + { + return WI_IsAnyFlagSet(_activeModifierKeys, ALT_PRESSED); + } + + constexpr bool IsCtrlPressed() const noexcept + { + return WI_IsAnyFlagSet(_activeModifierKeys, CTRL_PRESSED); + } + + constexpr bool IsAltGrPressed() const noexcept + { + return WI_IsFlagSet(_activeModifierKeys, LEFT_CTRL_PRESSED) && + WI_IsFlagSet(_activeModifierKeys, RIGHT_ALT_PRESSED); + } + + constexpr bool IsModifierPressed() const noexcept + { + return WI_IsAnyFlagSet(_activeModifierKeys, MOD_PRESSED); + } + + constexpr bool IsCursorKey() const noexcept + { + // true iff vk in [End, Home, Left, Up, Right, Down] + return (_virtualKeyCode >= VK_END) && (_virtualKeyCode <= VK_DOWN); + } + + constexpr bool IsAltNumpadSet() const noexcept + { + return WI_IsFlagSet(_activeModifierKeys, ALTNUMPAD_BIT); + } + + constexpr bool IsKeyDown() const noexcept + { + return _keyDown; + } + + constexpr bool IsPauseKey() const noexcept + { + return (_virtualKeyCode == VK_PAUSE); + } + + constexpr WORD GetRepeatCount() const noexcept + { + return _repeatCount; + } + + constexpr WORD GetVirtualKeyCode() const noexcept + { + return _virtualKeyCode; + } + + constexpr WORD GetVirtualScanCode() const noexcept + { + return _virtualScanCode; + } + + constexpr wchar_t GetCharData() const noexcept + { + return _charData; + } + + constexpr DWORD GetActiveModifierKeys() const noexcept + { + return _activeModifierKeys; + } + + void SetKeyDown(const bool keyDown) noexcept; + void SetRepeatCount(const WORD repeatCount) noexcept; + void SetVirtualKeyCode(const WORD virtualKeyCode) noexcept; + void SetVirtualScanCode(const WORD virtualScanCode) noexcept; + void SetCharData(const wchar_t character) noexcept; + + void SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept; + void DeactivateModifierKey(const ModifierKeyState modifierKey) noexcept; + void ActivateModifierKey(const ModifierKeyState modifierKey) noexcept; + bool DoActiveModifierKeysMatch(const std::unordered_set& consoleModifiers) const noexcept; + bool IsCommandLineEditingKey() const noexcept; + bool IsPopupKey() const noexcept; + +private: + bool _keyDown; + WORD _repeatCount; + WORD _virtualKeyCode; + WORD _virtualScanCode; + wchar_t _charData; + DWORD _activeModifierKeys; + + friend constexpr bool operator==(const KeyEvent& a, const KeyEvent& b) noexcept; +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent); +#endif +}; + +constexpr bool operator==(const KeyEvent& a, const KeyEvent& b) noexcept +{ + return (a._keyDown == b._keyDown && + a._repeatCount == b._repeatCount && + a._virtualKeyCode == b._virtualKeyCode && + a._virtualScanCode == b._virtualScanCode && + a._charData == b._charData && + a._activeModifierKeys == b._activeModifierKeys); +} + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const KeyEvent* const pKeyEvent); +#endif + +class MouseEvent : public IInputEvent +{ +public: + constexpr MouseEvent(const MOUSE_EVENT_RECORD& record) : + _position{ record.dwMousePosition }, + _buttonState{ record.dwButtonState }, + _activeModifierKeys{ record.dwControlKeyState }, + _eventFlags{ record.dwEventFlags } + { + } + + constexpr MouseEvent(const COORD position, + const DWORD buttonState, + const DWORD activeModifierKeys, + const DWORD eventFlags) : + _position{ position }, + _buttonState{ buttonState }, + _activeModifierKeys{ activeModifierKeys }, + _eventFlags{ eventFlags } + { + } + + ~MouseEvent(); + MouseEvent(const MouseEvent&) = default; + MouseEvent(MouseEvent&&) = default; + MouseEvent& operator=(const MouseEvent&)& = default; + MouseEvent& operator=(MouseEvent&&)& = default; + + INPUT_RECORD ToInputRecord() const noexcept override; + InputEventType EventType() const noexcept override; + + constexpr bool IsMouseMoveEvent() const noexcept + { + return _eventFlags == MOUSE_MOVED; + } + + constexpr COORD GetPosition() const noexcept + { + return _position; + } + + constexpr DWORD GetButtonState() const noexcept + { + return _buttonState; + } + + constexpr DWORD GetActiveModifierKeys() const noexcept + { + return _activeModifierKeys; + } + + constexpr DWORD GetEventFlags() const noexcept + { + return _eventFlags; + } + + void SetPosition(const COORD position) noexcept; + void SetButtonState(const DWORD buttonState) noexcept; + void SetActiveModifierKeys(const DWORD activeModifierKeys) noexcept; + void SetEventFlags(const DWORD eventFlags) noexcept; + +private: + COORD _position; + DWORD _buttonState; + DWORD _activeModifierKeys; + DWORD _eventFlags; + +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent); +#endif +}; + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const MouseEvent* const pMouseEvent); +#endif + +class WindowBufferSizeEvent : public IInputEvent +{ +public: + constexpr WindowBufferSizeEvent(const WINDOW_BUFFER_SIZE_RECORD& record) : + _size{ record.dwSize } + { + } + + constexpr WindowBufferSizeEvent(const COORD size) : + _size{ size } + { + } + + ~WindowBufferSizeEvent(); + WindowBufferSizeEvent(const WindowBufferSizeEvent&) = default; + WindowBufferSizeEvent(WindowBufferSizeEvent&&) = default; + WindowBufferSizeEvent& operator=(const WindowBufferSizeEvent&)& = default; + WindowBufferSizeEvent& operator=(WindowBufferSizeEvent&&)& = default; + + INPUT_RECORD ToInputRecord() const noexcept override; + InputEventType EventType() const noexcept override; + + constexpr COORD GetSize() const noexcept + { + return _size; + } + + void SetSize(const COORD size) noexcept; + +private: + COORD _size; + +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent); +#endif +}; + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const WindowBufferSizeEvent* const pEvent); +#endif + +class MenuEvent : public IInputEvent +{ +public: + constexpr MenuEvent(const MENU_EVENT_RECORD& record) : + _commandId{ record.dwCommandId } + { + } + + constexpr MenuEvent(const UINT commandId) : + _commandId{ commandId } + { + } + + ~MenuEvent(); + MenuEvent(const MenuEvent&) = default; + MenuEvent(MenuEvent&&) = default; + MenuEvent& operator=(const MenuEvent&)& = default; + MenuEvent& operator=(MenuEvent&&)& = default; + + INPUT_RECORD ToInputRecord() const noexcept override; + InputEventType EventType() const noexcept override; + + constexpr UINT GetCommandId() const noexcept + { + return _commandId; + } + + void SetCommandId(const UINT commandId) noexcept; + +private: + UINT _commandId; + +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); +#endif +}; + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const MenuEvent* const pMenuEvent); +#endif + +class FocusEvent : public IInputEvent +{ +public: + constexpr FocusEvent(const FOCUS_EVENT_RECORD& record) : + _focus{ !!record.bSetFocus } + { + } + + constexpr FocusEvent(const bool focus) : + _focus{ focus } + { + } + + ~FocusEvent(); + FocusEvent(const FocusEvent&) = default; + FocusEvent(FocusEvent&&) = default; + FocusEvent& operator=(const FocusEvent&)& = default; + FocusEvent& operator=(FocusEvent&&)& = default; + + INPUT_RECORD ToInputRecord() const noexcept override; + InputEventType EventType() const noexcept override; + + constexpr bool GetFocus() const noexcept + { + return _focus; + } + + void SetFocus(const bool focus) noexcept; + +private: + bool _focus; + +#ifdef UNIT_TESTING + friend std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); +#endif +}; + +#ifdef UNIT_TESTING +std::wostream& operator<<(std::wostream& stream, const FocusEvent* const pFocusEvent); +#endif diff --git a/src/types/inc/Utf16Parser.hpp b/src/types/inc/Utf16Parser.hpp new file mode 100644 index 000000000..4d541eb8f --- /dev/null +++ b/src/types/inc/Utf16Parser.hpp @@ -0,0 +1,59 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- Utf16Parser.hpp + +Abstract: +- Parser for grouping together utf16 codepoints from a string of utf16 encoded text + +Author(s): +- Austin Diviness (AustDi) 25-Apr-2018 + +--*/ + +#pragma once + +#include +#include +#include + + +class Utf16Parser final +{ +private: + static constexpr unsigned short IndicatorBitCount = 6; + static constexpr unsigned short WcharShiftAmount = sizeof(wchar_t) * 8 - IndicatorBitCount; + static constexpr std::bitset LeadingSurrogateMask = { 54 }; // 110 110 indicates a leading surrogate + static constexpr std::bitset TrailingSurrogateMask = { 55 }; // 110 111 indicates a trailing surrogate + +public: + static std::vector> Parse(std::wstring_view wstr); + static std::wstring_view ParseNext(std::wstring_view wstr); + + // Routine Description: + // - checks if wchar is a utf16 leading surrogate + // Arguments: + // - wch - the wchar to check + // Return Value: + // - true if wch is a leading surrogate, false otherwise + static inline bool IsLeadingSurrogate(const wchar_t wch) noexcept + { + const wchar_t bits = wch >> WcharShiftAmount; + const std::bitset possBits = { bits }; + return (possBits ^ LeadingSurrogateMask).none(); + } + + // Routine Description: + // - checks if wchar is a utf16 trailing surrogate + // Arguments: + // - wch - the wchar to check + // Return Value: + // - true if wch is a trailing surrogate, false otherwise + static inline bool IsTrailingSurrogate(const wchar_t wch) noexcept + { + const wchar_t bits = wch >> WcharShiftAmount; + const std::bitset possBits = { bits }; + return (possBits ^ TrailingSurrogateMask).none(); + } +}; diff --git a/src/types/inc/convert.hpp b/src/types/inc/convert.hpp new file mode 100644 index 000000000..68842767d --- /dev/null +++ b/src/types/inc/convert.hpp @@ -0,0 +1,50 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- convert.hpp + +Abstract: +- Defines functions for converting between A and W text strings. + Largely taken from misc.h. + +Author: +- Mike Griese (migrie) 30-Oct-2017 + +--*/ + +#pragma once +#include +#include +#include "IInputEvent.hpp" + +enum class CodepointWidth : BYTE +{ + Narrow, + Wide, + Ambiguous, // could be narrow or wide depending on the current codepage and font + Invalid // not a valid unicode codepoint +}; + +[[nodiscard]] +std::wstring ConvertToW(const UINT codepage, + const std::string_view source); + +[[nodiscard]] +std::string ConvertToA(const UINT codepage, + const std::wstring_view source); + +[[nodiscard]] +size_t GetALengthFromW(const UINT codepage, + const std::wstring_view source); + +std::deque> CharToKeyEvents(const wchar_t wch, const unsigned int codepage); + +std::deque> SynthesizeKeyboardEvents(const wchar_t wch, + const short keyState); + +std::deque> SynthesizeNumpadEvents(const wchar_t wch, const unsigned int codepage); + +CodepointWidth GetCharWidth(const wchar_t wch) noexcept; + +wchar_t Utf16ToUcs2(const std::wstring_view charData); diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp new file mode 100644 index 000000000..ec5fc8499 --- /dev/null +++ b/src/types/inc/utils.hpp @@ -0,0 +1,27 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- utils.hpp + +Abstract: +- Helpful cross-lib utilities + +Author(s): +- Mike Griese (migrie) 12-Jun-2018 +--*/ + +namespace Microsoft::Console::Utils +{ + bool IsValidHandle(const HANDLE handle) noexcept; + + std::wstring GuidToString(const GUID guid); + GUID GuidFromString(const std::wstring wstr); + + std::wstring ColorToHexString(const COLORREF color); + COLORREF ColorFromHexString(const std::wstring wstr); + + void InitializeCampbellColorTable(gsl::span& table); + void Initialize256ColorTable(gsl::span& table); + void SetColorTableAlpha(gsl::span& table, const BYTE newAlpha); +} diff --git a/src/types/inc/viewport.hpp b/src/types/inc/viewport.hpp new file mode 100644 index 000000000..09b7d67c0 --- /dev/null +++ b/src/types/inc/viewport.hpp @@ -0,0 +1,178 @@ +/*++ +Copyright (c) Microsoft Corporation + +Module Name: +- viewport.hpp + +Abstract: +- This method provides an interface for abstracting viewport operations + +Author(s): +- Michael Niksa (miniksa) Nov 2015 +--*/ + +#pragma once +#include "../../inc/operators.hpp" + +namespace Microsoft::Console::Types +{ + struct SomeViewports; + + class Viewport final + { + public: + ~Viewport() {} + Viewport(const Viewport& other) noexcept; + Viewport(Viewport&&) = default; + Viewport& operator=(const Viewport&)& = default; + Viewport& operator=(Viewport&&)& = default; + + static Viewport Empty() noexcept; + + static Viewport FromInclusive(const SMALL_RECT sr) noexcept; + + static Viewport FromExclusive(const SMALL_RECT sr) noexcept; + + static Viewport FromDimensions(const COORD origin, + const short width, + const short height) noexcept; + + static Viewport FromDimensions(const COORD origin, + const COORD dimensions) noexcept; + + static Viewport FromDimensions(const COORD dimensions) noexcept; + + static Viewport FromCoord(const COORD origin) noexcept; + + SHORT Left() const noexcept; + SHORT RightInclusive() const noexcept; + SHORT RightExclusive() const noexcept; + SHORT Top() const noexcept; + SHORT BottomInclusive() const noexcept; + SHORT BottomExclusive() const noexcept; + SHORT Height() const noexcept; + SHORT Width() const noexcept; + COORD Origin() const noexcept; + COORD Dimensions() const noexcept; + + bool IsInBounds(const Viewport& other) const noexcept; + bool IsInBounds(const COORD& pos) const noexcept; + + void Clamp(COORD& pos) const; + Viewport Clamp(const Viewport& other) const; + + bool MoveInBounds(const ptrdiff_t move, COORD& pos) const noexcept; + bool IncrementInBounds(COORD& pos) const noexcept; + bool IncrementInBoundsCircular(COORD& pos) const noexcept; + bool DecrementInBounds(COORD& pos) const noexcept; + bool DecrementInBoundsCircular(COORD& pos) const noexcept; + int CompareInBounds(const COORD& first, const COORD& second) const noexcept; + + enum class XWalk + { + LeftToRight, + RightToLeft + }; + + enum class YWalk + { + TopToBottom, + BottomToTop + }; + + struct WalkDir final + { + const XWalk x; + const YWalk y; + }; + + bool WalkInBounds(COORD& pos, const WalkDir dir) const noexcept; + bool WalkInBoundsCircular(COORD& pos, const WalkDir dir) const noexcept; + COORD GetWalkOrigin(const WalkDir dir) const noexcept; + static WalkDir DetermineWalkDirection(const Viewport& source, const Viewport& target) noexcept; + + bool TrimToViewport(_Inout_ SMALL_RECT* const psr) const noexcept; + void ConvertToOrigin(_Inout_ SMALL_RECT* const psr) const noexcept; + void ConvertToOrigin(_Inout_ COORD* const pcoord) const noexcept; + [[nodiscard]] + Viewport ConvertToOrigin(const Viewport& other) const noexcept; + void ConvertFromOrigin(_Inout_ SMALL_RECT* const psr) const noexcept; + void ConvertFromOrigin(_Inout_ COORD* const pcoord) const noexcept; + [[nodiscard]] + Viewport ConvertFromOrigin(const Viewport& other) const noexcept; + + SMALL_RECT ToExclusive() const noexcept; + SMALL_RECT ToInclusive() const noexcept; + RECT ToRect() const noexcept; + + Viewport ToOrigin() const noexcept; + + bool IsValid() const noexcept; + + [[nodiscard]] + static Viewport Offset(const Viewport& original, const COORD delta); + + [[nodiscard]] + static Viewport Union(const Viewport& lhs, const Viewport& rhs) noexcept; + + [[nodiscard]] + static Viewport Intersect(const Viewport& lhs, const Viewport& rhs) noexcept; + + [[nodiscard]] + static SomeViewports Subtract(const Viewport& original, const Viewport& removeMe) noexcept; + + private: + Viewport(const SMALL_RECT sr) noexcept; + + // This is always stored as a Inclusive rect. + SMALL_RECT _sr; + +#if UNIT_TESTING + friend class ViewportTests; +#endif + }; + + struct SomeViewports final + { + unsigned char used{ 0 }; + std::array viewports { Viewport::Empty(), Viewport::Empty(), Viewport::Empty(), Viewport::Empty() }; + + // These two methods are to make this vaguely look like a std::vector. + + // Size is the number of viewports that are valid inside this structure + size_t size() const noexcept { return used; } + + // At retrieves a viewport at a particular index. If you retrieve beyond the valid size(), + // it will throw std::out_of_range + const Viewport& at(size_t index) const + { + if (index >= used) + { + throw std::out_of_range("Access attempted beyond valid size."); + } + return viewports.at(index); + } + }; +} + +inline COORD operator-(const COORD& a, const COORD& b) noexcept +{ + return { a.X - b.X, a.Y - b.Y }; +} + +inline COORD operator-(const COORD& c) noexcept +{ + return { -c.X, -c.Y }; +} + +inline bool operator==(const Microsoft::Console::Types::Viewport& a, + const Microsoft::Console::Types::Viewport& b) noexcept +{ + return a.ToInclusive() == b.ToInclusive(); +} + +inline bool operator!=(const Microsoft::Console::Types::Viewport& a, + const Microsoft::Console::Types::Viewport& b) noexcept +{ + return !(a == b); +} diff --git a/src/types/lib/sources b/src/types/lib/sources new file mode 100644 index 000000000..d2fd46c81 --- /dev/null +++ b/src/types/lib/sources @@ -0,0 +1,8 @@ +!include ..\sources.inc + +# ------------------------------------- +# Program Information +# ------------------------------------- + +TARGETNAME = ConTypes +TARGETTYPE = LIBRARY diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj new file mode 100644 index 000000000..fe556f509 --- /dev/null +++ b/src/types/lib/types.vcxproj @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + {18D09A24-8240-42D6-8CB6-236EEE820263} + Win32Proj + types + Types + ConTypes + + + + + diff --git a/src/types/lib/types.vcxproj.filters b/src/types/lib/types.vcxproj.filters new file mode 100644 index 000000000..dcaacadd5 --- /dev/null +++ b/src/types/lib/types.vcxproj.filters @@ -0,0 +1,90 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A3FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52ECFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {77DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + \ No newline at end of file diff --git a/src/types/precomp.cpp b/src/types/precomp.cpp new file mode 100644 index 000000000..c51e9b31b --- /dev/null +++ b/src/types/precomp.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" diff --git a/src/types/precomp.h b/src/types/precomp.h new file mode 100644 index 000000000..3cc977333 --- /dev/null +++ b/src/types/precomp.h @@ -0,0 +1,81 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- precomp.h + +Abstract: +- Contains external headers to include in the precompile phase of console build process. +- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building). +--*/ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +// This includes support libraries from the CRT, STL, WIL, and GSL +#include "LibraryIncludes.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#endif + +// Windows Header Files: +#include + +typedef long NTSTATUS; +#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) +#define STATUS_SHARING_VIOLATION ((NTSTATUS)0xC0000043L) +#define STATUS_INSUFFICIENT_RESOURCES ((DWORD)0xC000009AL) +#define STATUS_ILLEGAL_FUNCTION ((DWORD)0xC00000AFL) +#define STATUS_PIPE_DISCONNECTED ((DWORD)0xC00000B0L) +#define STATUS_BUFFER_TOO_SMALL ((DWORD)0xC0000023L) + +// +// Map a WIN32 error value into an NTSTATUS +// Note: This assumes that WIN32 errors fall in the range -32k to 32k. +// + +#define FACILITY_NTWIN32 0x7 + +#define __NTSTATUS_FROM_WIN32(x) ((NTSTATUS)(x) <= 0 ? ((NTSTATUS)(x)) : ((NTSTATUS) (((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR))) + +#ifdef INLINE_NTSTATUS_FROM_WIN32 +#ifndef __midl +__inline NTSTATUS_FROM_WIN32(long x) { return x <= 0 ? (NTSTATUS)x : (NTSTATUS)(((x) & 0x0000FFFF) | (FACILITY_NTWIN32 << 16) | ERROR_SEVERITY_ERROR); } +#else +#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) +#endif +#else +#define NTSTATUS_FROM_WIN32(x) __NTSTATUS_FROM_WIN32(x) +#endif + +#include +#pragma prefast(push) +#pragma prefast(disable:26071, "Range violation in Intsafe. Not ours.") +#define ENABLE_INTSAFE_SIGNED_FUNCTIONS // Only unsigned intsafe math/casts available without this def +#include +#pragma prefast(pop) + +// private dependencies +#pragma warning(push) +#pragma warning(disable: ALL_CPPCORECHECK_WARNINGS) +#include "..\host\conddkrefs.h" +#pragma warning(pop) + +#include +#include +#include +#include +#include + +// TODO: MSFT 9355094 Find a better way of doing this. http://osgvsowi/9355094 +[[nodiscard]] +constexpr NTSTATUS NTSTATUS_FROM_HRESULT(HRESULT hr) noexcept +{ + return NTSTATUS_FROM_WIN32(HRESULT_CODE(hr)); +} diff --git a/src/types/sources.inc b/src/types/sources.inc new file mode 100644 index 000000000..a0cc8151a --- /dev/null +++ b/src/types/sources.inc @@ -0,0 +1,47 @@ +!include ..\..\project.inc + +# ------------------------------------- +# Windows Console +# - Console Types Library +# ------------------------------------- + +# This module encapsulates types and helpers that are common +# across the entire console project + +# ------------------------------------- +# Preprocessor Settings +# ------------------------------------- + +C_DEFINES = $(C_DEFINES) -DBUILD_ONECORE_INTERACTIVITY + +# ------------------------------------- +# Build System Settings +# ------------------------------------- + +# Code in the OneCore depot automatically excludes default Win32 libraries. + +# ------------------------------------- +# Sources, Headers, and Libraries +# ------------------------------------- + +PRECOMPILED_CXX = 1 +PRECOMPILED_INCLUDE = ..\precomp.h + +SOURCES= \ + ..\CodepointWidthDetector.cpp \ + ..\IInputEvent.cpp \ + ..\FocusEvent.cpp \ + ..\GlyphWidth.cpp \ + ..\KeyEvent.cpp \ + ..\MenuEvent.cpp \ + ..\ModifierKeyState.cpp \ + ..\MouseEvent.cpp \ + ..\Viewport.cpp \ + ..\WindowBufferSizeEvent.cpp \ + ..\convert.cpp \ + ..\Utf16Parser.cpp \ + ..\utils.cpp \ + +INCLUDES= \ + $(INCLUDES); \ + ..; \ diff --git a/src/types/utils.cpp b/src/types/utils.cpp new file mode 100644 index 000000000..cd099e88f --- /dev/null +++ b/src/types/utils.cpp @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/utils.hpp" +#include +using namespace Microsoft::Console; + +// Function Description: +// - Creates a String representation of a guid, in the format +// "{12345678-ABCD-EF12-3456-7890ABCDEF12}" +// Arguments: +// - guid: the GUID to create the string for +// Return Value: +// - a string representation of the GUID. On failure, throws E_INVALIDARG. +std::wstring Utils::GuidToString(const GUID guid) +{ + wchar_t guid_cstr[39]; + const int written = swprintf(guid_cstr, sizeof(guid_cstr), + L"{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", + guid.Data1, guid.Data2, guid.Data3, + guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], + guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + + THROW_HR_IF(E_INVALIDARG, written == -1); + + return std::wstring(guid_cstr); +} + +// Method Description: +// - Parses a GUID from a string representation of the GUID. Throws an exception +// if it fails to parse the GUID. See documentation of IIDFromString for +// details. +// Arguments: +// - wstr: a string representation of the GUID to parse +// Return Value: +// - A GUID if the string could successfully be parsed. On failure, throws the +// failing HRESULT. +GUID Utils::GuidFromString(const std::wstring wstr) +{ + GUID result{}; + THROW_IF_FAILED(IIDFromString(wstr.c_str(), &result)); + return result; +} + +// Function Description: +// - Creates a String representation of a color, in the format "#RRGGBB" +// Arguments: +// - color: the COLORREF to create the string for +// Return Value: +// - a string representation of the color +std::wstring Utils::ColorToHexString(const COLORREF color) +{ + std::wstringstream ss; + ss << L"#" << std::uppercase << std::setfill(L'0') << std::hex; + ss << std::setw(2) << GetRValue(color); + ss << std::setw(2) << GetGValue(color); + ss << std::setw(2) << GetBValue(color); + return ss.str(); +} + +// Function Description: +// - Parses a color from a string. The string should be in the format "#RRGGBB" +// Arguments: +// - wstr: a string representation of the COLORREF to parse +// Return Value: +// - A COLORREF if the string could successfully be parsed. If the string is not +// the correct format, throws E_INVALIDARG +COLORREF Utils::ColorFromHexString(const std::wstring wstr) +{ + THROW_HR_IF(E_INVALIDARG, wstr.size() < 7 || wstr.size() >= 8); + THROW_HR_IF(E_INVALIDARG, wstr[0] != L'#'); + + std::wstring rStr{ &wstr[1], 2 }; + std::wstring gStr{ &wstr[3], 2 }; + std::wstring bStr{ &wstr[5], 2 }; + + BYTE r = static_cast(std::stoul(rStr, nullptr, 16)); + BYTE g = static_cast(std::stoul(gStr, nullptr, 16)); + BYTE b = static_cast(std::stoul(bStr, nullptr, 16)); + + return RGB(r, g, b); +} + +// Routine Description: +// - Shorthand check if a handle value is null or invalid. +// Arguments: +// - Handle +// Return Value: +// - True if non zero and not set to invalid magic value. False otherwise. +bool Utils::IsValidHandle(const HANDLE handle) noexcept +{ + return handle != 0 && handle != INVALID_HANDLE_VALUE; +} + +// Function Description: +// - Fill the first 16 entries of a given color table with the Campbell color scheme +// Arguments: +// - table: a color table with at least 16 entries +// Return Value: +// - , throws if the table has less that 16 entries +void Utils::InitializeCampbellColorTable(gsl::span& table) +{ + THROW_HR_IF(E_INVALIDARG, table.size() < 16); + + table[0] = RGB( 12, 12, 12); + table[1] = RGB( 197, 15, 31); + table[2] = RGB( 19, 161, 14); + table[3] = RGB( 193, 156, 0); + table[4] = RGB( 0, 55, 218); + table[5] = RGB( 136, 23, 152); + table[6] = RGB( 58, 150, 221); + table[7] = RGB( 204, 204, 204); + table[8] = RGB( 118, 118, 118); + table[9] = RGB( 231, 72, 86); + table[10] = RGB( 22, 198, 12); + table[11] = RGB( 249, 241, 165); + table[12] = RGB( 59, 120, 255); + table[13] = RGB( 180, 0, 158); + table[14] = RGB( 97, 214, 214); + table[15] = RGB( 242, 242, 242); +} + +// Function Description: +// - Fill the first 255 entries of a given color table with the default values +// of a full 256-color table +// Arguments: +// - table: a color table with at least 256 entries +// Return Value: +// - , throws if the table has less that 256 entries +void Utils::Initialize256ColorTable(gsl::span& table) +{ + THROW_HR_IF(E_INVALIDARG, table.size() < 256); + + table[0] = RGB( 0x00, 0x00, 0x00); + table[1] = RGB( 0x80, 0x00, 0x00); + table[2] = RGB( 0x00, 0x80, 0x00); + table[3] = RGB( 0x80, 0x80, 0x00); + table[4] = RGB( 0x00, 0x00, 0x80); + table[5] = RGB( 0x80, 0x00, 0x80); + table[6] = RGB( 0x00, 0x80, 0x80); + table[7] = RGB( 0xc0, 0xc0, 0xc0); + table[8] = RGB( 0x80, 0x80, 0x80); + table[9] = RGB( 0xff, 0x00, 0x00); + table[10] = RGB( 0x00, 0xff, 0x00); + table[11] = RGB( 0xff, 0xff, 0x00); + table[12] = RGB( 0x00, 0x00, 0xff); + table[13] = RGB( 0xff, 0x00, 0xff); + table[14] = RGB( 0x00, 0xff, 0xff); + table[15] = RGB( 0xff, 0xff, 0xff); + table[16] = RGB( 0x00, 0x00, 0x00); + table[17] = RGB( 0x00, 0x00, 0x5f); + table[18] = RGB( 0x00, 0x00, 0x87); + table[19] = RGB( 0x00, 0x00, 0xaf); + table[20] = RGB( 0x00, 0x00, 0xd7); + table[21] = RGB( 0x00, 0x00, 0xff); + table[22] = RGB( 0x00, 0x5f, 0x00); + table[23] = RGB( 0x00, 0x5f, 0x5f); + table[24] = RGB( 0x00, 0x5f, 0x87); + table[25] = RGB( 0x00, 0x5f, 0xaf); + table[26] = RGB( 0x00, 0x5f, 0xd7); + table[27] = RGB( 0x00, 0x5f, 0xff); + table[28] = RGB( 0x00, 0x87, 0x00); + table[29] = RGB( 0x00, 0x87, 0x5f); + table[30] = RGB( 0x00, 0x87, 0x87); + table[31] = RGB( 0x00, 0x87, 0xaf); + table[32] = RGB( 0x00, 0x87, 0xd7); + table[33] = RGB( 0x00, 0x87, 0xff); + table[34] = RGB( 0x00, 0xaf, 0x00); + table[35] = RGB( 0x00, 0xaf, 0x5f); + table[36] = RGB( 0x00, 0xaf, 0x87); + table[37] = RGB( 0x00, 0xaf, 0xaf); + table[38] = RGB( 0x00, 0xaf, 0xd7); + table[39] = RGB( 0x00, 0xaf, 0xff); + table[40] = RGB( 0x00, 0xd7, 0x00); + table[41] = RGB( 0x00, 0xd7, 0x5f); + table[42] = RGB( 0x00, 0xd7, 0x87); + table[43] = RGB( 0x00, 0xd7, 0xaf); + table[44] = RGB( 0x00, 0xd7, 0xd7); + table[45] = RGB( 0x00, 0xd7, 0xff); + table[46] = RGB( 0x00, 0xff, 0x00); + table[47] = RGB( 0x00, 0xff, 0x5f); + table[48] = RGB( 0x00, 0xff, 0x87); + table[49] = RGB( 0x00, 0xff, 0xaf); + table[50] = RGB( 0x00, 0xff, 0xd7); + table[51] = RGB( 0x00, 0xff, 0xff); + table[52] = RGB( 0x5f, 0x00, 0x00); + table[53] = RGB( 0x5f, 0x00, 0x5f); + table[54] = RGB( 0x5f, 0x00, 0x87); + table[55] = RGB( 0x5f, 0x00, 0xaf); + table[56] = RGB( 0x5f, 0x00, 0xd7); + table[57] = RGB( 0x5f, 0x00, 0xff); + table[58] = RGB( 0x5f, 0x5f, 0x00); + table[59] = RGB( 0x5f, 0x5f, 0x5f); + table[60] = RGB( 0x5f, 0x5f, 0x87); + table[61] = RGB( 0x5f, 0x5f, 0xaf); + table[62] = RGB( 0x5f, 0x5f, 0xd7); + table[63] = RGB( 0x5f, 0x5f, 0xff); + table[64] = RGB( 0x5f, 0x87, 0x00); + table[65] = RGB( 0x5f, 0x87, 0x5f); + table[66] = RGB( 0x5f, 0x87, 0x87); + table[67] = RGB( 0x5f, 0x87, 0xaf); + table[68] = RGB( 0x5f, 0x87, 0xd7); + table[69] = RGB( 0x5f, 0x87, 0xff); + table[70] = RGB( 0x5f, 0xaf, 0x00); + table[71] = RGB( 0x5f, 0xaf, 0x5f); + table[72] = RGB( 0x5f, 0xaf, 0x87); + table[73] = RGB( 0x5f, 0xaf, 0xaf); + table[74] = RGB( 0x5f, 0xaf, 0xd7); + table[75] = RGB( 0x5f, 0xaf, 0xff); + table[76] = RGB( 0x5f, 0xd7, 0x00); + table[77] = RGB( 0x5f, 0xd7, 0x5f); + table[78] = RGB( 0x5f, 0xd7, 0x87); + table[79] = RGB( 0x5f, 0xd7, 0xaf); + table[80] = RGB( 0x5f, 0xd7, 0xd7); + table[81] = RGB( 0x5f, 0xd7, 0xff); + table[82] = RGB( 0x5f, 0xff, 0x00); + table[83] = RGB( 0x5f, 0xff, 0x5f); + table[84] = RGB( 0x5f, 0xff, 0x87); + table[85] = RGB( 0x5f, 0xff, 0xaf); + table[86] = RGB( 0x5f, 0xff, 0xd7); + table[87] = RGB( 0x5f, 0xff, 0xff); + table[88] = RGB( 0x87, 0x00, 0x00); + table[89] = RGB( 0x87, 0x00, 0x5f); + table[90] = RGB( 0x87, 0x00, 0x87); + table[91] = RGB( 0x87, 0x00, 0xaf); + table[92] = RGB( 0x87, 0x00, 0xd7); + table[93] = RGB( 0x87, 0x00, 0xff); + table[94] = RGB( 0x87, 0x5f, 0x00); + table[95] = RGB( 0x87, 0x5f, 0x5f); + table[96] = RGB( 0x87, 0x5f, 0x87); + table[97] = RGB( 0x87, 0x5f, 0xaf); + table[98] = RGB( 0x87, 0x5f, 0xd7); + table[99] = RGB( 0x87, 0x5f, 0xff); + table[100] = RGB(0x87, 0x87, 0x00); + table[101] = RGB(0x87, 0x87, 0x5f); + table[102] = RGB(0x87, 0x87, 0x87); + table[103] = RGB(0x87, 0x87, 0xaf); + table[104] = RGB(0x87, 0x87, 0xd7); + table[105] = RGB(0x87, 0x87, 0xff); + table[106] = RGB(0x87, 0xaf, 0x00); + table[107] = RGB(0x87, 0xaf, 0x5f); + table[108] = RGB(0x87, 0xaf, 0x87); + table[109] = RGB(0x87, 0xaf, 0xaf); + table[110] = RGB(0x87, 0xaf, 0xd7); + table[111] = RGB(0x87, 0xaf, 0xff); + table[112] = RGB(0x87, 0xd7, 0x00); + table[113] = RGB(0x87, 0xd7, 0x5f); + table[114] = RGB(0x87, 0xd7, 0x87); + table[115] = RGB(0x87, 0xd7, 0xaf); + table[116] = RGB(0x87, 0xd7, 0xd7); + table[117] = RGB(0x87, 0xd7, 0xff); + table[118] = RGB(0x87, 0xff, 0x00); + table[119] = RGB(0x87, 0xff, 0x5f); + table[120] = RGB(0x87, 0xff, 0x87); + table[121] = RGB(0x87, 0xff, 0xaf); + table[122] = RGB(0x87, 0xff, 0xd7); + table[123] = RGB(0x87, 0xff, 0xff); + table[124] = RGB(0xaf, 0x00, 0x00); + table[125] = RGB(0xaf, 0x00, 0x5f); + table[126] = RGB(0xaf, 0x00, 0x87); + table[127] = RGB(0xaf, 0x00, 0xaf); + table[128] = RGB(0xaf, 0x00, 0xd7); + table[129] = RGB(0xaf, 0x00, 0xff); + table[130] = RGB(0xaf, 0x5f, 0x00); + table[131] = RGB(0xaf, 0x5f, 0x5f); + table[132] = RGB(0xaf, 0x5f, 0x87); + table[133] = RGB(0xaf, 0x5f, 0xaf); + table[134] = RGB(0xaf, 0x5f, 0xd7); + table[135] = RGB(0xaf, 0x5f, 0xff); + table[136] = RGB(0xaf, 0x87, 0x00); + table[137] = RGB(0xaf, 0x87, 0x5f); + table[138] = RGB(0xaf, 0x87, 0x87); + table[139] = RGB(0xaf, 0x87, 0xaf); + table[140] = RGB(0xaf, 0x87, 0xd7); + table[141] = RGB(0xaf, 0x87, 0xff); + table[142] = RGB(0xaf, 0xaf, 0x00); + table[143] = RGB(0xaf, 0xaf, 0x5f); + table[144] = RGB(0xaf, 0xaf, 0x87); + table[145] = RGB(0xaf, 0xaf, 0xaf); + table[146] = RGB(0xaf, 0xaf, 0xd7); + table[147] = RGB(0xaf, 0xaf, 0xff); + table[148] = RGB(0xaf, 0xd7, 0x00); + table[149] = RGB(0xaf, 0xd7, 0x5f); + table[150] = RGB(0xaf, 0xd7, 0x87); + table[151] = RGB(0xaf, 0xd7, 0xaf); + table[152] = RGB(0xaf, 0xd7, 0xd7); + table[153] = RGB(0xaf, 0xd7, 0xff); + table[154] = RGB(0xaf, 0xff, 0x00); + table[155] = RGB(0xaf, 0xff, 0x5f); + table[156] = RGB(0xaf, 0xff, 0x87); + table[157] = RGB(0xaf, 0xff, 0xaf); + table[158] = RGB(0xaf, 0xff, 0xd7); + table[159] = RGB(0xaf, 0xff, 0xff); + table[160] = RGB(0xd7, 0x00, 0x00); + table[161] = RGB(0xd7, 0x00, 0x5f); + table[162] = RGB(0xd7, 0x00, 0x87); + table[163] = RGB(0xd7, 0x00, 0xaf); + table[164] = RGB(0xd7, 0x00, 0xd7); + table[165] = RGB(0xd7, 0x00, 0xff); + table[166] = RGB(0xd7, 0x5f, 0x00); + table[167] = RGB(0xd7, 0x5f, 0x5f); + table[168] = RGB(0xd7, 0x5f, 0x87); + table[169] = RGB(0xd7, 0x5f, 0xaf); + table[170] = RGB(0xd7, 0x5f, 0xd7); + table[171] = RGB(0xd7, 0x5f, 0xff); + table[172] = RGB(0xd7, 0x87, 0x00); + table[173] = RGB(0xd7, 0x87, 0x5f); + table[174] = RGB(0xd7, 0x87, 0x87); + table[175] = RGB(0xd7, 0x87, 0xaf); + table[176] = RGB(0xd7, 0x87, 0xd7); + table[177] = RGB(0xd7, 0x87, 0xff); + table[178] = RGB(0xdf, 0xaf, 0x00); + table[179] = RGB(0xdf, 0xaf, 0x5f); + table[180] = RGB(0xdf, 0xaf, 0x87); + table[181] = RGB(0xdf, 0xaf, 0xaf); + table[182] = RGB(0xdf, 0xaf, 0xd7); + table[183] = RGB(0xdf, 0xaf, 0xff); + table[184] = RGB(0xdf, 0xd7, 0x00); + table[185] = RGB(0xdf, 0xd7, 0x5f); + table[186] = RGB(0xdf, 0xd7, 0x87); + table[187] = RGB(0xdf, 0xd7, 0xaf); + table[188] = RGB(0xdf, 0xd7, 0xd7); + table[189] = RGB(0xdf, 0xd7, 0xff); + table[190] = RGB(0xdf, 0xff, 0x00); + table[191] = RGB(0xdf, 0xff, 0x5f); + table[192] = RGB(0xdf, 0xff, 0x87); + table[193] = RGB(0xdf, 0xff, 0xaf); + table[194] = RGB(0xdf, 0xff, 0xd7); + table[195] = RGB(0xdf, 0xff, 0xff); + table[196] = RGB(0xff, 0x00, 0x00); + table[197] = RGB(0xff, 0x00, 0x5f); + table[198] = RGB(0xff, 0x00, 0x87); + table[199] = RGB(0xff, 0x00, 0xaf); + table[200] = RGB(0xff, 0x00, 0xd7); + table[201] = RGB(0xff, 0x00, 0xff); + table[202] = RGB(0xff, 0x5f, 0x00); + table[203] = RGB(0xff, 0x5f, 0x5f); + table[204] = RGB(0xff, 0x5f, 0x87); + table[205] = RGB(0xff, 0x5f, 0xaf); + table[206] = RGB(0xff, 0x5f, 0xd7); + table[207] = RGB(0xff, 0x5f, 0xff); + table[208] = RGB(0xff, 0x87, 0x00); + table[209] = RGB(0xff, 0x87, 0x5f); + table[210] = RGB(0xff, 0x87, 0x87); + table[211] = RGB(0xff, 0x87, 0xaf); + table[212] = RGB(0xff, 0x87, 0xd7); + table[213] = RGB(0xff, 0x87, 0xff); + table[214] = RGB(0xff, 0xaf, 0x00); + table[215] = RGB(0xff, 0xaf, 0x5f); + table[216] = RGB(0xff, 0xaf, 0x87); + table[217] = RGB(0xff, 0xaf, 0xaf); + table[218] = RGB(0xff, 0xaf, 0xd7); + table[219] = RGB(0xff, 0xaf, 0xff); + table[220] = RGB(0xff, 0xd7, 0x00); + table[221] = RGB(0xff, 0xd7, 0x5f); + table[222] = RGB(0xff, 0xd7, 0x87); + table[223] = RGB(0xff, 0xd7, 0xaf); + table[224] = RGB(0xff, 0xd7, 0xd7); + table[225] = RGB(0xff, 0xd7, 0xff); + table[226] = RGB(0xff, 0xff, 0x00); + table[227] = RGB(0xff, 0xff, 0x5f); + table[228] = RGB(0xff, 0xff, 0x87); + table[229] = RGB(0xff, 0xff, 0xaf); + table[230] = RGB(0xff, 0xff, 0xd7); + table[231] = RGB(0xff, 0xff, 0xff); + table[232] = RGB(0x08, 0x08, 0x08); + table[233] = RGB(0x12, 0x12, 0x12); + table[234] = RGB(0x1c, 0x1c, 0x1c); + table[235] = RGB(0x26, 0x26, 0x26); + table[236] = RGB(0x30, 0x30, 0x30); + table[237] = RGB(0x3a, 0x3a, 0x3a); + table[238] = RGB(0x44, 0x44, 0x44); + table[239] = RGB(0x4e, 0x4e, 0x4e); + table[240] = RGB(0x58, 0x58, 0x58); + table[241] = RGB(0x62, 0x62, 0x62); + table[242] = RGB(0x6c, 0x6c, 0x6c); + table[243] = RGB(0x76, 0x76, 0x76); + table[244] = RGB(0x80, 0x80, 0x80); + table[245] = RGB(0x8a, 0x8a, 0x8a); + table[246] = RGB(0x94, 0x94, 0x94); + table[247] = RGB(0x9e, 0x9e, 0x9e); + table[248] = RGB(0xa8, 0xa8, 0xa8); + table[249] = RGB(0xb2, 0xb2, 0xb2); + table[250] = RGB(0xbc, 0xbc, 0xbc); + table[251] = RGB(0xc6, 0xc6, 0xc6); + table[252] = RGB(0xd0, 0xd0, 0xd0); + table[253] = RGB(0xda, 0xda, 0xda); + table[254] = RGB(0xe4, 0xe4, 0xe4); + table[255] = RGB(0xee, 0xee, 0xee); + +} + +// Function Description: +// - Fill the alpha byte of the colors in a given color table with the given value. +// Arguments: +// - table: a color table +// - newAlpha: the new value to use as the alpha for all the entries in that table. +// Return Value: +// - +void Utils::SetColorTableAlpha(gsl::span& table, const BYTE newAlpha) +{ + const auto shiftedAlpha = newAlpha << 24; + for( auto& color : table) + { + WI_UpdateFlagsInMask(color, 0xff000000, shiftedAlpha); + } +} diff --git a/src/types/viewport.cpp b/src/types/viewport.cpp new file mode 100644 index 000000000..8f076afc9 --- /dev/null +++ b/src/types/viewport.cpp @@ -0,0 +1,1051 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "inc/Viewport.hpp" + +using namespace Microsoft::Console::Types; + +Viewport::Viewport(const SMALL_RECT sr) noexcept : + _sr(sr) +{ + +} + +Viewport::Viewport(const Viewport& other) noexcept : + _sr(other._sr) +{ + +} + +Viewport Viewport::Empty() noexcept +{ + return Viewport({ 0, 0, -1, -1 }); +} + +Viewport Viewport::FromInclusive(const SMALL_RECT sr) noexcept +{ + return Viewport(sr); +} + +Viewport Viewport::FromExclusive(const SMALL_RECT sr) noexcept +{ + SMALL_RECT _sr = sr; + _sr.Bottom -= 1; + _sr.Right -= 1; + return Viewport::FromInclusive(_sr); +} + +// Function Description: +// - Creates a new Viewport at the given origin, with the given dimensions. +// Arguments: +// - origin: The origin of the new Viewport. Becomes the Viewport's Left, Top +// - width: The width of the new viewport +// - height: The height of the new viewport +// Return Value: +// - a new Viewport at the given origin, with the given dimensions. +Viewport Viewport::FromDimensions(const COORD origin, + const short width, + const short height) noexcept +{ + return Viewport::FromExclusive({ origin.X, origin.Y, + origin.X + width, origin.Y + height }); +} + +// Function Description: +// - Creates a new Viewport at the given origin, with the given dimensions. +// Arguments: +// - origin: The origin of the new Viewport. Becomes the Viewport's Left, Top +// - dimensions: A coordinate containing the width and height of the new viewport +// in the x and y coordinates respectively. +// Return Value: +// - a new Viewport at the given origin, with the given dimensions. +Viewport Viewport::FromDimensions(const COORD origin, + const COORD dimensions) noexcept +{ + return Viewport::FromExclusive({ origin.X, origin.Y, + origin.X + dimensions.X, origin.Y + dimensions.Y }); +} + +// Function Description: +// - Creates a new Viewport at the origin, with the given dimensions. +// Arguments: +// - dimensions: A coordinate containing the width and height of the new viewport +// in the x and y coordinates respectively. +// Return Value: +// - a new Viewport at the origin, with the given dimensions. +Viewport Viewport::FromDimensions(const COORD dimensions) noexcept +{ + return Viewport::FromDimensions({ 0 }, dimensions); +} + +// Method Description: +// - Creates a Viewport equivalent to a 1x1 rectangle at the given coordinate. +// Arguments: +// - origin: origin of the rectangle to create. +// Return Value: +// - a 1x1 Viewport at the given coordinate +Viewport Viewport::FromCoord(const COORD origin) noexcept +{ + return Viewport::FromInclusive({ origin.X, origin.Y, + origin.X, origin.Y }); +} + +SHORT Viewport::Left() const noexcept +{ + return _sr.Left; +} + +SHORT Viewport::RightInclusive() const noexcept +{ + return _sr.Right; +} + +SHORT Viewport::RightExclusive() const noexcept +{ + return _sr.Right + 1; +} + +SHORT Viewport::Top() const noexcept +{ + return _sr.Top; +} + +SHORT Viewport::BottomInclusive() const noexcept +{ + return _sr.Bottom; +} + +SHORT Viewport::BottomExclusive() const noexcept +{ + return _sr.Bottom + 1; +} + +SHORT Viewport::Height() const noexcept +{ + return BottomExclusive() - Top(); +} + +SHORT Viewport::Width() const noexcept +{ + return RightExclusive() - Left(); +} + +// Method Description: +// - Get a coord representing the origin of this viewport. +// Arguments: +// - +// Return Value: +// - the coordinates of this viewport's origin. +COORD Viewport::Origin() const noexcept +{ + return { Left(), Top() }; +} + +// Method Description: +// - Get a coord representing the dimensions of this viewport. +// Arguments: +// - +// Return Value: +// - the dimensions of this viewport. +COORD Viewport::Dimensions() const noexcept +{ + return { Width(), Height() }; +} + +// Method Description: +// - Determines if the given viewport fits within this viewport. +// Arguments: +// - other - The viewport to fit inside this one +// Return Value: +// - True if it fits. False otherwise. +bool Viewport::IsInBounds(const Viewport& other) const noexcept +{ + return other.Left() >= Left() && other.Left() <= RightInclusive() && + other.RightInclusive() >= Left() && other.RightInclusive() <= RightInclusive() && + other.Top() >= Top() && other.Top() <= other.BottomInclusive() && + other.BottomInclusive() >= Top() && other.BottomInclusive() <= BottomInclusive(); +} + +// Method Description: +// - Determines if the given coordinate position lies within this viewport. +// Arguments: +// - pos - Coordinate position +// Return Value: +// - True if it lies inside the viewport. False otherwise. +bool Viewport::IsInBounds(const COORD& pos) const noexcept +{ + return pos.X >= Left() && pos.X < RightExclusive() && + pos.Y >= Top() && pos.Y < BottomExclusive(); +} + +// Method Description: +// - Clamps a coordinate position into the inside of this viewport. +// Arguments: +// - pos - coordinate to update/clamp +// Return Value: +// - +void Viewport::Clamp(COORD& pos) const +{ + THROW_HR_IF(E_NOT_VALID_STATE, !IsValid()); // we can't clamp to an invalid viewport. + + pos.X = std::clamp(pos.X, Left(), RightInclusive()); + pos.Y = std::clamp(pos.Y, Top(), BottomInclusive()); +} + +// Method Description: +// - Clamps a viewport into the inside of this viewport. +// Arguments: +// - other - Viewport to clamp to the inside of this viewport +// Return Value: +// - Clamped viewport +Viewport Viewport::Clamp(const Viewport& other) const +{ + auto clampMe = other.ToInclusive(); + + clampMe.Left = std::clamp(clampMe.Left, Left(), RightInclusive()); + clampMe.Right = std::clamp(clampMe.Right, Left(), RightInclusive()); + clampMe.Top = std::clamp(clampMe.Top, Top(), BottomInclusive()); + clampMe.Bottom = std::clamp(clampMe.Bottom, Top(), BottomInclusive()); + + return Viewport::FromInclusive(clampMe); +} + +// Method Description: +// - Moves the coordinate given by the number of positions and +// in the direction given (repeated increment or decrement) +// Arguments: +// - move - Magnitude and direction of the move +// - pos - The coordinate position to adjust +// Return Value: +// - True if we successfully moved the requested distance. False if we had to stop early. +// - If False, we will restore the original position to the given coordinate. +bool Viewport::MoveInBounds(const ptrdiff_t move, COORD& pos) const noexcept +{ + const auto backup = pos; + bool success = true; // If nothing happens, we're still successful (e.g. add = 0) + + for (int i = 0; i < move; i++) + { + success = IncrementInBounds(pos); + + // If an operation fails, break. + if (!success) + { + break; + } + } + + for (int i = 0; i > move; i--) + { + success = DecrementInBounds(pos); + + // If an operation fails, break. + if (!success) + { + break; + } + } + + // If any operation failed, revert to backed up state. + if (!success) + { + pos = backup; + } + + return success; +} + +// Method Description: +// - Increments the given coordinate within the bounds of this viewport. +// Arguments: +// - pos - Coordinate position that will be incremented, if it can be. +// Return Value: +// - True if it could be incremented. False if it would move outside. +bool Viewport::IncrementInBounds(COORD& pos) const noexcept +{ + return WalkInBounds(pos, { XWalk::LeftToRight, YWalk::TopToBottom }); +} + +// Method Description: +// - Increments the given coordinate within the bounds of this viewport +// rotating around to the top when reaching the bottom right corner. +// Arguments: +// - pos - Coordinate position that will be incremented. +// Return Value: +// - True if it could be incremented inside the viewport. +// - False if it rolled over from the bottom right corner back to the top. +bool Viewport::IncrementInBoundsCircular(COORD& pos) const noexcept +{ + return WalkInBoundsCircular(pos, { XWalk::LeftToRight, YWalk::TopToBottom }); +} + +// Method Description: +// - Decrements the given coordinate within the bounds of this viewport. +// Arguments: +// - pos - Coordinate position that will be incremented, if it can be. +// Return Value: +// - True if it could be incremented. False if it would move outside. +bool Viewport::DecrementInBounds(COORD& pos) const noexcept +{ + return WalkInBounds(pos, { XWalk::RightToLeft, YWalk::BottomToTop }); +} + +// Method Description: +// - Decrements the given coordinate within the bounds of this viewport +// rotating around to the bottom right when reaching the top left corner. +// Arguments: +// - pos - Coordinate position that will be decremented. +// Return Value: +// - True if it could be decremented inside the viewport. +// - False if it rolled over from the top left corner back to the bottom right. +bool Viewport::DecrementInBoundsCircular(COORD& pos) const noexcept +{ + return WalkInBoundsCircular(pos, { XWalk::RightToLeft, YWalk::BottomToTop }); +} + +// Routine Description: +// - Compares two coordinate positions to determine whether they're the same, left, or right within the given buffer size +// Arguments: +// - first- The first coordinate position +// - second - The second coordinate position +// Return Value: +// - Negative if First is to the left of the Second. +// - 0 if First and Second are the same coordinate. +// - Positive if First is to the right of the Second. +// - This is so you can do s_CompareCoords(first, second) <= 0 for "first is left or the same as second". +// (the < looks like a left arrow :D) +// - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom) +int Viewport::CompareInBounds(const COORD& first, const COORD& second) const noexcept +{ + // Assert that our coordinates are within the expected boundaries + FAIL_FAST_IF(!IsInBounds(first)); + FAIL_FAST_IF(!IsInBounds(second)); + + // First set the distance vertically + // If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160. + // For the same row, it'll be 0 rows * 80 character width = 0 difference. + int retVal = (first.Y - second.Y) * Width(); + + // Now adjust for horizontal differences + // If first is in position 15 and second is in position 30, first is -15 left in relation to 30. + retVal += (first.X - second.X); + + // Further notes: + // If we already moved behind one row, this will help correct for when first is right of second. + // For example, with row 4, col 79 and row 5, col 0 as first and second respectively, the distance is -1. + // Assume the row width is 80. + // Step one will set the retVal as -80 as first is one row behind the second. + // Step two will then see that first is 79 - 0 = +79 right of second and add 79 + // The total is -80 + 79 = -1. + return retVal; +} + +// Method Description: +// - Walks the given coordinate within the bounds of this viewport in the specified +// X and Y directions. +// Arguments: +// - pos - Coordinate position that will be adjusted, if it can be. +// - dir - Walking direction specifying which direction to go when reaching the end of a row/column +// Return Value: +// - True if it could be adjusted as specified and remain in bounds. False if it would move outside. +bool Viewport::WalkInBounds(COORD& pos, const WalkDir dir) const noexcept +{ + auto copy = pos; + if (WalkInBoundsCircular(copy, dir)) + { + pos = copy; + return true; + } + else + { + return false; + } +} + +// Method Description: +// - Walks the given coordinate within the bounds of this viewport +// rotating around to the opposite corner when reaching the final corner +// in the specified direction. +// Arguments: +// - pos - Coordinate position that will be adjusted. +// - dir - Walking direction specifying which direction to go when reaching the end of a row/column +// Return Value: +// - True if it could be adjusted inside the viewport. +// - False if it rolled over from the final corner back to the initial corner +// for the specified walk direction. +bool Viewport::WalkInBoundsCircular(COORD& pos, const WalkDir dir) const noexcept +{ + // Assert that the position given fits inside this viewport. + FAIL_FAST_IF(!IsInBounds(pos)); + + if (dir.x == XWalk::LeftToRight) + { + if (pos.X == RightInclusive()) + { + pos.X = Left(); + + if (dir.y == YWalk::TopToBottom) + { + pos.Y++; + if (pos.Y > BottomInclusive()) + { + pos.Y = Top(); + return false; + } + } + else + { + pos.Y--; + if (pos.Y < Top()) + { + pos.Y = BottomInclusive(); + return false; + } + } + } + else + { + pos.X++; + } + } + else + { + if (pos.X == Left()) + { + pos.X = RightInclusive(); + + if (dir.y == YWalk::TopToBottom) + { + pos.Y++; + if (pos.Y > BottomInclusive()) + { + pos.Y = Top(); + return false; + } + } + else + { + pos.Y--; + if (pos.Y < Top()) + { + pos.Y = BottomInclusive(); + return false; + } + } + } + else + { + pos.X--; + } + } + + return true; +} + +// Routine Description: +// - If walking through a viewport, one might want to know the origin +// for the direction walking. +// - For example, for walking up and to the left (bottom right corner +// to top left corner), the origin would start at the bottom right. +// Arguments: +// - dir - The direction one intends to walk through the viewport +// Return Value: +// - The origin for the walk to reach every position without circling +// if using this same viewport with the `WalkInBounds` methods. +COORD Viewport::GetWalkOrigin(const WalkDir dir) const noexcept +{ + COORD origin; + origin.X = dir.x == XWalk::LeftToRight ? Left() : RightInclusive(); + origin.Y = dir.y == YWalk::TopToBottom ? Top() : BottomInclusive(); + return origin; +} + +// Routine Description: +// - Given two viewports that will be used for copying data from one to the other (source, target), +// determine which direction you will have to walk through them to ensure that an overlapped copy +// won't erase data in the source that hasn't yet been read and copied into the target at the same +// coordinate offset position from their respective origins. +// - Note: See elaborate ASCII-art comment inside the body of this function for more details on how/why this works. +// Arguments: +// - source - The viewport representing the region that will be copied from +// - target - The viewport representing the region that will be copied to +// Return Value: +// - The direction to walk through both viewports from the walk origins to touch every cell and not +// accidentally overwrite something that hasn't been read yet. (use with GetWalkOrigin and WalkInBounds) +Viewport::WalkDir Viewport::DetermineWalkDirection(const Viewport& source, const Viewport& target) noexcept +{ + // We can determine which direction we need to walk based on solely the origins of the two rectangles. + // I'll use a few examples to prove the situation. + // + // For the cardinal directions, let's start with this sample: + // + // source target + // origin 0,0 origin 4,0 + // | | + // v V + // +--source-----+--target--------- +--source-----+--target--------- + // | A B C D | E | 1 2 3 4 | becomes | A B C D | A | B C D E | + // | F G H I | J | 5 6 7 8 | =========> | F G H I | F | G H I J | + // | K L M N | O | 9 $ % @ | | K L M N | K | L M N O | + // -------------------------------- -------------------------------- + // + // The source and target overlap in the 5th column (X=4). + // To ensure that we don't accidentally write over the source + // data before we copy it into the target, we want to start by + // reading that column (a.k.a. writing to the farthest away column + // of the target). + // + // This means we want to copy from right to left. + // Top to bottom and bottom to top don't really matter for this since it's + // a cardinal direction shift. + // + // If we do the right most column first as so... + // + // +--source-----+--target--------- +--source-----+--target--------- + // | A B C D | E | 1 2 3 4 | step 1 | A B C D | E | 1 2 3 E | + // | F G H I | J | 5 6 7 8 | =========> | F G H I | J | 5 6 7 J | + // | K L M N | O | 9 $ % @ | | K L M N | O | 9 $ % O | + // -------------------------------- -------------------------------- + // + // ... then we can see that the EJO column is safely copied first out of the way and + // can be overwritten on subsequent steps without losing anything. + // The rest of the columns aren't overlapping, so they'll be fine. + // + // But we extrapolate this logic to follow for rectangles that overlap more columns, up + // to and including only leaving one column not overlapped... + // + // source target + // origin origin + // 0,0 / 1,0 + // | / + // v v + // +----+------target- +----+------target- + // | A | B C D | E | becomes | A | A B C | D | + // | F | G H I | J | =========> | F | F G H | I | + // | K | L M N | O | | K | K L M | N | + // ---source---------- ---source---------- + // + // ... will still be OK following the same Right-To-Left rule as the first move. + // + // +----+------target- +----+------target- + // | A | B C D | E | step 1 | A | B C D | D | + // | F | G H I | J | =========> | F | G H I | I | + // | K | L M N | O | | K | L M N | N | + // ---source---------- ---source---------- + // + // The DIN column from the source was moved to the target as the right most column + // of both rectangles. Now it is safe to iterate to the second column from the right + // and proceed with moving CHM on top of the source DIN as it was already moved. + // + // +----+------target- +----+------target- + // | A | B C D | E | step 2 | A | B C C | D | + // | F | G H I | J | =========> | F | G H H | I | + // | K | L M N | O | | K | L M M | N | + // ---source---------- ---source---------- + // + // Continue walking right to left (an exercise left to the reader,) and we never lose + // any source data before it reaches the target with the Right To Left pattern. + // + // We notice that the target origin was Right of the source origin in this circumstance, + // (target origin X is > source origin X) + // so it is asserted that targets right of sources means that we should "walk" right to left. + // + // Reviewing the above, it doesn't appear to matter if we go Top to Bottom or Bottom to Top, + // so the conclusion is drawn that it doesn't matter as long as the source and target origin + // Y values are the same. + // + // Also, extrapolating this cardinal direction move to the other 3 cardinal directions, + // it should follow that they would follow the same rules. + // That is, a target left of a source, or a Westbound move, opposite of the above Eastbound move, + // should be "walked" left to right. + // (target origin X is < source origin X) + // + // We haven't given the sample yet that Northbound and Southbound moves are the same, but we + // could reason that the same logic applies and the conclusion would be a Northbound move + // would walk from the target toward the source again... a.k.a. Top to Bottom. + // (target origin Y is < source origin Y) + // Then the Southbound move would be the opposite, Bottom to Top. + // (target origin Y is > source origin Y) + // + // To confirm, let's try one more example but moving both at once in an ordinal direction Northeast. + // + // target + // origin 1, 0 + // | + // v + // +----target-- +----target-- + // source A | B C | A | D E | + // origin-->+------------ | becomes +------------ | + // 0, 1 | D | E | F | =========> | D | G | H | + // | ------------- | ------------- + // | G H | I | G H | I + // --source----- --source----- + // + // Following our supposed rules from above, we have... + // Source Origin X = 0, Y = 1 + // Target Origin X = 1, Y = 0 + // + // Source Origin X < Target Origin X which means Right to Left + // Source Origin Y > Target Origin Y which means Top to Bottom + // + // So the first thing we should copy is the Top and Right most + // value from source to target. + // + // +----target-- +----target-- + // A | B C | A | B E | + // +------------ | step 1 +------------ | + // | D | E | F | =========> | D | E | F | + // | ------------- | ------------- + // | G H | I | G H | I + // --source----- --source----- + // + // And look. The E which was in the overlapping part of the source + // is the first thing copied out of the way and we're safe to copy the rest. + // + // We assume that this pattern then applies to all ordinal directions as well + // and it appears our rules hold. + // + // We've covered all cardinal and ordinal directions... all that is left is two + // rectangles of the same size and origin... and in that case, it doesn't matter + // as nothing is moving and therefore can't be covered up or lost. + // + // Therefore, we will codify our inequalities below as determining the walk direction + // for a given source and target viewport and use the helper `GetWalkOrigin` + // to return the place that we should start walking from when the copy commences. + + const auto sourceOrigin = source.Origin(); + const auto targetOrigin = target.Origin(); + + return Viewport::WalkDir{ targetOrigin.X < sourceOrigin.X ? Viewport::XWalk::LeftToRight : Viewport::XWalk::RightToLeft, + targetOrigin.Y < sourceOrigin.Y ? Viewport::YWalk::TopToBottom : Viewport::YWalk::BottomToTop }; +} + +// Method Description: +// - Clips the input rectangle to our bounds. Assumes that the input rectangle +//is an exclusive rectangle. +// Arguments: +// - psr: a pointer to an exclusive rect to clip. +// Return Value: +// - true iff the clipped rectangle is valid (with a width and height both >0) +bool Viewport::TrimToViewport(_Inout_ SMALL_RECT* const psr) const noexcept +{ + psr->Left = std::max(psr->Left, Left()); + psr->Right = std::min(psr->Right, RightExclusive()); + psr->Top = std::max(psr->Top, Top()); + psr->Bottom = std::min(psr->Bottom, BottomExclusive()); + + return psr->Left < psr->Right && psr->Top < psr->Bottom; +} + +// Method Description: +// - Translates the input SMALL_RECT out of our coordinate space, whose origin is +// at (this.Left, this.Right) +// Arguments: +// - psr: a pointer to a SMALL_RECT the translate into our coordinate space. +// Return Value: +// - +void Viewport::ConvertToOrigin(_Inout_ SMALL_RECT* const psr) const noexcept +{ + const short dx = Left(); + const short dy = Top(); + psr->Left -= dx; + psr->Right -= dx; + psr->Top -= dy; + psr->Bottom -= dy; +} + +// Method Description: +// - Translates the input coordinate out of our coordinate space, whose origin is +// at (this.Left, this.Right) +// Arguments: +// - pcoord: a pointer to a coordinate the translate into our coordinate space. +// Return Value: +// - +void Viewport::ConvertToOrigin(_Inout_ COORD* const pcoord) const noexcept +{ + pcoord->X -= Left(); + pcoord->Y -= Top(); +} + +// Method Description: +// - Translates the input SMALL_RECT to our coordinate space, whose origin is +// at (this.Left, this.Right) +// Arguments: +// - psr: a pointer to a SMALL_RECT the translate into our coordinate space. +// Return Value: +// - +void Viewport::ConvertFromOrigin(_Inout_ SMALL_RECT* const psr) const noexcept +{ + const short dx = Left(); + const short dy = Top(); + psr->Left += dx; + psr->Right += dx; + psr->Top += dy; + psr->Bottom += dy; +} + +// Method Description: +// - Translates the input coordinate to our coordinate space, whose origin is +// at (this.Left, this.Right) +// Arguments: +// - pcoord: a pointer to a coordinate the translate into our coordinate space. +// Return Value: +// - +void Viewport::ConvertFromOrigin(_Inout_ COORD* const pcoord) const noexcept +{ + pcoord->X += Left(); + pcoord->Y += Top(); +} + +// Method Description: +// - Returns an exclusive SMALL_RECT equivalent to this viewport. +// Arguments: +// - +// Return Value: +// - an exclusive SMALL_RECT equivalent to this viewport. +SMALL_RECT Viewport::ToExclusive() const noexcept +{ + return { Left(), Top(), RightExclusive(), BottomExclusive() }; +} + +// Method Description: +// - Returns an exclusive RECT equivalent to this viewport. +// Arguments: +// - +// Return Value: +// - an exclusive RECT equivalent to this viewport. +RECT Viewport::ToRect() const noexcept +{ + RECT r{0}; + r.left = Left(); + r.top = Top(); + r.right = RightExclusive(); + r.bottom = BottomExclusive(); + return r; +} + +// Method Description: +// - Returns an inclusive SMALL_RECT equivalent to this viewport. +// Arguments: +// - +// Return Value: +// - an inclusive SMALL_RECT equivalent to this viewport. +SMALL_RECT Viewport::ToInclusive() const noexcept +{ + return { Left(), Top(), RightInclusive(), BottomInclusive() }; +} + +// Method Description: +// - Returns a new viewport representing this viewport at the origin. +// For example: +// this = {6, 5, 11, 11} (w, h = 5, 6) +// result = {0, 0, 5, 6} (w, h = 5, 6) +// Arguments: +// - +// Return Value: +// - a new viewport with the same dimensions as this viewport with top, left = 0, 0 +Viewport Viewport::ToOrigin() const noexcept +{ + Viewport returnVal = *this; + ConvertToOrigin(&returnVal._sr); + return returnVal; +} + +// Method Description: +// - Translates another viewport to this viewport's coordinate space. +// For example: +// this = {5, 6, 7, 8} (w,h = 1, 1) +// other = {6, 5, 11, 11} (w, h = 5, 6) +// result = {1, -1, 6, 5} (w, h = 5, 6) +// Arguments: +// - other: the viewport to convert to this coordinate space +// Return Value: +// - the input viewport in a the coordinate space with origin at (this.Top, this.Left) +[[nodiscard]] +Viewport Viewport::ConvertToOrigin(const Viewport& other) const noexcept +{ + Viewport returnVal = other; + ConvertToOrigin(&returnVal._sr); + return returnVal; +} + +// Method Description: +// - Translates another viewport out of this viewport's coordinate space. +// For example: +// this = {5, 6, 7, 8} (w,h = 1, 1) +// other = {0, 0, 5, 6} (w, h = 5, 6) +// result = {5, 6, 10, 12} (w, h = 5, 6) +// Arguments: +// - other: the viewport to convert out of this coordinate space +// Return Value: +// - the input viewport in a the coordinate space with origin at (0, 0) +[[nodiscard]] +Viewport Viewport::ConvertFromOrigin(const Viewport& other) const noexcept +{ + Viewport returnVal = other; + ConvertFromOrigin(&returnVal._sr); + return returnVal; +} + +// Function Description: +// - Translates a given Viewport by the specified coord amount. Does the +// addition with safemath. +// Arguments: +// - original: The initial viewport to translate. Is unmodified by this operation. +// - delta: The amount to translate the original rect by, in both the x and y coordinates. +// Return Value: +// - The offset viewport by the given delta. +// - NOTE: Throws on safe math failure. +[[nodiscard]] +Viewport Viewport::Offset(const Viewport& original, const COORD delta) +{ + // If there's no delta, do nothing. + if (delta.X == 0 && delta.Y == 0) + { + return original; + } + + SHORT newTop = original._sr.Top; + SHORT newLeft = original._sr.Left; + SHORT newRight = original._sr.Right; + SHORT newBottom = original._sr.Bottom; + + THROW_IF_FAILED(ShortAdd(newLeft, delta.X, &newLeft)); + THROW_IF_FAILED(ShortAdd(newRight, delta.X, &newRight)); + THROW_IF_FAILED(ShortAdd(newTop, delta.Y, &newTop)); + THROW_IF_FAILED(ShortAdd(newBottom, delta.Y, &newBottom)); + + return Viewport({ newLeft, newTop, newRight, newBottom }); +} + +// Function Description: +// - Returns a viewport created from the union of both the parameter viewports. +// The result extends from the leftmost extent of either rect to the +// rightmost extent of either rect, and from the lowest top value to the +// highest bottom value, and everything in between. +// Arguments: +// - lhs: one of the viewports to or together +// - rhs: the other viewport to or together +// Return Value: +// - a Viewport representing the union of the other two viewports. +[[nodiscard]] +Viewport Viewport::Union(const Viewport& lhs, const Viewport& rhs) noexcept +{ + const auto leftValid = lhs.IsValid(); + const auto rightValid = rhs.IsValid(); + + // If neither are valid, return empty. + if (!leftValid && !rightValid) + { + return Viewport::Empty(); + } + // If left isn't valid, then return just the right. + else if (!leftValid) + { + return rhs; + } + // If right isn't valid, then return just the left. + else if (!rightValid) + { + return lhs; + } + // Otherwise, everything is valid. Find the actual union. + else + { + const auto left = std::min(lhs.Left(), rhs.Left()); + const auto top = std::min(lhs.Top(), rhs.Top()); + const auto right = std::max(lhs.RightInclusive(), rhs.RightInclusive()); + const auto bottom = std::max(lhs.BottomInclusive(), rhs.BottomInclusive()); + + return Viewport({ left, top, right, bottom }); + } +} + +// Function Description: +// - Creates a viewport from the intersection fo both the parameter viewports. +// The result will be the smallest area that fits within both rectangles. +// Arguments: +// - lhs: one of the viewports to intersect +// - rhs: the other viepwort to intersect +// Return Value: +// - a Viewport representing the intersection of the other two, or an empty viewport if there's no intersection. +[[nodiscard]] +Viewport Viewport::Intersect(const Viewport& lhs, const Viewport& rhs) noexcept +{ + const auto left = std::max(lhs.Left(), rhs.Left()); + const auto top = std::max(lhs.Top(), rhs.Top()); + const auto right = std::min(lhs.RightInclusive(), rhs.RightInclusive()); + const auto bottom = std::min(lhs.BottomInclusive(), rhs.BottomInclusive()); + + const Viewport intersection({ left, top, right, bottom }); + + // What we calculated with min/max might not actually represent a valid viewport that has area. + // If we calculated something that is nonsense (invalid), then just return the empty viewport. + if (!intersection.IsValid()) + { + return Viewport::Empty(); + } + else + { + // If it was valid, give back whatever we created. + return intersection; + } +} + +// Routine Description: +// - Returns a list of Viewports representing the area from the `original` Viewport that was NOT a part of +// the given `removeMe` Viewport. It can require multiple Viewports to represent the remaining +// area as a "region". +// Arguments: +// - original - The overall viewport to start from. +// - removeMe - The space that should be taken out of the main Viewport. +// Return Value: +// - Array of 4 Viewports representing non-overlapping segments of the remaining area +// that was covered by `main` before the regional area of `removeMe` was taken out. +// - You must check that each viewport .IsValid() before using it. +[[nodiscard]] +SomeViewports Viewport::Subtract(const Viewport& original, const Viewport& removeMe) noexcept +{ + SomeViewports result; + + // We could have up to four rectangles describing the area resulting when you take removeMe out of main. + // Find the intersection of the two so we know which bits of removeMe are actually applicable + // to the original rectangle for subtraction purposes. + const auto intersection = Viewport::Intersect(original, removeMe); + + // If there's no intersection, there's nothing to remove. + if (!intersection.IsValid()) + { + // Just put the original rectangle into the results and return early. + result.viewports.at(result.used++) = original; + } + else + { + // Generate our potential four viewports that represent the region of the original that falls outside of the remove area. + // We will bias toward generating wide rectangles over tall rectangles (if possible) so that optimizations that apply + // to manipulating an entire row at once can be realized by other parts of the console code. (i.e. Run Length Encoding) + // In the following examples, the found remaining regions are represented by: + // T = Top B = Bottom L = Left R = Right + // + // 4 Sides but Identical: + // |---------original---------| |---------original---------| + // | | | | + // | | | | + // | | | | + // | | ======> | intersect | ======> early return of 0x0 Viewport + // | | | | at Original's origin + // | | | | + // | | | | + // |---------removeMe---------| |--------------------------| + // + // 4 Sides: + // |---------original---------| |---------original---------| |--------------------------| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | |---------| | | |---------| | |LLLLLLLL|---------|RRRRRRR| + // | |removeMe | | ======> | |intersect| | ======> |LLLLLLLL| |RRRRRRR| + // | |---------| | | |---------| | |LLLLLLLL|---------|RRRRRRR| + // | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB| + // | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB| + // |--------------------------| |--------------------------| |--------------------------| + // + // 3 Sides: + // |---------original---------| |---------original---------| |--------------------------| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | |--------------------| | |-----------------| |LLLLLLLL|-----------------| + // | |removeMe | ======> | |intersect | ======> |LLLLLLLL| | + // | |--------------------| | |-----------------| |LLLLLLLL|-----------------| + // | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB| + // | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB| + // |--------------------------| |--------------------------| |--------------------------| + // + // 2 Sides: + // |---------original---------| |---------original---------| |--------------------------| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | |--------------------| | |-----------------| |LLLLLLLL|-----------------| + // | |removeMe | ======> | |intersect | ======> |LLLLLLLL| | + // | | | | | | |LLLLLLLL| | + // | | | | | | |LLLLLLLL| | + // | | | | | | |LLLLLLLL| | + // |--------| | |--------------------------| |--------------------------| + // | | + // |--------------------| + // + // 1 Side: + // |---------original---------| |---------original---------| |--------------------------| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT| + // |-----------------------------| |--------------------------| |--------------------------| + // | removeMe | ======> | intersect | ======> | | + // | | | | | | + // | | | | | | + // | | | | | | + // | | |--------------------------| |--------------------------| + // | | + // |-----------------------------| + // + // 0 Sides: + // |---------original---------| |---------original---------| + // | | | | + // | | | | + // | | | | + // | | ======> | | ======> early return of Original + // | | | | + // | | | | + // | | | | + // |--------------------------| |--------------------------| + // + // + // |---------------| + // | removeMe | + // |---------------| + + if (original == intersection) + { + result.viewports.at(result.used++) = Viewport::FromDimensions(original.Origin(), { 0, 0 }); + } + else + { + // We generate these rectangles by the original and intersection points, but some of them might be empty when the intersection + // lines up with the edge of the original. That's OK. That just means that the subtraction didn't leave anything behind. + // We will filter those out below when adding them to the result. + const auto top = Viewport({ original.Left(), original.Top(), original.RightInclusive(), intersection.Top() - 1 }); + const auto bottom = Viewport({ original.Left(), intersection.BottomExclusive(), original.RightInclusive(), original.BottomInclusive() }); + const auto left = Viewport({ original.Left(), intersection.Top(), intersection.Left() - 1, intersection.BottomInclusive() }); + const auto right = Viewport({ intersection.RightExclusive(), intersection.Top(), original.RightInclusive(), intersection.BottomInclusive() }); + + if (top.IsValid()) + { + result.viewports.at(result.used++) = top; + } + + if (bottom.IsValid()) + { + result.viewports.at(result.used++) = bottom; + } + + if (left.IsValid()) + { + result.viewports.at(result.used++) = left; + } + + if (right.IsValid()) + { + result.viewports.at(result.used++) = right; + } + } + } + + return result; +} + +// Method Description: +// - Returns true if the rectangle described by this Viewport has internal space +// - i.e. it has a positive, non-zero height and width. +bool Viewport::IsValid() const noexcept +{ + return Height() > 0 && Width() > 0; +} diff --git a/src/unit.tests.x64.runsettings b/src/unit.tests.x64.runsettings new file mode 100644 index 000000000..eb936b53d --- /dev/null +++ b/src/unit.tests.x64.runsettings @@ -0,0 +1,146 @@ + + + + + + .\TestResults + + + x64 + + + Framework45 + + + + + + + + + + + + + + + + + .*\.dll$ + .*\.exe$ + + + .*CPPUnitTestFramework.* + + + + + + + + ^Fabrikam\.UnitTest\..* + ^std::.* + ^ATL::.* + .*::__GetTestMethodInfo.* + ^Microsoft::VisualStudio::CppCodeCoverageFramework::.* + ^Microsoft::VisualStudio::CppUnitTestFramework::.* + ^WEX::.* + ^wil::.* + ^wistd::.* + ^.*Test.*::.* + ^.*::.*TAEF.* + + + + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.Runtime\.CompilerServices.CompilerGeneratedAttribute$ + ^System\.CodeDom\.Compiler.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis.ExcludeFromCodeCoverageAttribute$ + + + + + + + .*\\atlmfc\\.* + .*\\vctools\\.* + .*\\public\\sdk\\.* + .*\\microsoft sdks\\.* + .*\\windows kits\\.* + .*\\um\\.* + .*\\ucrt\\.* + .*\\vc\\include\\.* + .*\\ft_.*\\.* + .*\\ut_.*\\.* + + + + + + + + + + + ^B77A5C561934E089$ + ^B03F5F7F11D50A3A$ + ^31BF3856AD364E35$ + ^89845DCD8080CC91$ + ^71E9BCE111E9429C$ + ^8F50407C4E9E73B6$ + ^E361AF139669C375$ + + + + + True + True + True + False + + + + + + + + + + + + + True + false + False + False + + + + \ No newline at end of file diff --git a/src/unit.tests.x86.runsettings b/src/unit.tests.x86.runsettings new file mode 100644 index 000000000..e9a92a215 --- /dev/null +++ b/src/unit.tests.x86.runsettings @@ -0,0 +1,146 @@ + + + + + + .\TestResults + + + x86 + + + Framework45 + + + + + + + + + + + + + + + + + .*\.dll$ + .*\.exe$ + + + .*CPPUnitTestFramework.* + + + + + + + + ^Fabrikam\.UnitTest\..* + ^std::.* + ^ATL::.* + .*::__GetTestMethodInfo.* + ^Microsoft::VisualStudio::CppCodeCoverageFramework::.* + ^Microsoft::VisualStudio::CppUnitTestFramework::.* + ^WEX::.* + ^wil::.* + ^wistd::.* + ^.*Test.*::.* + ^.*::.*TAEF.* + + + + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.Runtime\.CompilerServices.CompilerGeneratedAttribute$ + ^System\.CodeDom\.Compiler.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis.ExcludeFromCodeCoverageAttribute$ + + + + + + + .*\\atlmfc\\.* + .*\\vctools\\.* + .*\\public\\sdk\\.* + .*\\microsoft sdks\\.* + .*\\windows kits\\.* + .*\\um\\.* + .*\\ucrt\\.* + .*\\vc\\include\\.* + .*\\ft_.*\\.* + .*\\ut_.*\\.* + + + + + + + + + + + ^B77A5C561934E089$ + ^B03F5F7F11D50A3A$ + ^31BF3856AD364E35$ + ^89845DCD8080CC91$ + ^71E9BCE111E9429C$ + ^8F50407C4E9E73B6$ + ^E361AF139669C375$ + + + + + True + True + True + False + + + + + + + + + + + + + True + false + False + False + + + + \ No newline at end of file diff --git a/src/wap-common.build.post.props b/src/wap-common.build.post.props new file mode 100644 index 000000000..b375ae3bb --- /dev/null +++ b/src/wap-common.build.post.props @@ -0,0 +1,14 @@ + + + + + + + Retail + Debug + $(PlatformShortName) + true + + + + diff --git a/src/wap-common.build.pre.props b/src/wap-common.build.pre.props new file mode 100644 index 000000000..93bd26345 --- /dev/null +++ b/src/wap-common.build.pre.props @@ -0,0 +1,48 @@ + + + + 15.0 + + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + + true + + + + + 10.0.17134.0 + 10.0.17134.0 + en-US + + + diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis new file mode 100644 index 000000000..d2d618303 --- /dev/null +++ b/tools/ConsoleTypes.natvis @@ -0,0 +1,75 @@ + + + + + {{Index:{_index}}} + {{Default}} + {{RGB:{_red},{_green},{_blue}}} + + + + + + {{FG:{_foreground},BG:{_background},{_wAttrLegacy}, Bold}} + {{FG:{_foreground},BG:{_background},{_wAttrLegacy}, Normal}} + + _wAttrLegacy + _isBold + _foreground + _background + + + + + + {{LT({_sr.Left}, {_sr.Top}) RB({_sr.Right}, {_sr.Bottom}) [{_sr.Right-_sr.Left+1} x { _sr.Bottom-_sr.Top+1}]}} + + _sr + + + + + {{{X},{Y}}} + + + + {{LT({Left}, {Top}) RB({Right}, {Bottom}) In:[{Right-Left+1} x {Bottom-Top+1}] Ex:[{Right-Left} x {Bottom-Top}]}} + + + + Stored Glyph, go to UnicodeStorage. + {_wch,X} Single + {_wch,X} Lead + {_wch,X} Trail + + + + {{ size={_cchRowWidth} }} + + _list + + + + + {{ wrap={_wrapForced} padded={_doubleBytePadded} }} + + _data + + + + + {{ id={_id} width={_rowWidth} }} + + _charRow + _attrRow + + + + + + _Mypair._Myval2 + + + diff --git a/tools/OpenConsole.psm1 b/tools/OpenConsole.psm1 new file mode 100644 index 000000000..953f72007 --- /dev/null +++ b/tools/OpenConsole.psm1 @@ -0,0 +1,241 @@ + +# The project's root directory. +Set-Item -force -path "env:OpenConsoleRoot" -value "$PSScriptRoot\.." + +#.SYNOPSIS +# Grabs all environment variable set after vcvarsall.bat is called and pulls +# them into the Powershell environment. +function Set-MsbuildDevEnvironment() +{ + $path = "$env:VS140COMNTOOLS\..\.." + pushd $path + cmd /c "vcvarsall.bat&set" | foreach { + if ($_ -match "=") + { + $s = $_.Split("="); + Set-Item -force -path "env:\$($s[0])" -value "$($s[1])" + } + } + popd + Write-Host "Dev environment variables set" -ForegroundColor Green +} + +#.SYNOPSIS +# Runs a Taef test suite in a new OpenConsole window. +# +#.PARAMETER OpenConsolePath +# Path to the OpenConsole.exe to run. +# +#.PARAMETER $TaefPath +# Path to the taef.exe to run. +# +#.PARAMETER $TestDll +# Path to the test DLL to run with Taef. +# +#.PARAMETER $TaefArgs +# Any arguments to path to Taef. +function Invoke-TaefInNewWindow() +{ + [CmdletBinding()] + Param ( + [parameter(Mandatory=$true)] + [string]$OpenConsolePath, + + [parameter(Mandatory=$true)] + [string]$TaefPath, + + [parameter(Mandatory=$true)] + [string]$TestDll, + + [parameter(Mandatory=$false)] + [string]$TaefArgs + ) + + Start-Process $OpenConsolePath -Wait -ArgumentList "powershell.exe $TaefPath $TestDll $TaefArgs; Read-Host 'Press enter to continue...'" +} + +#.SYNOPSIS +# Runs OpenConsole's tests. Will only run unit tests by default. Each ft test is +# run in its own window. Note that the uia tests will move the mouse around, so +# it must be left alone for the duration of the test. +# +#.PARAMETER AllTests +# When set, all tests will be run. +# +#.PARAMETER FTOnly +# When set, only ft tests will be run. +# +#.PARAMETER Test +# Can be used to specify that only a particular test should be run. +# Current values allowed are: host, interactivityWin32, terminal, adapter, +# feature, uia, textbuffer. +# +#.PARAMETER TaefArgs +# Used to pass any additional arguments to the test runner. +# +#.PARAMETER Platform +# The platform of the OpenConsole tests to run. Can be "x64" or "x86". +# Defaults to "x64". +# +#.PARAMETER Configuration +# The configuration of the OpenConsole tests to run. Can be "Debug" or +# "Release". Defaults to "Debug". +function Invoke-OpenConsoleTests() +{ + [CmdletBinding()] + Param ( + [parameter(Mandatory=$false)] + [switch]$AllTests, + + [parameter(Mandatory=$false)] + [switch]$FTOnly, + + [parameter(Mandatory=$false)] + [ValidateSet('host', 'interactivityWin32', 'terminal', 'adapter', 'feature', 'uia', 'textbuffer')] + [string]$Test, + + [parameter(Mandatory=$false)] + [string]$TaefArgs, + + [parameter(Mandatory=$false)] + [ValidateSet('x64', 'x86')] + [string]$Platform = "x64", + + [parameter(Mandatory=$false)] + [ValidateSet('Debug', 'Release')] + [string]$Configuration = "Debug" + + ) + + if (($AllTests -and $FTOnly) -or ($AllTests -and $Test) -or ($FTOnly -and $Test)) + { + Write-Host "Invalid combination of flags" -ForegroundColor Red + return + } + $OpenConsolePlatform = $Platform + if ($Platform -eq 'x86') + { + $OpenConsolePlatform = 'Win32' + } + $OpenConsolePath = "$env:OpenConsoleroot\bin\$OpenConsolePlatform\$Configuration\OpenConsole.exe" + $RunTePath = "$env:OpenConsoleRoot\tools\runte.cmd" + $TaefExePath = "$env:OpenConsoleRoot\packages\Taef.Redist.Wlk.10.30.180808002\build\binaries\$Platform\te.exe" + $BinDir = "$env:OpenConsoleRoot\bin\$OpenConsolePlatform\$Configuration" + [xml]$TestConfig = Get-Content "$env:OpenConsoleRoot\tools\tests.xml" + + # check if WinAppDriver needs to be started + $WinAppDriverExe = $null + if ($AllTests -or $FtOnly -or $Test -eq "uia") + { + $WinAppDriverExe = [Diagnostics.Process]::Start("$env:OpenConsoleRoot\dep\WinAppDriver\WinAppDriver.exe") + } + + # select tests to run + if ($AllTests) + { + $TestsToRun = $TestConfig.tests.test + } + elseif ($FTOnly) + { + $TestsToRun = $TestConfig.tests.test | Where-Object { $_.type -eq "ft" } + } + elseif ($Test) + { + $TestsToRun = $TestConfig.tests.test | Where-Object { $_.name -eq $Test } + } + else + { + # run unit tests by default + $TestsToRun = $TestConfig.tests.test | Where-Object { $_.type -eq "unit" } + } + + # run selected tests + foreach ($t in $TestsToRun) + { + if ($t.type -eq "unit") + { + & $TaefExePath "$BinDir\$($t.binary)" $TaefArgs + } + elseif ($t.type -eq "ft") + { + Invoke-TaefInNewWindow -OpenConsolePath $OpenConsolePath -TaefPath $TaefExePath -TestDll "$BinDir\$($t.binary)" -TaefArgs $TaefArgs + } + else + { + Write-Host "Invalid test type $t.type for test: $t.name" -ForegroundColor Red + return + } + } + + # stop running WinAppDriver if it was launched + if ($WinAppDriverExe) + { + Stop-Process -Id $WinAppDriverExe.Id + } +} + + +#.SYNOPSIS +# Builds OpenConsole.sln using msbuild. Any arguments get passed on to msbuild. +function Invoke-OpenConsoleBuild() +{ + & "$env:OpenConsoleRoot\dep\nuget\nuget.exe" restore "$env:OpenConsoleRoot\OpenConsole.sln" + msbuild.exe "$env:OpenConsoleRoot\OpenConsole.sln" @args +} + +#.SYNOPSIS +# Launches an OpenConsole process. +# +#.PARAMETER Platform +# The platform of the OpenConsole executable to launch. Can be "x64" or "x86". +# Defaults to "x64". +# +#.PARAMETER Configuration +# The configuration of the OpenConsole executable to launch. Can be "Debug" or +# "Release". Defaults to "Debug". +function Start-OpenConsole() +{ + [CmdletBinding()] + Param ( + [parameter(Mandatory=$false)] + [string]$Platform = "x64", + + [parameter(Mandatory=$false)] + [string]$Configuration = "Debug" + ) + if ($Platform -like "x86") + { + $Platform = "Win32" + } + & "$env:OpenConsoleRoot\bin\$Platform\$Configuration\OpenConsole.exe" +} + +#.SYNOPSIS +# Launches an OpenConsole process and attaches the default debugger. +# +#.PARAMETER Platform +# The platform of the OpenConsole executable to launch. Can be "x64" or "x86". +# Defaults to "x64". +# +#.PARAMETER Configuration +# The configuration of the OpenConsole executable to launch. Can be "Debug" or +# "Release". Defaults to "Debug". +function Debug-OpenConsole() +{ + [CmdletBinding()] + Param ( + [parameter(Mandatory=$false)] + [string]$Platform = "x64", + + [parameter(Mandatory=$false)] + [string]$Configuration = "Debug" + ) + if ($Platform -like "x86") + { + $Platform = "Win32" + } + $process = [Diagnostics.Process]::Start("$env:OpenConsoleRoot\bin\$Platform\$Configuration\OpenConsole.exe") + Debug-Process -Id $process.Id +} + +Export-ModuleMember -Function Set-MsbuildDevEnvironment,Invoke-OpenConsoleTests,Invoke-OpenConsoleBuild,Start-OpenConsole,Debug-OpenConsole diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..5f52410e3 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,63 @@ +# OpenConsole Tools + +These are a collection of tools and scripts to make your life building the +OpenConsole project easier. Many of them are designed to be functional clones of +tools that we used to use when developing inside the Windows build system. + +## Razzle + +This is a script that quickly sets up your environment variables so that these +tools can run easily. It's named after another script used by Windows developers +to similar effect. + - It adds msbuild to your path. + - It adds the tools directory to your path as well, so all these scripts are + easily available. + - It executes `\tools\.razzlerc.cmd` to add any other personal configuration to + your environment as well, or creates one if it doesn't exist. + - It sets up the default build configuration to be 'Debug'. If you'd like to + manually specify a build configuration, pass the parameter `dbg` for Debug, and + `rel` for Release. + +## bcz + +`bcz` can quick be used to clean and build the project. By default, it builds +the `%DEFAULT_CONFIGURATION%` configuration, which + + - `bcz dbg` can be used to manually build the Debug configuration. + - `bcz rel` can be used to manually build the Release configuration. + + +## opencon (and openbash, openps) + +`opencon` can be used to launch the **last built** OpenConsole binary. If given an +argument, it will try and run that program in the launched window. Otherwise it +will default to cmd.exe. + +`openbash` is similar, it immediately launches bash.exe (the Windows Subsystem +for Linux entrypoint) in your `~` directory. + +Likewise, `openps` launches powershell. + +## testcon, runut, runft +`runut` will automatically run all of the unit tests through TAEF. `runft` will +run the feature tests, and `testcon` runs all of them. They'll pass any +arguments through to TAEF, so you can more finely control the testing. + +A recommended workflow is the following command: +``` +bcz dbg && runut /name:** +``` +Where `` is the name of the test testing the relevant feature area +you're working on. For example, if I was working on the VT Mouse input support, +I would use `MouseInputTest` as that string, to isolate the mouse input tests. +If you'd like to run all the tests, just ignore the `/name` param: +`bcz dbg && runut` + +To make sure your code is ready for a pull request, run the build, then launch +the built console, then run the tests in it. The built console will inherit all +of the razzle environment, so you can immediately start using the macros: + 1. `bcz` + 2. `opencon` + 3. `testcon` (in the new console window) + +If they all come out green, then you're ready for a pull request! diff --git a/tools/WindbgExtension.js b/tools/WindbgExtension.js new file mode 100644 index 000000000..2a2c3e622 --- /dev/null +++ b/tools/WindbgExtension.js @@ -0,0 +1,25 @@ + +// Usage from within windbg: +// 1. load symbols for the module containing conhost +// 2. .load jsprovider.dll +// 3. .scriptload \WindbgExtension.js +// +// From here, commands can be called. For example, to get information about ServiceLocator::s_globals: +// dx @$scriptContents.ServiceLocatorVar("s_globals") + + +function initializeScript() +{ + host.diagnostics.debugLog("***> OpenConsole debugger extension loaded \n"); +} + +// Routine Description: +// - Displays information about a field found in the ServiceLocator class +// Arguments: +// - varName - the variable to display information for +// Return Value: +// - debugger object used to display information about varName +function ServiceLocatorVar(varName) +{ + return host.namespace.Debugger.Utility.Control.ExecuteCommand("dx Microsoft::Console::Interactivity::ServiceLocator::" + varName); +} diff --git a/tools/bcz.cmd b/tools/bcz.cmd new file mode 100644 index 000000000..c14d2c442 --- /dev/null +++ b/tools/bcz.cmd @@ -0,0 +1,55 @@ +@echo off + +rem bcz - Clean and build the project +rem This is another script to help Microsoft developers feel at home working on the openconsole project. + +if (%_LAST_BUILD_CONF%)==() ( + set _LAST_BUILD_CONF=%DEFAULT_CONFIGURATION% +) + +set _MSBUILD_TARGET=Clean,Build + +:ARGS_LOOP +if (%1) == () goto :POST_ARGS_LOOP +if (%1) == (dbg) ( + echo Manually building debug + set _LAST_BUILD_CONF=Debug +) +if (%1) == (rel) ( + echo Manually building release + set _LAST_BUILD_CONF=Release +) +if (%1) == (no_clean) ( + set _MSBUILD_TARGET=Build +) +shift +goto :ARGS_LOOP + +:POST_ARGS_LOOP +echo Starting build... + +nuget.exe restore %OPENCON%\OpenConsole.sln + +rem /p:AppxBundle=Never prevents us from building the appxbundle from the commandline. +rem We don't want to do this from a debug build, because it takes ages, so disable it. +rem if you want the appx, build release + +set _APPX_ARGS= + +if (%_LAST_BUILD_CONF%) == (Debug) ( + echo Skipping building appx... + set _APPX_ARGS=/p:AppxBundle=false +) else ( + echo Building Appx... +) + +set _BUILD_CMDLINE=%MSBUILD% %OPENCON%\OpenConsole.sln /t:%_MSBUILD_TARGET% /m /p:Configuration=%_LAST_BUILD_CONF% /p:Platform=%ARCH% %_APPX_ARGS% + +echo %_BUILD_CMDLINE% +%_BUILD_CMDLINE% + +rem Cleanup unused variables here. Note we cannot use setlocal because we need to pass modified +rem _LAST_BUILD_CONF out to OpenCon.cmd later. +rem +set _MSBUILD_TARGET= +set _BIN_=%~dp0\bin\%PLATFORM%\%_LAST_BUILD_CONF% diff --git a/tools/bz.cmd b/tools/bz.cmd new file mode 100644 index 000000000..f2c3c45f6 --- /dev/null +++ b/tools/bz.cmd @@ -0,0 +1,6 @@ +@echo off + +rem bcz - Build the project without clean it first +rem This is another script to help Microsoft developers feel at home working on the openconsole project. + +call bcz no_clean %* diff --git a/tools/echokey.cmd b/tools/echokey.cmd new file mode 100644 index 000000000..90cc5cac3 --- /dev/null +++ b/tools/echokey.cmd @@ -0,0 +1,25 @@ +@echo off + +setlocal +set _last_build=%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF% + +if not exist %_last_build%\OpenConsole.exe ( + echo Could not locate the OpenConsole.exe in %_last_build%. Double check that it has been built and try again. + goto :eof +) +if not exist %_last_build%\conechokey.exe ( + echo Could not locate the conechokey.exe in %_last_build%. Double check that it has been built and try again. + goto :eof +) + +rem %_last_build%\conechokey.exe %* + +set _r=%random% +set copy_dir=OpenConsole\%_r% +rem Generate a unique name, so that we can debug multiple revisions of the binary at the same time if needed. + +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\OpenConsole.exe) > nul +(echo f | xcopy /Y %_last_build%\conechokey.exe %TEMP%\%copy_dir%\conechokey.exe) > nul + +rem start %TEMP%\%copy_dir%\OpenConsole.exe %TEMP%\%copy_dir%\conechokey.exe %* +%TEMP%\%copy_dir%\conechokey.exe %* diff --git a/tools/openbash.cmd b/tools/openbash.cmd new file mode 100644 index 000000000..1026df362 --- /dev/null +++ b/tools/openbash.cmd @@ -0,0 +1,7 @@ +@echo off + +rem openbash - launch bash running in the generated OpenConsole binary +rem Runs the OpenConsole.exe binary generated by the build in the debug directory. +rem Defaults to your `~` directory. + +call opencon bash ~ \ No newline at end of file diff --git a/tools/opencas.cmd b/tools/opencas.cmd new file mode 100644 index 000000000..311dc4730 --- /dev/null +++ b/tools/opencas.cmd @@ -0,0 +1,29 @@ +@echo off + +rem opencon - launch the openconsole binary. +rem Runs the OpenConsole.exe binary generated by the build in the debug directory. +rem Passes any args along. + +if not exist %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\OpenConsole.exe ( + echo Could not locate the OpenConsole.exe in %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%. Double check that it has been built and try again. + goto :eof +) +if not exist %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\cascadia.exe ( + echo Could not locate the cascadia.exe in %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%. Double check that it has been built and try again. + goto :eof +) + +setlocal +rem Generate a unique name, so that we can debug multiple revisions of the binary at the same time if needed. +set rand_val=%random% +set _r=%random% +set _last_build=%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF% +set copy_dir=OpenConsole\%_r% + +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\OpenConsole.exe) > nul +(echo f | xcopy /Y %_last_build%\cascadia.exe %TEMP%\%copy_dir%\cascadia.exe) > nul +(echo f | xcopy /Y %_last_build%\VtPipeTerm.exe %TEMP%\%copy_dir%\VtPipeTerm.exe) > nul +(echo f | xcopy /Y %_last_build%\Nihilist.exe %TEMP%\%copy_dir%\Nihilist.exe) > nul +(echo f | xcopy /Y %_last_build%\console.dll %TEMP%\%copy_dir%\console.dll) > nul + +start %TEMP%\%copy_dir%\cascadia.exe %* diff --git a/tools/opencon.cmd b/tools/opencon.cmd new file mode 100644 index 000000000..96425a99a --- /dev/null +++ b/tools/opencon.cmd @@ -0,0 +1,25 @@ +@echo off + +rem opencon - launch the openconsole binary. +rem Runs the OpenConsole.exe binary generated by the build in the debug directory. +rem Passes any args along. + +if not exist %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\OpenConsole.exe ( + echo Could not locate the OpenConsole.exe in %OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%. Double check that it has been built and try again. + goto :eof +) + +setlocal +rem Generate a unique name, so that we can debug multiple revisions of the binary at the same time if needed. +set rand_val=%random% +set _r=%random% +set _last_build=%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF% +set copy_dir=OpenConsole\%_r% + +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\OpenConsole.exe) > nul +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\conhost.exe) > nul +(echo f | xcopy /Y %_last_build%\VtPipeTerm.exe %TEMP%\%copy_dir%\VtPipeTerm.exe) > nul +(echo f | xcopy /Y %_last_build%\Nihilist.exe %TEMP%\%copy_dir%\Nihilist.exe) > nul +(echo f | xcopy /Y %_last_build%\console.dll %TEMP%\%copy_dir%\console.dll) > nul + +start %TEMP%\%copy_dir%\OpenConsole.exe %* diff --git a/tools/openps.cmd b/tools/openps.cmd new file mode 100644 index 000000000..1fdcb98fb --- /dev/null +++ b/tools/openps.cmd @@ -0,0 +1,6 @@ +@echo off + +rem openps - launch Powershell running in the generated OpenConsole binary +rem Runs the OpenConsole.exe binary generated by the build in the debug directory. + +call opencon powershell.exe \ No newline at end of file diff --git a/tools/openterm.cmd b/tools/openterm.cmd new file mode 100644 index 000000000..92333249f --- /dev/null +++ b/tools/openterm.cmd @@ -0,0 +1,33 @@ +@echo off + +rem opencon - launch the openconsole binary. +rem Runs the OpenConsole.exe binary generated by the build in the debug directory. +rem Passes any args along. + +if not exist %OPENCON%\%PLATFORM%\%_LAST_BUILD_CONF%\CascadiaWin32.exe ( + echo Could not locate the CascadiaWin32.exe in %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%. Double check that it has been built and try again. + goto :eof +) + +setlocal +rem Generate a unique name, so that we can debug multiple revisions of the binary at the same time if needed. +set rand_val=%random% +set _r=%random% +set _last_build=%OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF% +set _last_cascadia_build=%OPENCON%\%PLATFORM%\%_LAST_BUILD_CONF% +set copy_dir=OpenConsole\%_r% +set cascadia_copy_dir=OpenConsole\%_r%\Cascadia + +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\OpenConsole.exe) > nul +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%cascadia_copy_dir%\conhost.exe) > nul +(echo f | xcopy /Y %_last_build%\VtPipeTerm.exe %TEMP%\%copy_dir%\VtPipeTerm.exe) > nul +(echo f | xcopy /Y %_last_build%\Nihilist.exe %TEMP%\%copy_dir%\Nihilist.exe) > nul +(echo f | xcopy /Y %_last_build%\console.dll %TEMP%\%copy_dir%\console.dll) > nul +(echo f | xcopy /Y %_last_cascadia_build%\CascadiaWin32.exe %TEMP%\%cascadia_copy_dir%\CascadiaWin32.exe) > nul +(echo f | xcopy /Y %_last_cascadia_build%\TerminalConnection.dll %TEMP%\%cascadia_copy_dir%\TerminalConnection.dll) > nul +(echo f | xcopy /Y %_last_cascadia_build%\TerminalControl.dll %TEMP%\%cascadia_copy_dir%\TerminalControl.dll) > nul +(echo f | xcopy /Y %_last_cascadia_build%\TerminalSettings.dll %TEMP%\%cascadia_copy_dir%\TerminalSettings.dll) > nul +(echo f | xcopy /Y %_last_cascadia_build%\TerminalApp.dll %TEMP%\%cascadia_copy_dir%\TerminalApp.dll) > nul + +echo Launching %TEMP%\%cascadia_copy_dir%\CascadiaWin32.exe... +start %TEMP%\%cascadia_copy_dir%\CascadiaWin32.exe %* diff --git a/tools/openvt.cmd b/tools/openvt.cmd new file mode 100644 index 000000000..c3bc3d387 --- /dev/null +++ b/tools/openvt.cmd @@ -0,0 +1,25 @@ +@echo off + +rem openvt - launch the vtterm binary +rem Runs the VtPipeTerm.exe binary generated by the build in the debug directory. +rem Passes any args along. + +setlocal +set _last_build=%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF% + +if not exist %_last_build%\VtPipeTerm.exe ( + echo Could not locate the VtPipeTerm.exe in %_last_build%. Double check that it has been built and try again. + goto :eof +) + +set _r=%random% +set copy_dir=OpenConsole\%_r% +rem Generate a unique name, so that we can debug multiple revisions of the binary at the same time if needed. + +(echo f | xcopy /Y %_last_build%\OpenConsole.exe %TEMP%\%copy_dir%\conhost.exe) > nul +(echo f | xcopy /Y %_last_build%\console.dll %TEMP%\%copy_dir%\console.dll) > nul +(echo f | xcopy /Y %_last_build%\VtPipeTerm.exe %TEMP%\%copy_dir%\VtPipeTerm.exe) > nul +(echo f | xcopy /Y %_last_build%\Nihilist.exe %TEMP%\%copy_dir%\Nihilist.exe) > nul + +start %TEMP%\%copy_dir%\conhost.exe %TEMP%\%copy_dir%\VtPipeTerm.exe %* +echo Launching %TEMP%\%copy_dir%\VtPipeTerm.exe... diff --git a/tools/razzle.cmd b/tools/razzle.cmd new file mode 100644 index 000000000..c09c3159a --- /dev/null +++ b/tools/razzle.cmd @@ -0,0 +1,111 @@ +@echo off + +rem Open Console build environment setup +rem Adds msbuild to your path, and adds the open\tools directory as well +rem This recreates what it's like to be an actual windows developer! + +rem skip the setup if we're already ready. +if not "%OpenConBuild%" == "" goto :END + +rem Add path to MSBuild Binaries +set MSBUILD=() +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe" ( + set MSBUILD="%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe" + goto :FOUND_MSBUILD +) +if exist "%ProgramFiles%\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe" ( + set MSBUILD="%ProgramFiles%\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe" + goto :FOUND_MSBUILD +) +if exist "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe" ( + set MSBUILD="%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe" + goto :FOUND_MSBUILD +) +if exist "%ProgramFiles%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe" ( + set MSBUILD="%ProgramFiles%\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe" + goto :FOUND_MSBUILD +) +if exist "%ProgramFiles(x86)%\MSBuild\14.0\bin" ( + set MSBUILD="%ProgramFiles(x86)%\MSBuild\14.0\bin\msbuild.exe" + goto :FOUND_MSBUILD +) +if exist "%ProgramFiles%\MSBuild\14.0\bin" ( + set MSBUILD="%ProgramFiles%\MSBuild\14.0\bin\msbuild.exe" + goto :FOUND_MSBUILD +) + +if %MSBUILD%==() ( + echo "Could not find MsBuild on your machine. It may be installed somewhere else." + goto :EXIT +) + +:FOUND_MSBUILD + +rem Add Opencon build scripts to path +set PATH=%PATH%;%~dp0; + +rem add some helper envvars - The Opencon root, and also the processor arch, for output paths +set OPENCON_TOOLS=%~dp0 +rem The opencon root is at ...\open\tools\, without the last 7 chars ('\tools\') +set OPENCON=%OPENCON_TOOLS:~0,-7% + +rem Add nuget to PATH +set PATH=%PATH%%OPENCON%\dep\nuget; + +if "%PROCESSOR_ARCHITECTURE%" == "AMD64" ( + set ARCH=x64 + set PLATFORM=x64 +) else ( + set ARCH=x86 + set PLATFORM=Win32 +) +set DEFAULT_CONFIGURATION=Debug + +rem call .razzlerc - for your generic razzle environment stuff +if exist "%OPENCON_TOOLS%\.razzlerc.cmd" ( + call %OPENCON_TOOLS%\.razzlerc.cmd +) else ( + ( + echo @echo off + echo. + echo rem This is your razzlerc file. It can be used for default dev environment setup. + ) > %OPENCON_TOOLS%\.razzlerc.cmd +) + +rem if there are args, run them. This can be used for additional env. customization, +rem especially on a per shortcut basis. +:ARGS_LOOP +if (%1) == () goto :POST_ARGS_LOOP +if (%1) == (dbg) ( + set DEFAULT_CONFIGURATION=Debug + shift + goto :ARGS_LOOP +) +if (%1) == (rel) ( + set DEFAULT_CONFIGURATION=Release + shift + goto :ARGS_LOOP +) +if (%1) == (x86) ( + set ARCH=x86 + set PLATFORM=Win32 + shift + goto :ARGS_LOOP +) +if exist %1 ( + call %1 +) else ( + echo Could not locate "%1" +) +shift +goto :ARGS_LOOP + +:POST_ARGS_LOOP +set TAEF=%OPENCON%\packages\Taef.Redist.Wlk.10.30.180808002\build\binaries\%ARCH%\TE.exe +rem Set this envvar so setup won't repeat itself +set OpenConBuild=true + +:END +echo The dev environment is ready to go! + +:EXIT diff --git a/tools/runft.cmd b/tools/runft.cmd new file mode 100644 index 000000000..bddc6230e --- /dev/null +++ b/tools/runft.cmd @@ -0,0 +1,7 @@ +@echo off + +rem Run the console feature tests. + +call %TAEF% ^ + %OPENCON%\bin\%ARCH%\Debug\ConHost.Feature.Tests.dll ^ + %* diff --git a/tools/runuia.cmd b/tools/runuia.cmd new file mode 100644 index 000000000..8b6501769 --- /dev/null +++ b/tools/runuia.cmd @@ -0,0 +1,14 @@ +@echo off + +rem Run the console UI Automation tests. + +rem Get AppDriver going first... You'll have to close it yourself. + +start %OPENCON%\dep\WinAppDriver\WinAppDriver.exe + +rem Then run the tests. + +call %TAEF% ^ + %OPENCON%\bin\%ARCH%\Debug\Conhost.UIA.Tests.dll ^ + /p:VtApp=%OPENCON%\bin\%ARCH%\Debug\VtApp.exe ^ + %* diff --git a/tools/runut.cmd b/tools/runut.cmd new file mode 100644 index 000000000..4aaf99497 --- /dev/null +++ b/tools/runut.cmd @@ -0,0 +1,11 @@ +@echo off + +rem Run the console unit tests. + +call %TAEF% ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\Conhost.Unit.Tests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\TextBuffer.Unittests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\Conhost.Interactivity.Win32.Unit.Tests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\ConParser.Unit.Tests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\ConAdapter.Unit.Tests.dll ^ + %* diff --git a/tools/testcon.cmd b/tools/testcon.cmd new file mode 100644 index 000000000..db5c0bb75 --- /dev/null +++ b/tools/testcon.cmd @@ -0,0 +1,18 @@ +@echo off + +rem Run the console tests. + +rem I couldn't come up with a good way to combine the ft and ut lists +rem automatically, so this is manually updated for now :( + +rem NOTE: UIA tests are still seperate because they takeover your mouse/keyboard. +rem Run them on your own. + +call %TAEF% ^ + %OPENCON%\bin\%ARCH%\Debug\ConHost.Api.Tests.dll ^ + %OPENCON%\bin\%ARCH%\Debug\ConHost.Resize.Tests.dll ^ + %OPENCON%\bin\%ARCH%\Debug\Conhost.Unit.Tests.dll ^ + %OPENCON%\bin\%ARCH%\Debug\ConHost.CJK.Tests.dll ^ + %OPENCON%\bin\%ARCH%\Debug\ConParser.Unit.Tests.dll ^ + %OPENCON%\bin\%ARCH%\Debug\ConAdapter.Unit.Tests.dll ^ + %* diff --git a/tools/tests.xml b/tools/tests.xml new file mode 100644 index 000000000..5350f8e62 --- /dev/null +++ b/tools/tests.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tools/vso_ut.cmd b/tools/vso_ut.cmd new file mode 100644 index 000000000..9b68aac4c --- /dev/null +++ b/tools/vso_ut.cmd @@ -0,0 +1,29 @@ +@echo off + +@rem This script can be used for running the unit tests just the same way +@rem they'll run on VSO. + +setlocal +set VSTEST_PATH="C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe" + +set test_cmd=%VSTEST_PATH% ^ + "%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\ConAdapter.Unit.Tests.dll" ^ + "%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\Conhost.Interactivity.Win32.Unit.Tests.dll" ^ + "%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\Conhost.Unit.Tests.dll" ^ + "%OPENCON%\bin\%ARCH%\%_LAST_BUILD_CONF%\ConParser.Unit.Tests.dll" ^ + /Settings:"%OPENCON%\src\unit.tests.%ARCH%.runsettings" ^ + /EnableCodeCoverage ^ + /logger:trx ^ + /TestAdapterPath:"%OPENCON%" ^ + %* + +@rem Note: You can't use the same /name*test* parameters to regex find tests with this tester. +@rem you instead need to use /Tests:[] +@rem ex: vso_ut /Tests:DtorTest +@rem will match any test with "DtorTest" in the name. Note that Test Discovery will take FOREVER. + +echo Starting tests with the following commandline in a new window: +echo ``` +echo %test_cmd% +echo ``` +start cmd /k "%test_cmd%"